12 Commits

Author SHA1 Message Date
41c58b400f WIP: keep adding projects
Signed-off-by: Nikolai Rodionov <iam@allanger.xyz>
2026-05-26 16:47:00 +02:00
21a4c53e0f Implement list projects
Signed-off-by: Nikolai Rodionov <iam@allanger.xyz>
2026-05-22 11:23:04 +02:00
4e617c90ef Add basic health checks
Signed-off-by: Nikolai Rodionov <iam@allanger.xyz>
2026-05-22 09:39:25 +02:00
e132e143bd WIP: Fix the create project test
Signed-off-by: Nikolai Rodionov <iam@allanger.xyz>
2026-05-21 17:12:44 +02:00
a4ab4fa6c6 WIP: Fix the create project test
Signed-off-by: Nikolai Rodionov <iam@allanger.xyz>
2026-05-21 17:03:59 +02:00
891b9854b5 WIP: Add a migration for project membmership
Signed-off-by: Nikolai Rodionov <iam@allanger.xyz>
2026-05-21 16:48:12 +02:00
d1d4cba05b WIP: Adding list project
Signed-off-by: Nikolai Rodionov <iam@allanger.xyz>
2026-05-21 14:55:34 +02:00
9c558d601b WIP: Keep writing projectS
Signed-off-by: Nikolai Rodionov <iam@allanger.xyz>
2026-05-20 13:37:48 +02:00
2285bb10ae WIP: Adding projects
Some checks failed
ci/woodpecker/push/build Pipeline failed
Signed-off-by: Nikolai Rodionov <iam@allanger.xyz>
2026-05-19 14:04:26 +02:00
28235765cd Add tests for the transactions
All checks were successful
ci/woodpecker/push/build Pipeline was successful
Signed-off-by: Nikolai Rodionov <iam@allanger.xyz>
2026-05-19 11:32:34 +02:00
c8d3112fbd Restructure the projec and start adding projects
All checks were successful
ci/woodpecker/push/build Pipeline was successful
Signed-off-by: Nikolai Rodionov <iam@allanger.xyz>
2026-05-19 10:55:53 +02:00
25ebb4805e WIP: Start adding projects
Signed-off-by: Nikolai Rodionov <allanger@badhouseplants.net>
2026-05-18 22:15:28 +02:00
33 changed files with 1108 additions and 293 deletions

View File

@@ -61,6 +61,30 @@ tasks:
SOFTPLAYER_DB_CONNECTION_STRING: postgres://softplayer:qwertyu9@localhost:30432/softplayer?sslmode=disable SOFTPLAYER_DB_CONNECTION_STRING: postgres://softplayer:qwertyu9@localhost:30432/softplayer?sslmode=disable
cmd: go run main.go migrate --migrations-path=file://migrations cmd: go run main.go migrate --migrations-path=file://migrations
force-migration:
desc: Force migrate to a desired version
vars:
SOFTPLAYER_DB_CONNECTION_STRING: postgres://softplayer:qwertyu9@localhost:30432/softplayer?sslmode=disable
cmd: "{{ .MIGRATE }} -path=./migrations -database={{ .SOFTPLAYER_DB_CONNECTION_STRING }} force {{ .CLI_ARGS }}"
deps:
- migrate
down-migrations:
desc: Roll back all migrations
vars:
SOFTPLAYER_DB_CONNECTION_STRING: postgres://softplayer:qwertyu9@localhost:30432/softplayer?sslmode=disable
cmd: "{{ .MIGRATE }} -path=./migrations -database={{ .SOFTPLAYER_DB_CONNECTION_STRING }} down -all"
deps:
- migrate
drop-migrations:
desc: Drop migrations
vars:
SOFTPLAYER_DB_CONNECTION_STRING: postgres://softplayer:qwertyu9@localhost:30432/softplayer?sslmode=disable
cmd: "{{ .MIGRATE }} -path=./migrations -database={{ .SOFTPLAYER_DB_CONNECTION_STRING }} drop"
deps:
- migrate
run-server-dev: run-server-dev:
desc: Run the local dev server desc: Run the local dev server
deps: deps:
@@ -113,6 +137,8 @@ tasks:
desc: Add a new database migration desc: Add a new database migration
silent: true silent: true
cmd: "{{.MIGRATE}} create -dir migrations -ext sql {{.CLI_ARGS}}" cmd: "{{.MIGRATE}} create -dir migrations -ext sql {{.CLI_ARGS}}"
deps:
- migrate
# Install required tools # Install required tools
localbin: localbin:
@@ -132,6 +158,7 @@ tasks:
TARGET: "{{.MIGRATE}}" TARGET: "{{.MIGRATE}}"
PACKAGE: github.com/golang-migrate/migrate/v4/cmd/migrate PACKAGE: github.com/golang-migrate/migrate/v4/cmd/migrate
VERSION: latest VERSION: latest
TAGS: "postgres"
go-install-tool: go-install-tool:
internal: true internal: true
@@ -150,7 +177,13 @@ tasks:
echo "Downloading $PACKAGE" echo "Downloading $PACKAGE"
rm -f "$TARGET" rm -f "$TARGET"
GOBIN="{{.LOCALBIN}}" go install "$PACKAGE" TAGS="{{.TAGS}}"
if [ -n "$TAGS" ]; then
echo "Using build tags: $TAGS"
GOBIN="{{.LOCALBIN}}" go install -tags "$TAGS" "$PACKAGE"
else
GOBIN="{{.LOCALBIN}}" go install "$PACKAGE"
fi
mv "{{.LOCALBIN}}/$(basename "$TARGET")" "$VERSIONED" mv "{{.LOCALBIN}}/$(basename "$TARGET")" "$VERSIONED"
ln -sf "$(realpath "$VERSIONED")" "$TARGET" ln -sf "$(realpath "$VERSIONED")" "$TARGET"

View File

@@ -1,42 +0,0 @@
package v1
import (
"context"
"gitea.badhouseplants.net/softplayer/softplayer-backend/internal/controllers"
accounts "gitea.badhouseplants.net/softplayer/softplayer-go-proto/pkg/accounts/v1"
"google.golang.org/protobuf/types/known/emptypb"
)
func NewAccountServer(
accountsCtrl *controllers.AccountController,
authorizationCtrl *controllers.AuthController,
) *AccountsServer {
return &AccountsServer{
accountsCtrl: accountsCtrl,
authorizationCtrl: authorizationCtrl,
}
}
//var _ accounts.AccountsServiceServer = (*AccountsServer)(nil)
type AccountsServer struct {
accounts.UnimplementedAccountsServiceServer
accountsCtrl *controllers.AccountController
authorizationCtrl *controllers.AuthController
}
// IsEmailVerified implements [v1.AccountsServiceServer].
func (a *AccountsServer) IsEmailVerified(context.Context, *accounts.IsEmailVerifiedRequest) (*accounts.IsEmailVerifiedResponse, error) {
panic("unimplemented")
}
// RemoveSession implements [v1.AccountsServiceServer].
func (a *AccountsServer) RemoveSession(context.Context, *accounts.RemoveSessionRequest) (*emptypb.Empty, error) {
panic("unimplemented")
}
// TokenAuthorization implements [v1.AccountsServiceServer].
func (a *AccountsServer) TokenAuthorization(context.Context, *emptypb.Empty) (*emptypb.Empty, error) {
panic("unimplemented")
}

View File

@@ -1,78 +0,0 @@
package v1
import (
"context"
"errors"
"fmt"
"gitea.badhouseplants.net/softplayer/softplayer-backend/internal/controllers"
accounts "gitea.badhouseplants.net/softplayer/softplayer-go-proto/pkg/accounts/v1"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)
func NewRefreshSessionServer(
authorizationCtrl *controllers.AuthController,
) *RefreshSessionService {
return &RefreshSessionService{
authorizationCtrl: authorizationCtrl,
}
}
type RefreshSessionService struct {
accounts.UnimplementedRefreshSessionServiceServer
authorizationCtrl *controllers.AuthController
}
func (srv *RefreshSessionService) RefreshSession(ctx context.Context, in *accounts.RefreshSessionRequest) (*accounts.RefreshSessionResponse, error) {
fmt.Println(in.GetRefreshToken())
claims, err := srv.authorizationCtrl.ParseToken(in.GetRefreshToken())
if err != nil {
fmt.Println(err)
return nil, status.Error(codes.Aborted, "Invalid token is sent")
}
if claims.TokenType != controllers.TokenTypeRefresh {
return nil, status.Error(codes.Unauthenticated, "Invalid token")
}
session, err := srv.authorizationCtrl.GetSession(ctx, claims.TokenID)
if err != nil {
if errors.Is(err, controllers.ErrSessionNotFound) {
return nil, status.Error(codes.Unauthenticated, "Session doesn't exists")
}
return nil, status.Error(codes.Internal, "Somethings is broken on our side")
}
if session.UserID != claims.UserID {
return nil, status.Error(codes.Unauthenticated, "Invalid session")
}
accessToken, _, err := srv.authorizationCtrl.GenerateToken(&controllers.JWTData{
UserID: claims.UserID,
TokenType: controllers.TokenTypeAccess,
TokenAud: controllers.TokenAudWeb,
})
if err != nil {
return nil, status.Error(codes.Aborted, "Couldn't generate an access token")
}
refreshToken, tokenID, err := srv.authorizationCtrl.GenerateToken(&controllers.JWTData{
UserID: claims.UserID,
TokenType: controllers.TokenTypeRefresh,
TokenAud: controllers.TokenAudWeb,
})
if err != nil {
return nil, status.Error(codes.Aborted, "Couldn't generate an access token")
}
newSession := &controllers.Session{UserID: session.UserID}
if err := srv.authorizationCtrl.SaveSession(ctx, tokenID, newSession); err != nil {
return nil, status.Error(codes.Aborted, "Couldn't store session")
}
return &accounts.RefreshSessionResponse{TokenPair: &accounts.TokenPair{
AccessToken: accessToken,
RefreshToken: refreshToken,
}}, nil
}

View File

@@ -2,15 +2,16 @@ package cmd
import ( import (
"context" "context"
"database/sql"
"fmt" "fmt"
"net" "net"
"strings" "strings"
"time" "time"
v1 "gitea.badhouseplants.net/softplayer/softplayer-backend/api/v1" v1 "gitea.badhouseplants.net/softplayer/softplayer-backend/internal/api/v1"
"gitea.badhouseplants.net/softplayer/softplayer-backend/internal/controllers"
"gitea.badhouseplants.net/softplayer/softplayer-backend/internal/helpers/logger" "gitea.badhouseplants.net/softplayer/softplayer-backend/internal/helpers/logger"
"gitea.badhouseplants.net/softplayer/softplayer-backend/internal/helpers/postgres" "gitea.badhouseplants.net/softplayer/softplayer-backend/internal/helpers/postgres"
"gitea.badhouseplants.net/softplayer/softplayer-backend/internal/services"
accounts "gitea.badhouseplants.net/softplayer/softplayer-go-proto/pkg/accounts/v1" accounts "gitea.badhouseplants.net/softplayer/softplayer-go-proto/pkg/accounts/v1"
test "gitea.badhouseplants.net/softplayer/softplayer-go-proto/pkg/test/v1" test "gitea.badhouseplants.net/softplayer/softplayer-go-proto/pkg/test/v1"
tokens "gitea.badhouseplants.net/softplayer/softplayer-go-proto/pkg/tokens/v1" tokens "gitea.badhouseplants.net/softplayer/softplayer-go-proto/pkg/tokens/v1"
@@ -21,6 +22,9 @@ import (
_ "github.com/jackc/pgx/v5/stdlib" _ "github.com/jackc/pgx/v5/stdlib"
"github.com/redis/go-redis/v9" "github.com/redis/go-redis/v9"
"google.golang.org/grpc" "google.golang.org/grpc"
"google.golang.org/grpc/health"
healthgrpc "google.golang.org/grpc/health/grpc_health_v1"
healthpb "google.golang.org/grpc/health/grpc_health_v1"
"google.golang.org/grpc/reflection" "google.golang.org/grpc/reflection"
) )
@@ -38,12 +42,14 @@ type Server struct {
DBConnectionString string `env:"SOFTPLAYER_DB_CONNECTION_STRING"` DBConnectionString string `env:"SOFTPLAYER_DB_CONNECTION_STRING"`
RedisHost string `env:"SOFTPLAYER_REDIS_HOST"` RedisHost string `env:"SOFTPLAYER_REDIS_HOST"`
// JWT parameters // JWT parameters
RefreshTokenTTL time.Duration `default:"8h"` RefrestTokenTTL time.Duration `default:"8h"`
AccessTokenTTL time.Duration `default:"15m"` AccessTokenTTL time.Duration `default:"15m"`
JWTSecret string `default:"qwertyu9"` JWTSecret string `default:"qwertyu9"`
// Dev and logging // Dev and logging
Reflection bool `env:"SOFTPLAYER_REFLECTION" default:"false"` Reflection bool `env:"SOFTPLAYER_REFLECTION" default:"false"`
DevMode bool `env:"SOFTPLAYER_DEV_MODE" default:"false"` DevMode bool `env:"SOFTPLAYER_DEV_MODE" default:"false"`
// HealthChecks
HealthCheckInterval time.Duration `env:"SOFTPLAYER_HEALTH_CHECK_INTERVAL" default:"5s"`
} }
// Run the grpc backend server // Run the grpc backend server
@@ -68,10 +74,10 @@ func (cmd *Server) Run(ctx context.Context) error {
Addr: cmd.RedisHost, Addr: cmd.RedisHost,
}) })
authController := controllers.NewAuthController( authController := services.NewAuthController(
[]byte(cmd.JWTSecret), []byte(cmd.JWTSecret),
cmd.AccessTokenTTL, cmd.AccessTokenTTL,
cmd.RefreshTokenTTL, cmd.RefrestTokenTTL,
rdb, rdb,
) )
@@ -97,16 +103,16 @@ func (cmd *Server) Run(ctx context.Context) error {
reflection.Register(grpcServer) reflection.Register(grpcServer)
} }
tokenCtrl := &controllers.TokenController{ tokenCtrl := &services.TokenController{
DB: db, DB: db,
Redis: rdb, Redis: rdb,
} }
accountCtrl := &controllers.AccountController{ accountCtrl := &services.AccountController{
HashCost: cmd.HashCost, HashCost: cmd.HashCost,
DB: db, DB: db,
DevMode: cmd.DevMode, DevMode: cmd.DevMode,
RefreshTokenTTL: cmd.RefreshTokenTTL, RefreshTokenTTL: cmd.RefrestTokenTTL,
AccessTokenTTL: cmd.AccessTokenTTL, AccessTokenTTL: cmd.AccessTokenTTL,
JWTSecret: []byte(cmd.JWTSecret), JWTSecret: []byte(cmd.JWTSecret),
Redis: rdb, Redis: rdb,
@@ -114,12 +120,38 @@ func (cmd *Server) Run(ctx context.Context) error {
// Services that should be accessible for tokens should go here // Services that should be accessible for tokens should go here
accounts.RegisterAccountsServiceServer(grpcServer, v1.NewAccountServer(accountCtrl, authController)) accounts.RegisterAccountsServiceServer(grpcServer, v1.NewAccountServer(accountCtrl, authController))
accounts.RegisterPublicAccountsServiceServer(grpcServer, v1.NewPublicAccountServer(accountCtrl, authController))
accounts.RegisterRefreshSessionServiceServer(grpcServer, v1.NewRefreshSessionServer(authController))
test.RegisterTestServiceServer(grpcServer, v1.NewTestServer()) test.RegisterTestServiceServer(grpcServer, v1.NewTestServer())
test.RegisterPublicTestServiceServer(grpcServer, v1.NewPublicTestServer()) test.RegisterPublicTestServiceServer(grpcServer, v1.NewPublicTestServer())
tokens.RegisterTokensServiceServer(grpcServer, v1.NewTokensServer(tokenCtrl, authController)) tokens.RegisterTokensServiceServer(grpcServer, v1.NewTokensServer(tokenCtrl, authController))
tokens.RegisterPublicTokensServiceServer(grpcServer, v1.NewPublicTokensServer(tokenCtrl, authController)) tokens.RegisterPublicTokensServiceServer(grpcServer, v1.NewPublicTokensServer(tokenCtrl, authController))
accounts.RegisterPublicAccountsServiceServer(grpcServer, v1.NewPublicAccountServer(accountCtrl, authController))
healthcheck := health.NewServer()
healthgrpc.RegisterHealthServer(grpcServer, healthcheck)
go func() {
ticker := time.NewTicker(10 * time.Second)
defer ticker.Stop()
for range ticker.C {
// Example checks
dbOK := checkDatabase(db)
redisOK := checkRedis(rdb)
status := healthpb.HealthCheckResponse_SERVING
if !dbOK || !redisOK {
status = healthpb.HealthCheckResponse_NOT_SERVING
}
healthcheck.SetServingStatus(
"",
status,
)
}
}()
info := grpcServer.GetServiceInfo() info := grpcServer.GetServiceInfo()
tokenCtrl.SetGRPCInfo(info) tokenCtrl.SetGRPCInfo(info)
@@ -145,7 +177,7 @@ func selectorRequireAuth(ctx context.Context, callMeta interceptors.CallMeta) bo
return false return false
} }
if strings.HasPrefix(serviceName, "RefreshSession") { if serviceName == "Health" {
return false return false
} }
@@ -159,3 +191,33 @@ func selectorRequireAuth(ctx context.Context, callMeta interceptors.CallMeta) bo
return true return true
} }
func checkDatabase(db *sql.DB) bool {
ctx, cancel := context.WithTimeout(
context.Background(),
2*time.Second,
)
defer cancel()
// Fast connectivity check
if err := db.PingContext(ctx); err != nil {
return false
}
return true
}
func checkRedis(rdb *redis.Client) bool {
ctx, cancel := context.WithTimeout(
context.Background(),
2*time.Second,
)
defer cancel()
err := rdb.Ping(ctx).Err()
if err != nil {
return false
}
return true
}

6
go.mod
View File

@@ -10,7 +10,7 @@ require (
github.com/golang-jwt/jwt/v5 v5.3.1 github.com/golang-jwt/jwt/v5 v5.3.1
github.com/golang-migrate/migrate/v4 v4.19.1 github.com/golang-migrate/migrate/v4 v4.19.1
github.com/google/uuid v1.6.0 github.com/google/uuid v1.6.0
github.com/grpc-ecosystem/go-grpc-middleware v1.4.0 github.com/grpc-ecosystem/go-grpc-middleware v1.3.0
github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.3 github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.3
github.com/jackc/pgerrcode v0.0.0-20250907135507-afb5586c32a6 github.com/jackc/pgerrcode v0.0.0-20250907135507-afb5586c32a6
github.com/jackc/pgx/v5 v5.5.4 github.com/jackc/pgx/v5 v5.5.4
@@ -43,8 +43,8 @@ require (
) )
require ( require (
gitea.badhouseplants.net/softplayer/softplayer-go-proto v0.0.0-20260528090010-7bf4ddafe7f0 gitea.badhouseplants.net/softplayer/softplayer-go-proto v0.0.0-20260518175130-4b27db42e21e
github.com/golang/protobuf v1.5.4 // indirect github.com/golang/protobuf v1.5.4
golang.org/x/net v0.51.0 // indirect golang.org/x/net v0.51.0 // indirect
golang.org/x/sys v0.42.0 // indirect golang.org/x/sys v0.42.0 // indirect
golang.org/x/text v0.34.0 // indirect golang.org/x/text v0.34.0 // indirect

22
go.sum
View File

@@ -2,8 +2,8 @@ cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMT
cloud.google.com/go v0.121.6 h1:waZiuajrI28iAf40cWgycWNgaXPO06dupuS+sgibK6c= cloud.google.com/go v0.121.6 h1:waZiuajrI28iAf40cWgycWNgaXPO06dupuS+sgibK6c=
cloud.google.com/go/compute/metadata v0.9.0 h1:pDUj4QMoPejqq20dK0Pg2N4yG9zIkYGdBtwLoEkH9Zs= cloud.google.com/go/compute/metadata v0.9.0 h1:pDUj4QMoPejqq20dK0Pg2N4yG9zIkYGdBtwLoEkH9Zs=
cloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10= cloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10=
gitea.badhouseplants.net/softplayer/softplayer-go-proto v0.0.0-20260528090010-7bf4ddafe7f0 h1:CI6EwQndn8cr6ofpc1HbDsphCwK3NOZrdl2PS0BnX+Q= gitea.badhouseplants.net/softplayer/softplayer-go-proto v0.0.0-20260518175130-4b27db42e21e h1:9pt3cvnJ3slg0lDjCwgVbvS/kI1JlKuNUFxdlYCGWF0=
gitea.badhouseplants.net/softplayer/softplayer-go-proto v0.0.0-20260528090010-7bf4ddafe7f0/go.mod h1:EcQEZ3NN06b3UmKxiRnQnXDDjQ9kmJgoQQBAS+fpRQw= gitea.badhouseplants.net/softplayer/softplayer-go-proto v0.0.0-20260518175130-4b27db42e21e/go.mod h1:EcQEZ3NN06b3UmKxiRnQnXDDjQ9kmJgoQQBAS+fpRQw=
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg= github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg=
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
@@ -15,7 +15,6 @@ github.com/alecthomas/kong v1.15.0 h1:BVJstKbpO73zKpmIu+m/aLRrNmWwxXPIGTNin9VmLV
github.com/alecthomas/kong v1.15.0/go.mod h1:wrlbXem1CWqUV5Vbmss5ISYhsVPkBb1Yo7YKJghju2I= github.com/alecthomas/kong v1.15.0/go.mod h1:wrlbXem1CWqUV5Vbmss5ISYhsVPkBb1Yo7YKJghju2I=
github.com/alecthomas/repr v0.5.2 h1:SU73FTI9D1P5UNtvseffFSGmdNci/O6RsqzeXJtP0Qs= github.com/alecthomas/repr v0.5.2 h1:SU73FTI9D1P5UNtvseffFSGmdNci/O6RsqzeXJtP0Qs=
github.com/alecthomas/repr v0.5.2/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4= github.com/alecthomas/repr v0.5.2/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=
github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs= github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c= github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c=
github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA= github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
@@ -57,9 +56,7 @@ github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSw
github.com/geozelot/intree v1.0.0 h1:xUyiXMt0wD9zbPMOjy2rVShiUc3PGMPddPuTmi+Jy2s= github.com/geozelot/intree v1.0.0 h1:xUyiXMt0wD9zbPMOjy2rVShiUc3PGMPddPuTmi+Jy2s=
github.com/geozelot/intree v1.0.0/go.mod h1:JrqfsNwe17AgzOM023tCXPyUB89NhaZAb8o5rzfZQ7Q= github.com/geozelot/intree v1.0.0/go.mod h1:JrqfsNwe17AgzOM023tCXPyUB89NhaZAb8o5rzfZQ7Q=
github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY=
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A=
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
@@ -87,8 +84,6 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/grpc-ecosystem/go-grpc-middleware v1.3.0 h1:+9834+KizmvFV7pXQGSXQTsaWhq2GjuNUt0aUU0YBYw= github.com/grpc-ecosystem/go-grpc-middleware v1.3.0 h1:+9834+KizmvFV7pXQGSXQTsaWhq2GjuNUt0aUU0YBYw=
github.com/grpc-ecosystem/go-grpc-middleware v1.3.0/go.mod h1:z0ButlSOZa5vEBq9m2m2hlwIgKw+rp3sdCBRoJY+30Y= github.com/grpc-ecosystem/go-grpc-middleware v1.3.0/go.mod h1:z0ButlSOZa5vEBq9m2m2hlwIgKw+rp3sdCBRoJY+30Y=
github.com/grpc-ecosystem/go-grpc-middleware v1.4.0 h1:UH//fgunKIs4JdUbpDl1VZCDaL56wXCB/5+wF6uHfaI=
github.com/grpc-ecosystem/go-grpc-middleware v1.4.0/go.mod h1:g5qyo/la0ALbONm6Vbp88Yd8NsDy6rZz+RcrMPxvld8=
github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.3 h1:B+8ClL/kCQkRiU82d9xajRPKYMrB7E0MbtzWVi1K4ns= github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.3 h1:B+8ClL/kCQkRiU82d9xajRPKYMrB7E0MbtzWVi1K4ns=
github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.3/go.mod h1:NbCUVmiS4foBGBHOYlCT25+YmGpJ32dZPi75pGEUpj4= github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.3/go.mod h1:NbCUVmiS4foBGBHOYlCT25+YmGpJ32dZPi75pGEUpj4=
github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM= github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=
@@ -109,11 +104,8 @@ github.com/klauspost/cpuid/v2 v2.0.9 h1:lgaqFMSdTdQYdZ04uHyN2d/eKdOMyi2YLSvlQIBF
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 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 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
@@ -174,18 +166,14 @@ go.opentelemetry.io/otel/sdk/metric v1.43.0/go.mod h1:C/RJtwSEJ5hzTiUz5pXF1kILHS
go.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09nk+3A= go.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09nk+3A=
go.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0= go.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0=
go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
go.uber.org/goleak v1.1.10/go.mod h1:8a7PlsEVH3e/a/GLqe5IIrQx6GzcnRmZEufDUTk4A7A=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= 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/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU=
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= 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/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
go.uber.org/zap v1.18.1/go.mod h1:xg/QME4nWcxGxrpdeYfq7UvYrLh66cuVKdrbD1XF/NI=
go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
@@ -197,7 +185,6 @@ golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@@ -225,7 +212,6 @@ golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5h
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo= golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
@@ -238,7 +224,6 @@ golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGm
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20191108193012-7d206e10da11/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
@@ -265,13 +250,10 @@ google.golang.org/grpc v1.81.1/go.mod h1:xGH9GfzOyMTGIOXBJmXt+BX/V0kcdQbdcuwQ/zN
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/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 h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=

View File

@@ -72,9 +72,6 @@ spec:
- server - server
- --dev-mode - --dev-mode
- --reflection - --reflection
{{- with .Values.args }}
{{ . | toYaml | nindent 12 }}
{{- end }}
ports: ports:
- name: grpc - name: grpc
containerPort: {{ .Values.service.port }} containerPort: {{ .Values.service.port }}

View File

@@ -15,8 +15,6 @@ podAnnotations: {}
podLabels: {} podLabels: {}
podSecurityContext: {} podSecurityContext: {}
securityContext: {} securityContext: {}
args: []
# capabilities: # capabilities:
# drop: # drop:
# - ALL # - ALL

View File

@@ -0,0 +1,87 @@
package v1
import (
"context"
"errors"
"gitea.badhouseplants.net/softplayer/softplayer-backend/internal/services"
accounts "gitea.badhouseplants.net/softplayer/softplayer-go-proto/pkg/accounts/v1"
"github.com/golang/protobuf/ptypes/empty"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/metadata"
"google.golang.org/grpc/status"
"google.golang.org/protobuf/types/known/emptypb"
)
func NewAccountServer(
accountsCtrl *services.AccountController,
authorizationCtrl *services.AuthController,
) *AccountsServer {
return &AccountsServer{
accountsCtrl: accountsCtrl,
authorizationCtrl: authorizationCtrl,
}
}
type AccountsServer struct {
accounts.UnimplementedAccountsServiceServer
accountsCtrl *services.AccountController
authorizationCtrl *services.AuthController
}
func (srv *AccountsServer) RefreshToken(ctx context.Context, in *empty.Empty) (*empty.Empty, error) {
claims, err := services.ClaimsFromContext(ctx)
if err != nil {
return nil, status.Error(codes.Aborted, "Context is invalid")
}
if claims.TokenType != services.TokenTypeRefresh {
return nil, status.Error(codes.Unauthenticated, "Invalid token")
}
session, err := srv.authorizationCtrl.GetSession(ctx, claims.TokenID)
if err != nil {
if errors.Is(err, services.ErrSessionNotFound) {
return nil, status.Error(codes.Unauthenticated, "Session doesn't exists")
}
return nil, status.Error(codes.Internal, "Somethings is broken on our side")
}
if session.UserID != claims.UserID {
return nil, status.Error(codes.Unauthenticated, "Invalid session")
}
accessToken, _, err := srv.authorizationCtrl.GenerateToken(&services.JWTData{
UserID: claims.UserID,
TokenType: services.TokenTypeAccess,
TokenAud: services.TokenAudWeb,
})
if err != nil {
return nil, status.Error(codes.Aborted, "Couldn't generate an access token")
}
refreshToken, tokenID, err := srv.authorizationCtrl.GenerateToken(&services.JWTData{
UserID: claims.UserID,
TokenType: services.TokenTypeRefresh,
TokenAud: services.TokenAudWeb,
})
if err != nil {
return nil, status.Error(codes.Aborted, "Couldn't generate an access token")
}
newSession := &services.Session{UserID: session.UserID}
if err := srv.authorizationCtrl.SaveSession(ctx, tokenID, newSession); err != nil {
return nil, status.Error(codes.Aborted, "Couldn't store session")
}
header := metadata.New(map[string]string{
"X-Access-Token": accessToken,
"X-Refresh-Token": refreshToken,
})
if err := grpc.SetHeader(ctx, header); err != nil {
return nil, status.Error(codes.Aborted, "Couldn't set metadata")
}
return &emptypb.Empty{}, nil
}

View File

@@ -0,0 +1,38 @@
package v1
import (
"context"
projects "gitea.badhouseplants.net/softplayer/softplayer-go-proto/pkg/projects/v1"
"google.golang.org/grpc"
)
func NewProjectsServer() *ProjectsServer {
return &ProjectsServer{}
}
// var _ projects.ProjectsServiceServer = (*ProjectsServer)(nil)
type ProjectsServer struct {
projects.UnimplementedProjectsServiceServer
}
// CreateProject implements [v1.ProjectsServiceServer].
func (p *ProjectsServer) CreateProject(context.Context, *projects.CreateProjectRequest) (*projects.CreateProjectResponse, error) {
panic("unimplemented")
}
// GetProject implements [v1.ProjectsServiceServer].
func (p *ProjectsServer) GetProject(context.Context, *projects.GetProjectRequest) (*projects.GetProjectResponse, error) {
panic("unimplemented")
}
// ListProjects implements [v1.ProjectsServiceServer].
func (p *ProjectsServer) ListProjects(*projects.ListProjectsRequest, grpc.ServerStreamingServer[projects.ListProjectsResponse]) error {
panic("unimplemented")
}
// UpdateProject implements [v1.ProjectsServiceServer].
func (p *ProjectsServer) UpdateProject(context.Context, *projects.UpdateProjectRequest) (*projects.UpdateProjectResponse, error) {
panic("unimplemented")
}

View File

@@ -3,15 +3,19 @@ package v1
import ( import (
"context" "context"
"gitea.badhouseplants.net/softplayer/softplayer-backend/internal/controllers" "gitea.badhouseplants.net/softplayer/softplayer-backend/internal/services"
accounts "gitea.badhouseplants.net/softplayer/softplayer-go-proto/pkg/accounts/v1" accounts "gitea.badhouseplants.net/softplayer/softplayer-go-proto/pkg/accounts/v1"
"github.com/golang/protobuf/ptypes/empty"
"google.golang.org/grpc"
"google.golang.org/grpc/codes" "google.golang.org/grpc/codes"
"google.golang.org/grpc/metadata"
"google.golang.org/grpc/status" "google.golang.org/grpc/status"
"google.golang.org/protobuf/types/known/emptypb"
) )
func NewPublicAccountServer( func NewPublicAccountServer(
accountsCtrl *controllers.AccountController, accountsCtrl *services.AccountController,
authorizationCtrl *controllers.AuthController, authorizationCtrl *services.AuthController,
) *PublicAccountService { ) *PublicAccountService {
return &PublicAccountService{ return &PublicAccountService{
accountsCtrl: accountsCtrl, accountsCtrl: accountsCtrl,
@@ -21,85 +25,91 @@ func NewPublicAccountServer(
type PublicAccountService struct { type PublicAccountService struct {
accounts.UnimplementedPublicAccountsServiceServer accounts.UnimplementedPublicAccountsServiceServer
accountsCtrl *controllers.AccountController accountsCtrl *services.AccountController
authorizationCtrl *controllers.AuthController authorizationCtrl *services.AuthController
} }
func (a *PublicAccountService) SignIn(ctx context.Context, in *accounts.SignInRequest) (*accounts.SignInResponse, error) { func (a *PublicAccountService) SignIn(ctx context.Context, in *accounts.SignInRequest) (*empty.Empty, error) {
id, err := a.accountsCtrl.Login(ctx, in.GetEmail(), in.GetPassword()) id, err := a.accountsCtrl.Login(ctx, in.GetEmail(), in.GetPassword())
if err != nil { if err != nil {
return nil, status.Error(codes.Aborted, "Couldn't create a user") return nil, status.Error(codes.Aborted, "Couldn't create a user")
} }
accessToken, _, err := a.authorizationCtrl.GenerateToken(&controllers.JWTData{ accessToken, _, err := a.authorizationCtrl.GenerateToken(&services.JWTData{
UserID: id, UserID: id,
TokenType: controllers.TokenTypeAccess, TokenType: services.TokenTypeAccess,
TokenAud: controllers.TokenAudWeb, TokenAud: services.TokenAudWeb,
}) })
if err != nil { if err != nil {
return nil, status.Error(codes.Aborted, "Couldn't generate an access token") return nil, status.Error(codes.Aborted, "Couldn't generate an access token")
} }
refreshToken, tokenID, err := a.authorizationCtrl.GenerateToken(&controllers.JWTData{ refreshToken, tokenID, err := a.authorizationCtrl.GenerateToken(&services.JWTData{
UserID: id, UserID: id,
TokenType: controllers.TokenTypeRefresh, TokenType: services.TokenTypeRefresh,
TokenAud: controllers.TokenAudWeb, TokenAud: services.TokenAudWeb,
}) })
if err != nil { if err != nil {
return nil, status.Error(codes.Aborted, "Couldn't generate an access token") return nil, status.Error(codes.Aborted, "Couldn't generate an access token")
} }
session := &controllers.Session{UserID: id} session := &services.Session{UserID: id}
if err := a.authorizationCtrl.SaveSession(ctx, tokenID, session); err != nil { if err := a.authorizationCtrl.SaveSession(ctx, tokenID, session); err != nil {
return nil, status.Error(codes.Aborted, "Couldn't store session") return nil, status.Error(codes.Aborted, "Couldn't store session")
} }
return &accounts.SignInResponse{ header := metadata.New(map[string]string{
TokenPair: &accounts.TokenPair{ "X-Access-Token": accessToken,
AccessToken: accessToken, "X-Refresh-Token": refreshToken,
RefreshToken: refreshToken, })
}, if err := grpc.SetHeader(ctx, header); err != nil {
}, nil return nil, status.Error(codes.Aborted, "Couldn't set metadata")
}
return &emptypb.Empty{}, nil
} }
// Create a new account in Softplayer // Create a new account in Softplayer
func (a *PublicAccountService) SignUp(ctx context.Context, in *accounts.SignUpRequest) (*accounts.SignUpResponse, error) { func (a *PublicAccountService) SignUp(ctx context.Context, in *accounts.SignUpRequest) (*empty.Empty, error) {
data := &controllers.AccountData{ data := &services.AccountData{
Password: in.GetPassword(), Password: in.GetPassword(),
Email: in.GetEmail(), Email: in.GetEmail(),
Name: in.PersonalData.GetName(),
Surname: in.PersonalData.GetSurname(),
} }
id, err := a.accountsCtrl.Create(ctx, data) id, err := a.accountsCtrl.Create(ctx, data)
if err != nil { if err != nil {
return nil, status.Error(codes.Aborted, "Couldn't create a user") return nil, status.Error(codes.Aborted, "Couldn't create a user")
} }
accessToken, _, err := a.authorizationCtrl.GenerateToken(&controllers.JWTData{ accessToken, _, err := a.authorizationCtrl.GenerateToken(&services.JWTData{
UserID: id, UserID: id,
TokenType: controllers.TokenTypeAccess, TokenType: services.TokenTypeAccess,
TokenAud: controllers.TokenAudWeb, TokenAud: services.TokenAudWeb,
}) })
if err != nil { if err != nil {
return nil, status.Error(codes.Aborted, "Couldn't generate an access token") return nil, status.Error(codes.Aborted, "Couldn't generate an access token")
} }
refreshToken, tokenID, err := a.authorizationCtrl.GenerateToken(&controllers.JWTData{ refreshToken, tokenID, err := a.authorizationCtrl.GenerateToken(&services.JWTData{
UserID: id, UserID: id,
TokenType: controllers.TokenTypeRefresh, TokenType: services.TokenTypeRefresh,
TokenAud: controllers.TokenAudWeb, TokenAud: services.TokenAudWeb,
}) })
if err != nil { if err != nil {
return nil, status.Error(codes.Aborted, "Couldn't generate an access token") return nil, status.Error(codes.Aborted, "Couldn't generate an access token")
} }
session := &controllers.Session{UserID: id} session := &services.Session{UserID: id}
if err := a.authorizationCtrl.SaveSession(ctx, tokenID, session); err != nil { if err := a.authorizationCtrl.SaveSession(ctx, tokenID, session); err != nil {
return nil, status.Error(codes.Aborted, "Couldn't store session") return nil, status.Error(codes.Aborted, "Couldn't store session")
} }
return &accounts.SignUpResponse{ header := metadata.New(map[string]string{
TokenPair: &accounts.TokenPair{ "X-Access-Token": accessToken,
AccessToken: accessToken, "X-Refresh-Token": refreshToken,
RefreshToken: refreshToken, })
}, if err := grpc.SetHeader(ctx, header); err != nil {
}, nil return nil, status.Error(codes.Aborted, "Couldn't set metadata")
}
return &emptypb.Empty{}, nil
} }

View File

@@ -4,7 +4,7 @@ import (
"context" "context"
"errors" "errors"
"gitea.badhouseplants.net/softplayer/softplayer-backend/internal/controllers" "gitea.badhouseplants.net/softplayer/softplayer-backend/internal/services"
tokens "gitea.badhouseplants.net/softplayer/softplayer-go-proto/pkg/tokens/v1" tokens "gitea.badhouseplants.net/softplayer/softplayer-go-proto/pkg/tokens/v1"
"google.golang.org/grpc" "google.golang.org/grpc"
"google.golang.org/grpc/codes" "google.golang.org/grpc/codes"
@@ -17,13 +17,13 @@ import (
type PublicTokensServer struct { type PublicTokensServer struct {
tokens.UnimplementedPublicTokensServiceServer tokens.UnimplementedPublicTokensServiceServer
tokenCtrl *controllers.TokenController tokenCtrl *services.TokenController
authorizationCtrl *controllers.AuthController authorizationCtrl *services.AuthController
} }
func NewPublicTokensServer( func NewPublicTokensServer(
tokenCtrl *controllers.TokenController, tokenCtrl *services.TokenController,
authorizationCtrl *controllers.AuthController, authorizationCtrl *services.AuthController,
) *PublicTokensServer { ) *PublicTokensServer {
return &PublicTokensServer{ return &PublicTokensServer{
tokenCtrl: tokenCtrl, tokenCtrl: tokenCtrl,
@@ -34,19 +34,19 @@ func NewPublicTokensServer(
func (srv *PublicTokensServer) AuthenticateWithToken(ctx context.Context, in *tokens.AuthenticateWithTokenRequest) (*emptypb.Empty, error) { func (srv *PublicTokensServer) AuthenticateWithToken(ctx context.Context, in *tokens.AuthenticateWithTokenRequest) (*emptypb.Empty, error) {
tokenAuthRes, err := srv.tokenCtrl.AuthenticateWithToken(ctx, in.TokenValue.Token) tokenAuthRes, err := srv.tokenCtrl.AuthenticateWithToken(ctx, in.TokenValue.Token)
if err != nil { if err != nil {
if errors.Is(err, controllers.ErrBadToken) { if errors.Is(err, services.ErrBadToken) {
return nil, status.Error(codes.Unauthenticated, "Token is not valid") return nil, status.Error(codes.Unauthenticated, "Token is not valid")
} }
if errors.Is(err, controllers.ErrServerError) { if errors.Is(err, services.ErrServerError) {
return nil, status.Error(codes.Internal, "Something is broken on our side") return nil, status.Error(codes.Internal, "Something is broken on our side")
} }
return nil, status.Error(codes.Aborted, "Couldn't authorize") return nil, status.Error(codes.Aborted, "Couldn't authorize")
} }
jwtData := &controllers.JWTData{ jwtData := &services.JWTData{
UserID: tokenAuthRes.UserID, UserID: tokenAuthRes.UserID,
TokenType: controllers.TokenTypeAccess, TokenType: services.TokenTypeAccess,
TokenAud: controllers.TokenAudToken, TokenAud: services.TokenAudToken,
Scope: tokenAuthRes.Scope, Scope: tokenAuthRes.Scope,
} }
accessToken, _, err := srv.authorizationCtrl.GenerateToken(jwtData) accessToken, _, err := srv.authorizationCtrl.GenerateToken(jwtData)

View File

@@ -4,7 +4,7 @@ import (
"context" "context"
"errors" "errors"
"gitea.badhouseplants.net/softplayer/softplayer-backend/internal/controllers" "gitea.badhouseplants.net/softplayer/softplayer-backend/internal/services"
tokens "gitea.badhouseplants.net/softplayer/softplayer-go-proto/pkg/tokens/v1" tokens "gitea.badhouseplants.net/softplayer/softplayer-go-proto/pkg/tokens/v1"
"google.golang.org/grpc" "google.golang.org/grpc"
"google.golang.org/grpc/codes" "google.golang.org/grpc/codes"
@@ -18,13 +18,13 @@ import (
// TokensServer implements the Token Service // TokensServer implements the Token Service
type TokensServer struct { type TokensServer struct {
tokens.UnimplementedTokensServiceServer tokens.UnimplementedTokensServiceServer
tokenCtrl *controllers.TokenController tokenCtrl *services.TokenController
authorizationCtrl *controllers.AuthController authorizationCtrl *services.AuthController
} }
func NewTokensServer( func NewTokensServer(
tokenCtrl *controllers.TokenController, tokenCtrl *services.TokenController,
authorizationCtrl *controllers.AuthController, authorizationCtrl *services.AuthController,
) *TokensServer { ) *TokensServer {
return &TokensServer{ return &TokensServer{
tokenCtrl: tokenCtrl, tokenCtrl: tokenCtrl,
@@ -34,7 +34,7 @@ func NewTokensServer(
// CreateToken implements [v1.TokensServiceServer]. // CreateToken implements [v1.TokensServiceServer].
func (srv *TokensServer) CreateToken(ctx context.Context, in *tokens.CreateTokenRequest) (*tokens.CreateTokenResponse, error) { func (srv *TokensServer) CreateToken(ctx context.Context, in *tokens.CreateTokenRequest) (*tokens.CreateTokenResponse, error) {
claims, err := controllers.ClaimsFromContext(ctx) claims, err := services.ClaimsFromContext(ctx)
if err != nil { if err != nil {
return nil, status.Error(codes.Aborted, "Context is invalid") return nil, status.Error(codes.Aborted, "Context is invalid")
} }
@@ -50,7 +50,7 @@ func (srv *TokensServer) CreateToken(ctx context.Context, in *tokens.CreateToken
permissions[service] = methods.GetMethods() permissions[service] = methods.GetMethods()
} }
tokenData := &controllers.TokenData{ tokenData := &services.TokenData{
Name: in.TokenMetadata.GetName(), Name: in.TokenMetadata.GetName(),
UserID: claims.UserID, UserID: claims.UserID,
ExpiresAt: in.TokenMetadata.ExpiresAt.AsTime(), ExpiresAt: in.TokenMetadata.ExpiresAt.AsTime(),
@@ -59,7 +59,7 @@ func (srv *TokensServer) CreateToken(ctx context.Context, in *tokens.CreateToken
token, tokenID, err := srv.tokenCtrl.Create(ctx, tokenData) token, tokenID, err := srv.tokenCtrl.Create(ctx, tokenData)
if err != nil { if err != nil {
if errors.Is(err, controllers.ErrServerError) { if errors.Is(err, services.ErrServerError) {
return nil, status.Error(codes.Internal, "Something is broken on our side") return nil, status.Error(codes.Internal, "Something is broken on our side")
} }
return nil, status.Error(codes.Aborted, "Couldn't create a token") return nil, status.Error(codes.Aborted, "Couldn't create a token")
@@ -75,7 +75,7 @@ func (srv *TokensServer) CreateToken(ctx context.Context, in *tokens.CreateToken
// ForceTokenExpiration implements [v1.TokensServiceServer]. // ForceTokenExpiration implements [v1.TokensServiceServer].
func (srv *TokensServer) ForceTokenExpiration(ctx context.Context, in *tokens.ForceTokenExpirationRequest) (*emptypb.Empty, error) { func (srv *TokensServer) ForceTokenExpiration(ctx context.Context, in *tokens.ForceTokenExpirationRequest) (*emptypb.Empty, error) {
claims, err := controllers.ClaimsFromContext(ctx) claims, err := services.ClaimsFromContext(ctx)
if err != nil { if err != nil {
return nil, status.Error(codes.Aborted, "Context is invalid") return nil, status.Error(codes.Aborted, "Context is invalid")
} }
@@ -84,14 +84,14 @@ func (srv *TokensServer) ForceTokenExpiration(ctx context.Context, in *tokens.Fo
} }
if err := srv.tokenCtrl.VerifyTokenOwner(ctx, claims.UserID, in.TokenUuid.Uuid); err != nil { if err := srv.tokenCtrl.VerifyTokenOwner(ctx, claims.UserID, in.TokenUuid.Uuid); err != nil {
if errors.Is(err, controllers.ErrServerError) { if errors.Is(err, services.ErrServerError) {
return nil, status.Error(codes.Internal, "Something is broken on our side") return nil, status.Error(codes.Internal, "Something is broken on our side")
} }
return nil, status.Error(codes.Aborted, "User is now allowed to manipulate this token") return nil, status.Error(codes.Aborted, "User is now allowed to manipulate this token")
} }
if err := srv.tokenCtrl.ForceExpiration(ctx, in.TokenUuid.GetUuid()); err != nil { if err := srv.tokenCtrl.ForceExpiration(ctx, in.TokenUuid.GetUuid()); err != nil {
if errors.Is(err, controllers.ErrServerError) { if errors.Is(err, services.ErrServerError) {
return nil, status.Error(codes.Internal, "Something is broken on our side") return nil, status.Error(codes.Internal, "Something is broken on our side")
} }
return nil, status.Error(codes.Aborted, "Couldn't create a token") return nil, status.Error(codes.Aborted, "Couldn't create a token")
@@ -101,7 +101,7 @@ func (srv *TokensServer) ForceTokenExpiration(ctx context.Context, in *tokens.Fo
// GetToken implements [v1.TokensServiceServer]. // GetToken implements [v1.TokensServiceServer].
func (srv *TokensServer) GetToken(ctx context.Context, in *tokens.GetTokenRequest) (*tokens.GetTokenResponse, error) { func (srv *TokensServer) GetToken(ctx context.Context, in *tokens.GetTokenRequest) (*tokens.GetTokenResponse, error) {
claims, err := controllers.ClaimsFromContext(ctx) claims, err := services.ClaimsFromContext(ctx)
if err != nil { if err != nil {
return nil, status.Error(codes.Aborted, "Context is invalid") return nil, status.Error(codes.Aborted, "Context is invalid")
} }
@@ -109,7 +109,7 @@ func (srv *TokensServer) GetToken(ctx context.Context, in *tokens.GetTokenReques
return nil, status.Error(codes.Aborted, "Context is invalid") return nil, status.Error(codes.Aborted, "Context is invalid")
} }
if err := srv.tokenCtrl.VerifyTokenOwner(ctx, claims.UserID, in.TokenUuid.Uuid); err != nil { if err := srv.tokenCtrl.VerifyTokenOwner(ctx, claims.UserID, in.TokenUuid.Uuid); err != nil {
if errors.Is(err, controllers.ErrServerError) { if errors.Is(err, services.ErrServerError) {
return nil, status.Error(codes.Internal, "Something is broken on our side") return nil, status.Error(codes.Internal, "Something is broken on our side")
} }
return nil, status.Error(codes.Aborted, "User is now allowed to manipulate this token") return nil, status.Error(codes.Aborted, "User is now allowed to manipulate this token")
@@ -117,7 +117,7 @@ func (srv *TokensServer) GetToken(ctx context.Context, in *tokens.GetTokenReques
token, err := srv.tokenCtrl.Get(ctx, in.TokenUuid.Uuid, claims.UserID) token, err := srv.tokenCtrl.Get(ctx, in.TokenUuid.Uuid, claims.UserID)
if err != nil { if err != nil {
if errors.Is(err, controllers.ErrServerError) { if errors.Is(err, services.ErrServerError) {
return nil, status.Error(codes.Internal, "Something is broken on our side") return nil, status.Error(codes.Internal, "Something is broken on our side")
} }
return nil, status.Error(codes.Aborted, "Couldn't list tokens") return nil, status.Error(codes.Aborted, "Couldn't list tokens")
@@ -141,7 +141,7 @@ func (srv *TokensServer) GetToken(ctx context.Context, in *tokens.GetTokenReques
// ListTokens implements [v1.TokensServiceServer]. // ListTokens implements [v1.TokensServiceServer].
func (srv *TokensServer) ListTokens(in *emptypb.Empty, stream grpc.ServerStreamingServer[tokens.ListTokensResponse]) error { func (srv *TokensServer) ListTokens(in *emptypb.Empty, stream grpc.ServerStreamingServer[tokens.ListTokensResponse]) error {
claims, err := controllers.ClaimsFromContext(stream.Context()) claims, err := services.ClaimsFromContext(stream.Context())
if err != nil { if err != nil {
return status.Error(codes.Aborted, "Context is invalid") return status.Error(codes.Aborted, "Context is invalid")
} }
@@ -151,7 +151,7 @@ func (srv *TokensServer) ListTokens(in *emptypb.Empty, stream grpc.ServerStreami
tokensRes, err := srv.tokenCtrl.List(stream.Context(), claims.UserID) tokensRes, err := srv.tokenCtrl.List(stream.Context(), claims.UserID)
if err != nil { if err != nil {
if errors.Is(err, controllers.ErrServerError) { if errors.Is(err, services.ErrServerError) {
return status.Error(codes.Internal, "Something is broken on our side") return status.Error(codes.Internal, "Something is broken on our side")
} }
return status.Error(codes.Aborted, "Couldn't list tokens") return status.Error(codes.Aborted, "Couldn't list tokens")
@@ -179,7 +179,7 @@ func (srv *TokensServer) ListTokens(in *emptypb.Empty, stream grpc.ServerStreami
// RegenerateToken implements [v1.TokensServiceServer]. // RegenerateToken implements [v1.TokensServiceServer].
func (srv *TokensServer) RegenerateToken(ctx context.Context, in *tokens.RegenerateTokenRequest) (*tokens.RegenerateTokenResponse, error) { func (srv *TokensServer) RegenerateToken(ctx context.Context, in *tokens.RegenerateTokenRequest) (*tokens.RegenerateTokenResponse, error) {
claims, err := controllers.ClaimsFromContext(ctx) claims, err := services.ClaimsFromContext(ctx)
if err != nil { if err != nil {
return nil, status.Error(codes.Aborted, "Context is invalid") return nil, status.Error(codes.Aborted, "Context is invalid")
} }
@@ -187,7 +187,7 @@ func (srv *TokensServer) RegenerateToken(ctx context.Context, in *tokens.Regener
return nil, status.Error(codes.Aborted, "Context is invalid") return nil, status.Error(codes.Aborted, "Context is invalid")
} }
if err := srv.tokenCtrl.VerifyTokenOwner(ctx, claims.UserID, in.TokenUuid.Uuid); err != nil { if err := srv.tokenCtrl.VerifyTokenOwner(ctx, claims.UserID, in.TokenUuid.Uuid); err != nil {
if errors.Is(err, controllers.ErrServerError) { if errors.Is(err, services.ErrServerError) {
return nil, status.Error(codes.Internal, "Something is broken on our side") return nil, status.Error(codes.Internal, "Something is broken on our side")
} }
return nil, status.Error(codes.Aborted, "User is now allowed to manipulate this token") return nil, status.Error(codes.Aborted, "User is now allowed to manipulate this token")
@@ -195,7 +195,7 @@ func (srv *TokensServer) RegenerateToken(ctx context.Context, in *tokens.Regener
tokenVal, err := srv.tokenCtrl.Regenerate(ctx, in.TokenUuid.GetUuid()) tokenVal, err := srv.tokenCtrl.Regenerate(ctx, in.TokenUuid.GetUuid())
if err != nil { if err != nil {
if errors.Is(err, controllers.ErrServerError) { if errors.Is(err, services.ErrServerError) {
return nil, status.Error(codes.Internal, "Something is broken on our side") return nil, status.Error(codes.Internal, "Something is broken on our side")
} }
return nil, status.Error(codes.Aborted, "Couldn't list tokens") return nil, status.Error(codes.Aborted, "Couldn't list tokens")
@@ -209,7 +209,7 @@ func (srv *TokensServer) RegenerateToken(ctx context.Context, in *tokens.Regener
// UpdateToken implements [v1.TokensServiceServer]. // UpdateToken implements [v1.TokensServiceServer].
func (srv *TokensServer) UpdateToken(ctx context.Context, in *tokens.UpdateTokenRequest) (*tokens.UpdateTokenResponse, error) { func (srv *TokensServer) UpdateToken(ctx context.Context, in *tokens.UpdateTokenRequest) (*tokens.UpdateTokenResponse, error) {
claims, err := controllers.ClaimsFromContext(ctx) claims, err := services.ClaimsFromContext(ctx)
if err != nil { if err != nil {
return nil, status.Error(codes.Aborted, "Context is invalid") return nil, status.Error(codes.Aborted, "Context is invalid")
} }
@@ -218,7 +218,7 @@ func (srv *TokensServer) UpdateToken(ctx context.Context, in *tokens.UpdateToken
} }
if err := srv.tokenCtrl.VerifyTokenOwner(ctx, claims.UserID, in.TokenUuid.Uuid); err != nil { if err := srv.tokenCtrl.VerifyTokenOwner(ctx, claims.UserID, in.TokenUuid.Uuid); err != nil {
if errors.Is(err, controllers.ErrServerError) { if errors.Is(err, services.ErrServerError) {
return nil, status.Error(codes.Internal, "Something is broken on our side") return nil, status.Error(codes.Internal, "Something is broken on our side")
} }
return nil, status.Error(codes.Aborted, "User is now allowed to manipulate this token") return nil, status.Error(codes.Aborted, "User is now allowed to manipulate this token")
@@ -231,12 +231,12 @@ func (srv *TokensServer) UpdateToken(ctx context.Context, in *tokens.UpdateToken
for service, methods := range in.TokenPermissions.Permissions { for service, methods := range in.TokenPermissions.Permissions {
permissions[service] = methods.GetMethods() permissions[service] = methods.GetMethods()
} }
tokenData := &controllers.TokenData{ tokenData := &services.TokenData{
Name: in.TokenMetadata.Name, Name: in.TokenMetadata.Name,
Scopes: permissions, Scopes: permissions,
} }
if err := srv.tokenCtrl.Update(ctx, tokenData); err != nil { if err := srv.tokenCtrl.Update(ctx, tokenData); err != nil {
if errors.Is(err, controllers.ErrServerError) { if errors.Is(err, services.ErrServerError) {
return nil, status.Error(codes.Internal, "Something is broken on our side") return nil, status.Error(codes.Internal, "Something is broken on our side")
} }
return nil, status.Error(codes.Aborted, "Couldn't list tokens") return nil, status.Error(codes.Aborted, "Couldn't list tokens")

View File

@@ -10,21 +10,24 @@ import (
) )
var ( var (
ErrAlreadyExists = errors.New("entry already exists") ErrAlreadyExists = errors.New("entry already exists")
ErrNotFound = errors.New("entry not found") ErrCheckNotPassed = errors.New("sql checks not passed")
ErrNotFound = errors.New("entry not found")
) )
type AccountData struct { type AccountData struct {
UUID string UUID string
Email string Email string
PasswordHash string PasswordHash string
Name string
Surname string
} }
// CreateAccount adds a new account to a database // CreateAccount adds a new account to a database
func CreateAccount(ctx context.Context, db *sql.DB, account *AccountData) error { func CreateAccount(ctx context.Context, db *sql.DB, account *AccountData) error {
query := "INSERT INTO accounts (uuid, email, password_hash) VALUES ($1, $2, $3)" query := "INSERT INTO accounts (uuid, email, password_hash, name, surname) VALUES ($1, $2, $3, $4, $5)"
if _, err := db.ExecContext(ctx, query, account.UUID, account.Email, account.PasswordHash); err != nil { if _, err := db.ExecContext(ctx, query, account.UUID, account.Email, account.PasswordHash, account.Name, account.Surname); err != nil {
var pgErr *pgconn.PgError var pgErr *pgconn.PgError
if errors.As(err, &pgErr) { if errors.As(err, &pgErr) {
if pgErr.Code == pgerrcode.UniqueViolation { if pgErr.Code == pgerrcode.UniqueViolation {

View File

@@ -0,0 +1,169 @@
package repository
import (
"context"
"database/sql"
"errors"
"time"
"github.com/jackc/pgerrcode"
"github.com/jackc/pgx/v5/pgconn"
)
type ProjectData struct {
UUID string
Name string
Slug string
Description string
CreatedBy string
CreatedAt time.Time
ClosedAt sql.NullTime
Blocked bool
UpdatedAt time.Time
UpdatedBy string
}
// CreateProject adds a new projects to the database
func CreateProject(ctx context.Context, db *sql.DB, data *ProjectData) error {
tx, err := StartTransaction(ctx, db)
if err != nil {
return err
}
defer tx.Rollback()
queryProject := `
INSERT INTO projects
(uuid, name, slug, description, owner_user_id, billing_account_id, created_by, created_at, updated_by, updated_at)
VALUES
($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)
`
if _, err := tx.ExecContext(ctx, queryProject,
data.UUID, data.Name, data.Slug, data.Description, data.CreatedBy,
data.CreatedBy, data.CreatedBy, data.CreatedAt, data.CreatedBy, data.CreatedAt); err != nil {
var pgErr *pgconn.PgError
if errors.As(err, &pgErr) {
switch pgErr.Code {
case pgerrcode.UniqueViolation:
return ErrAlreadyExists
case pgerrcode.CheckViolation:
return ErrCheckNotPassed
}
return err
}
return err
}
// When a project is created, we need to insert the default owner project membership
queryMembership := `
INSERT INTO project_membership
(project_uuid, user_uuid, role, status, invited_by, joined_at)
VALUES
($1, $2, $3, $4, $5, $6);
`
if _, err := tx.ExecContext(ctx, queryMembership,
data.UUID, data.CreatedBy, "owner", "active", data.CreatedBy, data.CreatedAt,
); err != nil {
var pgErr *pgconn.PgError
if errors.As(err, &pgErr) {
switch pgErr.Code {
case pgerrcode.UniqueViolation:
return ErrAlreadyExists
case pgerrcode.CheckViolation:
return ErrCheckNotPassed
}
return err
}
return err
}
if err := tx.Commit(); err != nil {
return err
}
return nil
}
// GetProjectByID returns a project from the database
func GetProjectByID(ctx context.Context, db *sql.DB, projectID string) (*ProjectData, error) {
data := &ProjectData{}
query := `
SELECT uuid, name, slug, description, owner_user_id, closed_at, created_at
FROM projects
WHERE uuid=$1`
if err := db.QueryRowContext(ctx, query, projectID).Scan(
&data.UUID,
&data.Name,
&data.Slug,
&data.Description,
&data.CreatedBy,
&data.ClosedAt,
&data.CreatedAt,
); err != nil {
if errors.Is(err, sql.ErrNoRows) {
return nil, ErrNotFound
}
return nil, err
}
return data, nil
}
// UpdateProject change editable project data
func UpdateProject(ctx context.Context, db *sql.DB, data *ProjectData) error {
query := `
UPDATE projects
SET
name = $2,
description = $3,
updated_at = $4,
updated_by = $5
WHERE uuid = $1;`
if _, err := db.Query(query, data.UUID, data.Name, data.Description, data.UpdatedAt, data.UpdatedBy); err != nil {
return err
}
return nil
}
// ListProjects get all projects that are available for the user from the database
func ListProjects(ctx context.Context, db *sql.DB, userID string) ([]*ProjectData, error) {
query := `
SELECT p.uuid, p.name
FROM projects p
JOIN project_membership pm ON pm.project_uuid = p.uuid
WHERE pm.user_uuid = $1`
rows, err := db.QueryContext(ctx, query, userID)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
return nil, ErrNotFound
}
return nil, err
}
result := []*ProjectData{}
for rows.Next() {
pd := &ProjectData{}
err := rows.Scan(&pd.UUID, &pd.Name)
if err != nil {
return nil, err
}
result = append(result, pd)
}
return result, nil
}
// GetProjectOwner should return an owner if a project
func GetProjectOwner(ctx context.Context, db *sql.DB, projectID string) (userID string, err error) {
query := `
SELECT user_uuid
FROM project_membership
WHERE project_uuid = $1 AND role = 'owner'`
if err := db.QueryRowContext(ctx, query, projectID).Scan(
&userID,
); err != nil {
if errors.Is(err, sql.ErrNoRows) {
return "", ErrNotFound
}
return "", err
}
return
}

View File

@@ -0,0 +1,246 @@
package repository_test
import (
"context"
"database/sql"
"fmt"
"testing"
"time"
"gitea.badhouseplants.net/softplayer/softplayer-backend/internal/repository"
"github.com/google/uuid"
"github.com/stretchr/testify/assert"
)
func newTestUniqueEmail(prefix string) string {
if prefix == "" {
prefix = "test"
}
return fmt.Sprintf(
"%s-%d-%s@example.com",
prefix,
time.Now().UnixMilli(),
uuid.NewString(),
)
}
func accountForProject(ctx context.Context) (string, error) {
account := &repository.AccountData{
UUID: uuid.NewString(),
Email: newTestUniqueEmail("projects"),
PasswordHash: "dummy",
Name: "John",
Surname: "Doe",
}
if err := repository.CreateAccount(ctx, newTestDBConnection(ctx), account); err != nil {
return "", err
}
return account.UUID, nil
}
func TestIntegrationProjectsCreate_Success(t *testing.T) {
createdBy, err := accountForProject(t.Context())
assert.NoError(t, err)
project := &repository.ProjectData{
UUID: uuid.NewString(),
Name: "test-1",
Slug: uuid.NewString(),
Description: "Test Project Number 1",
CreatedBy: createdBy,
CreatedAt: time.Now(),
ClosedAt: sql.NullTime{Time: time.Now()},
Blocked: false,
UpdatedAt: time.Now(),
UpdatedBy: createdBy,
}
assert.NoError(t, repository.CreateProject(t.Context(), newTestDBConnection(t.Context()), project))
}
func TestIntegrationProjectsCreate_CheckFailed(t *testing.T) {
createdBy, err := accountForProject(t.Context())
assert.NoError(t, err)
project := &repository.ProjectData{
UUID: uuid.NewString(),
Name: "test-2",
Slug: "test_2",
Description: "Test Project Number 1",
CreatedBy: createdBy,
CreatedAt: time.Now(),
ClosedAt: sql.NullTime{Time: time.Now()},
Blocked: false,
UpdatedAt: time.Now(),
UpdatedBy: createdBy,
}
assert.Error(t, repository.CreateProject(t.Context(), newTestDBConnection(t.Context()), project), repository.ErrCheckNotPassed)
}
func TestIntegrationProjectsCreate_AlreadyExistsFail(t *testing.T) {
createdBy, err := accountForProject(t.Context())
assert.NoError(t, err)
project := &repository.ProjectData{
UUID: uuid.NewString(),
Name: "test-3",
Slug: uuid.NewString(),
Description: "Test Project Number 1",
CreatedBy: createdBy,
CreatedAt: time.Now(),
ClosedAt: sql.NullTime{Time: time.Now()},
Blocked: false,
UpdatedAt: time.Now(),
UpdatedBy: createdBy,
}
assert.NoError(t, repository.CreateProject(t.Context(), newTestDBConnection(t.Context()), project))
assert.Error(t, repository.CreateProject(t.Context(), newTestDBConnection(t.Context()), project), repository.ErrAlreadyExists)
}
func TestIntegrationProjectsCreateAndGet_Success(t *testing.T) {
createdBy, err := accountForProject(t.Context())
assert.NoError(t, err)
project := &repository.ProjectData{
UUID: uuid.NewString(),
Name: "test-4",
Slug: uuid.NewString(),
Description: "Test Project Number 1",
CreatedBy: createdBy,
CreatedAt: time.Now(),
Blocked: false,
UpdatedAt: time.Now(),
UpdatedBy: createdBy,
}
assert.NoError(t, repository.CreateProject(t.Context(), newTestDBConnection(t.Context()), project))
data, err := repository.GetProjectByID(t.Context(), newTestDBConnection(t.Context()), project.UUID)
assert.NoError(t, err)
assert.Equal(t, project.Name, data.Name)
assert.Equal(t, project.Slug, data.Slug)
assert.Equal(t, project.Description, data.Description)
assert.Equal(t, project.CreatedBy, data.CreatedBy)
assert.Equal(t, project.ClosedAt.Time.Truncate(time.Second), data.ClosedAt.Time.Truncate(time.Second))
assert.Equal(t, project.CreatedAt.Truncate(time.Second), data.CreatedAt.Truncate(time.Second))
}
func TestIntegrationProjectsCreateAndGet_NotFound(t *testing.T) {
data, err := repository.GetProjectByID(t.Context(), newTestDBConnection(t.Context()), uuid.NewString())
assert.ErrorIs(t, err, repository.ErrNotFound)
assert.Nil(t, data)
}
func TestIntegrationProjectsCreateUpdateAndGet_Success(t *testing.T) {
createdBy, err := accountForProject(t.Context())
assert.NoError(t, err)
project := &repository.ProjectData{
UUID: uuid.NewString(),
Name: "test-5",
Slug: uuid.NewString(),
Description: "Test Project Number 1",
CreatedBy: createdBy,
CreatedAt: time.Now(),
Blocked: false,
UpdatedAt: time.Now(),
UpdatedBy: createdBy,
}
assert.NoError(t, repository.CreateProject(t.Context(), newTestDBConnection(t.Context()), project))
data, err := repository.GetProjectByID(t.Context(), newTestDBConnection(t.Context()), project.UUID)
assert.NoError(t, err)
assert.Equal(t, project.Name, data.Name)
assert.Equal(t, project.Slug, data.Slug)
assert.Equal(t, project.Description, data.Description)
assert.Equal(t, project.CreatedBy, data.CreatedBy)
assert.Equal(t, project.ClosedAt.Time.Truncate(time.Second), data.ClosedAt.Time.Truncate(time.Second))
assert.Equal(t, project.CreatedAt.Truncate(time.Second), data.CreatedAt.Truncate(time.Second))
project.UpdatedBy = uuid.NewString()
project.UpdatedAt = time.Now()
project.Description = "Updated description"
project.Name = "update name"
assert.NoError(t, repository.UpdateProject(t.Context(), newTestDBConnection(t.Context()), project))
data, err = repository.GetProjectByID(t.Context(), newTestDBConnection(t.Context()), project.UUID)
assert.NoError(t, err)
assert.Equal(t, project.Name, data.Name)
assert.Equal(t, project.Slug, data.Slug)
assert.Equal(t, project.Description, data.Description)
}
func TestIntegrationProjectsCreateAndGetOwner_Success(t *testing.T) {
createdBy, err := accountForProject(t.Context())
assert.NoError(t, err)
project := &repository.ProjectData{
UUID: uuid.NewString(),
Name: "test-6",
Slug: uuid.NewString(),
Description: "Test Project Number 1",
CreatedBy: createdBy,
CreatedAt: time.Now(),
ClosedAt: sql.NullTime{Time: time.Now()},
Blocked: false,
UpdatedAt: time.Now(),
UpdatedBy: createdBy,
}
assert.NoError(t, repository.CreateProject(t.Context(), newTestDBConnection(t.Context()), project))
userID, err := repository.GetProjectOwner(t.Context(), newTestDBConnection(t.Context()), project.UUID)
assert.NoError(t, err)
assert.Equal(t, project.CreatedBy, userID)
}
func TestIntegrationListProjectsWorkflow(t *testing.T) {
createdBy1, err := accountForProject(t.Context())
assert.NoError(t, err)
project1 := &repository.ProjectData{
UUID: uuid.NewString(),
Name: "List 1",
Slug: uuid.NewString(),
Description: "Test Project Number 1",
CreatedBy: createdBy1,
CreatedAt: time.Now(),
ClosedAt: sql.NullTime{Time: time.Now()},
Blocked: false,
UpdatedAt: time.Now(),
UpdatedBy: createdBy1,
}
assert.NoError(t, repository.CreateProject(t.Context(), newTestDBConnection(t.Context()), project1))
project2 := &repository.ProjectData{
UUID: uuid.NewString(),
Name: "List 2",
Slug: uuid.NewString(),
Description: "Test Project Number 1",
CreatedBy: createdBy1,
CreatedAt: time.Now(),
ClosedAt: sql.NullTime{Time: time.Now()},
Blocked: false,
UpdatedAt: time.Now(),
UpdatedBy: createdBy1,
}
assert.NoError(t, repository.CreateProject(t.Context(), newTestDBConnection(t.Context()), project2))
createdBy2, err := accountForProject(t.Context())
assert.NoError(t, err)
project3 := &repository.ProjectData{
UUID: uuid.NewString(),
Name: "List 3",
Slug: uuid.NewString(),
Description: "Test Project Number 1",
CreatedBy: createdBy2,
CreatedAt: time.Now(),
ClosedAt: sql.NullTime{Time: time.Now()},
Blocked: false,
UpdatedAt: time.Now(),
UpdatedBy: createdBy2,
}
assert.NoError(t, repository.CreateProject(t.Context(), newTestDBConnection(t.Context()), project3))
projects, err := repository.ListProjects(t.Context(), newTestDBConnection(t.Context()), createdBy1)
assert.NoError(t, err)
assert.Len(t, projects, 2)
projects, err = repository.ListProjects(t.Context(), newTestDBConnection(t.Context()), createdBy2)
assert.NoError(t, err)
assert.Len(t, projects, 1)
}

View File

@@ -0,0 +1,22 @@
package repository
import (
"context"
"database/sql"
)
func StartTransaction(ctx context.Context, db *sql.DB) (*sql.Tx, error) {
tx, err := db.BeginTx(ctx, nil)
if err != nil {
return nil, err
}
return tx, nil
}
func CommitTransaction(ctx context.Context, tx *sql.Tx) error {
return tx.Commit()
}
func RollBackTransaction(ctx context.Context, tx *sql.Tx) error {
return tx.Rollback()
}

View File

@@ -0,0 +1,95 @@
package repository_test
import (
"context"
"database/sql"
"os"
"testing"
"gitea.badhouseplants.net/softplayer/softplayer-backend/internal/helpers/postgres"
"gitea.badhouseplants.net/softplayer/softplayer-backend/internal/repository"
"github.com/stretchr/testify/assert"
)
func newTestDBConnection(ctx context.Context) *sql.DB {
connStr, ok := os.LookupEnv("SOFTPLAYER_DB_CONNECTION_STRING")
if !ok {
// Default connection string
connStr = "postgres://softplayer:qwertyu9@localhost:30432/softplayer?sslmode=disable"
}
db, err := postgres.Open(ctx, connStr)
if err != nil {
panic(err)
}
return db
}
func TestTransactionFlow_Success(t *testing.T) {
db := newTestDBConnection(t.Context())
tx, err := repository.StartTransaction(t.Context(), db)
assert.NoError(t, err)
query1 := "CREATE TABLE transaction_test_success (id SERIAL PRIMARY KEY, data VARCHAR (50) NOT NULL)"
_, err = db.Exec(query1)
assert.NoError(t, err)
data1 := "data1"
data2 := "data2"
query2 := "INSERT INTO transaction_test_success (data) VALUES ($1)"
_, err = tx.Exec(query2, data1)
assert.NoError(t, err)
_, err = tx.Exec(query2, data2)
assert.NoError(t, err)
assert.NoError(t, repository.CommitTransaction(t.Context(), tx))
exists := false
queryTest := `SELECT EXISTS(
SELECT 1 FROM transaction_test_success WHERE data = $1
)`
err = db.QueryRow(queryTest, data1).Scan(&exists)
assert.NoError(t, err)
assert.True(t, exists)
err = db.QueryRow(queryTest, data2).Scan(&exists)
assert.NoError(t, err)
assert.True(t, exists)
queryCleanup := "DROP TABLE transaction_test_success"
_, err = db.Exec(queryCleanup)
assert.NoError(t, err)
}
func TestTransactionFlow_Rollback(t *testing.T) {
db := newTestDBConnection(t.Context())
tx, err := repository.StartTransaction(t.Context(), db)
assert.NoError(t, err)
query1 := "CREATE TABLE transaction_test_rollback (id SERIAL PRIMARY KEY, data VARCHAR (50) NOT NULL)"
_, err = db.Exec(query1)
assert.NoError(t, err)
data1 := "data1"
data2 := "data2"
query2 := "INSERT INTO transaction_test_rollback (data) VALUES ($1)"
_, err = tx.Exec(query2, data1)
assert.NoError(t, err)
_, err = tx.Exec(query2, data2)
assert.NoError(t, err)
assert.NoError(t, repository.RollBackTransaction(t.Context(), tx))
exists := false
queryTest := `SELECT EXISTS(
SELECT 1 FROM transaction_test_rollback WHERE data = $1
)`
err = db.QueryRow(queryTest, data1).Scan(&exists)
assert.NoError(t, err)
assert.False(t, exists)
err = db.QueryRow(queryTest, data2).Scan(&exists)
assert.NoError(t, err)
assert.False(t, exists)
queryCleanup := "DROP TABLE transaction_test_rollback"
_, err = db.Exec(queryCleanup)
assert.NoError(t, err)
}

View File

@@ -1,4 +1,4 @@
package controllers package services
import ( import (
"context" "context"
@@ -41,6 +41,8 @@ type AccountData struct {
Password string Password string
Email string Email string
UUID string UUID string
Name string
Surname string
} }
// Create a new account // Create a new account
@@ -59,6 +61,8 @@ func (c *AccountController) Create(ctx context.Context, data *AccountData) (stri
UUID: data.UUID, UUID: data.UUID,
Email: data.Email, Email: data.Email,
PasswordHash: passwordHash, PasswordHash: passwordHash,
Name: data.Name,
Surname: data.Surname,
} }
if err := repository.CreateAccount(ctx, c.DB, queryData); err != nil { if err := repository.CreateAccount(ctx, c.DB, queryData); err != nil {

View File

@@ -1,4 +1,4 @@
package controllers_test package services_test
import ( import (
"context" "context"
@@ -8,8 +8,8 @@ import (
"testing" "testing"
"time" "time"
"gitea.badhouseplants.net/softplayer/softplayer-backend/internal/controllers"
"gitea.badhouseplants.net/softplayer/softplayer-backend/internal/helpers/postgres" "gitea.badhouseplants.net/softplayer/softplayer-backend/internal/helpers/postgres"
"gitea.badhouseplants.net/softplayer/softplayer-backend/internal/services"
"github.com/google/uuid" "github.com/google/uuid"
_ "github.com/jackc/pgx/v5/stdlib" _ "github.com/jackc/pgx/v5/stdlib"
"github.com/redis/go-redis/v9" "github.com/redis/go-redis/v9"
@@ -40,8 +40,8 @@ func newTestRedisConnection() *redis.Client {
}) })
} }
func newTestAccountController(ctx context.Context) *controllers.AccountController { func newTestAccountController(ctx context.Context) *services.AccountController {
return &controllers.AccountController{ return &services.AccountController{
DB: newTestDBConnection(ctx), DB: newTestDBConnection(ctx),
Redis: newTestRedisConnection(), Redis: newTestRedisConnection(),
DevMode: true, DevMode: true,
@@ -67,9 +67,11 @@ func newTestUniqueEmail(prefix string) string {
func TestIntegrationAccountCreate_Success(t *testing.T) { func TestIntegrationAccountCreate_Success(t *testing.T) {
ctrl := newTestAccountController(t.Context()) ctrl := newTestAccountController(t.Context())
accountData := &controllers.AccountData{ accountData := &services.AccountData{
Password: "qwertyu9", Password: "qwertyu9",
Email: newTestUniqueEmail("accounts"), Email: newTestUniqueEmail("accounts"),
Surname: "Doe",
Name: "John",
} }
id, err := ctrl.Create(t.Context(), accountData) id, err := ctrl.Create(t.Context(), accountData)
assert.NoError(t, err) assert.NoError(t, err)
@@ -79,9 +81,11 @@ func TestIntegrationAccountCreate_Success(t *testing.T) {
func TestIntegrationAccountCreate_ExistingAccountErr(t *testing.T) { func TestIntegrationAccountCreate_ExistingAccountErr(t *testing.T) {
ctrl := newTestAccountController(t.Context()) ctrl := newTestAccountController(t.Context())
email := newTestUniqueEmail("accounts") email := newTestUniqueEmail("accounts")
accountData := &controllers.AccountData{ accountData := &services.AccountData{
Password: "qwertyu9", Password: "qwertyu9",
Email: email, Email: email,
Surname: "Doe",
Name: "John",
} }
id, err := ctrl.Create(t.Context(), accountData) id, err := ctrl.Create(t.Context(), accountData)
assert.NoError(t, err) assert.NoError(t, err)
@@ -90,13 +94,13 @@ func TestIntegrationAccountCreate_ExistingAccountErr(t *testing.T) {
id, err = ctrl.Create(t.Context(), accountData) id, err = ctrl.Create(t.Context(), accountData)
assert.Empty(t, id) assert.Empty(t, id)
assert.Error(t, err) assert.Error(t, err)
assert.ErrorIs(t, err, controllers.ErrEmailUsed) assert.ErrorIs(t, err, services.ErrEmailUsed)
} }
func TestIntegrationAccountLogin_Success(t *testing.T) { func TestIntegrationAccountLogin_Success(t *testing.T) {
ctrl := newTestAccountController(t.Context()) ctrl := newTestAccountController(t.Context())
email := newTestUniqueEmail("accounts") email := newTestUniqueEmail("accounts")
accountData := &controllers.AccountData{ accountData := &services.AccountData{
Password: "qwertyu9", Password: "qwertyu9",
Email: email, Email: email,
} }
@@ -113,7 +117,7 @@ func TestIntegrationAccountLogin_Success(t *testing.T) {
func TestIntegrationAccountLogin_WrongPassword(t *testing.T) { func TestIntegrationAccountLogin_WrongPassword(t *testing.T) {
ctrl := newTestAccountController(t.Context()) ctrl := newTestAccountController(t.Context())
email := newTestUniqueEmail("accounts") email := newTestUniqueEmail("accounts")
accountData := &controllers.AccountData{ accountData := &services.AccountData{
Password: "qwertyu9", Password: "qwertyu9",
Email: email, Email: email,
} }
@@ -125,13 +129,13 @@ func TestIntegrationAccountLogin_WrongPassword(t *testing.T) {
id, err = ctrl.Login(t.Context(), accountData.Email, "Wrong Password") id, err = ctrl.Login(t.Context(), accountData.Email, "Wrong Password")
assert.Empty(t, id) assert.Empty(t, id)
assert.Error(t, err) assert.Error(t, err)
assert.ErrorIs(t, err, controllers.ErrWrongPassword) assert.ErrorIs(t, err, services.ErrWrongPassword)
} }
func TestIntegrationAccountLogin_WrongEmail(t *testing.T) { func TestIntegrationAccountLogin_WrongEmail(t *testing.T) {
ctrl := newTestAccountController(t.Context()) ctrl := newTestAccountController(t.Context())
email := newTestUniqueEmail("accounts") email := newTestUniqueEmail("accounts")
accountData := &controllers.AccountData{ accountData := &services.AccountData{
Password: "qwertyu9", Password: "qwertyu9",
Email: email, Email: email,
} }
@@ -143,5 +147,5 @@ func TestIntegrationAccountLogin_WrongEmail(t *testing.T) {
id, err = ctrl.Login(t.Context(), "some@email.com", "Wrong Password") id, err = ctrl.Login(t.Context(), "some@email.com", "Wrong Password")
assert.Empty(t, id) assert.Empty(t, id)
assert.Error(t, err) assert.Error(t, err)
assert.ErrorIs(t, err, controllers.ErrUserNotFound) assert.ErrorIs(t, err, services.ErrUserNotFound)
} }

View File

@@ -1,4 +1,4 @@
package controllers package services
import ( import (
"context" "context"

View File

@@ -1,10 +1,10 @@
package controllers_test package services_test
import ( import (
"testing" "testing"
"time" "time"
"gitea.badhouseplants.net/softplayer/softplayer-backend/internal/controllers" "gitea.badhouseplants.net/softplayer/softplayer-backend/internal/services"
"github.com/google/uuid" "github.com/google/uuid"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
) )
@@ -16,24 +16,24 @@ var (
) )
func TestGenerateInvalidTokenType(t *testing.T) { func TestGenerateInvalidTokenType(t *testing.T) {
data := &controllers.JWTData{ data := &services.JWTData{
UserID: testUserID, UserID: testUserID,
TokenType: "invalid_type", TokenType: "invalid_type",
} }
authCtrl := controllers.NewAuthController([]byte("test"), testAccessTTL, testRefreshTTL, nil) authCtrl := services.NewAuthController([]byte("test"), testAccessTTL, testRefreshTTL, nil)
token, _, err := authCtrl.GenerateToken(data) token, _, err := authCtrl.GenerateToken(data)
assert.Equal(t, "", token) assert.Equal(t, "", token)
assert.ErrorIs(t, controllers.ErrUnknownTokenType, err) assert.ErrorIs(t, services.ErrUnknownTokenType, err)
} }
func TestGenerateValidateAccessToken(t *testing.T) { func TestGenerateValidateAccessToken(t *testing.T) {
data := &controllers.JWTData{ data := &services.JWTData{
UserID: testUserID, UserID: testUserID,
TokenType: controllers.TokenTypeAccess, TokenType: services.TokenTypeAccess,
} }
authCtrl := controllers.NewAuthController([]byte("test"), testAccessTTL, testRefreshTTL, nil) authCtrl := services.NewAuthController([]byte("test"), testAccessTTL, testRefreshTTL, nil)
now := time.Now() now := time.Now()
token, _, err := authCtrl.GenerateToken(data) token, _, err := authCtrl.GenerateToken(data)
assert.NoError(t, err) assert.NoError(t, err)
@@ -43,18 +43,18 @@ func TestGenerateValidateAccessToken(t *testing.T) {
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, testUserID, claims.UserID) assert.Equal(t, testUserID, claims.UserID)
assert.NotEmpty(t, claims.TokenID) assert.NotEmpty(t, claims.TokenID)
assert.Equal(t, controllers.TokenTypeAccess, claims.TokenType) assert.Equal(t, services.TokenTypeAccess, claims.TokenType)
assert.Equal(t, now.Add(testAccessTTL).Unix(), claims.ExpiresAt.Unix()) assert.Equal(t, now.Add(testAccessTTL).Unix(), claims.ExpiresAt.Unix())
assert.Equal(t, now.Unix(), claims.IssuedAt.Unix()) assert.Equal(t, now.Unix(), claims.IssuedAt.Unix())
assert.Equal(t, now.Unix(), claims.NotBefore.Unix()) assert.Equal(t, now.Unix(), claims.NotBefore.Unix())
} }
func TestGenerateValidateRefreshToken(t *testing.T) { func TestGenerateValidateRefreshToken(t *testing.T) {
data := &controllers.JWTData{ data := &services.JWTData{
UserID: testUserID, UserID: testUserID,
TokenType: controllers.TokenTypeRefresh, TokenType: services.TokenTypeRefresh,
} }
authCtrl := controllers.NewAuthController([]byte("test"), testAccessTTL, testRefreshTTL, nil) authCtrl := services.NewAuthController([]byte("test"), testAccessTTL, testRefreshTTL, nil)
now := time.Now() now := time.Now()
token, _, err := authCtrl.GenerateToken(data) token, _, err := authCtrl.GenerateToken(data)
assert.NoError(t, err) assert.NoError(t, err)
@@ -64,7 +64,7 @@ func TestGenerateValidateRefreshToken(t *testing.T) {
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, testUserID, claims.UserID) assert.Equal(t, testUserID, claims.UserID)
assert.NotEmpty(t, claims.TokenID) assert.NotEmpty(t, claims.TokenID)
assert.Equal(t, controllers.TokenTypeRefresh, claims.TokenType) assert.Equal(t, services.TokenTypeRefresh, claims.TokenType)
assert.Equal(t, now.Add(testRefreshTTL).Unix(), claims.ExpiresAt.Unix()) assert.Equal(t, now.Add(testRefreshTTL).Unix(), claims.ExpiresAt.Unix())
assert.Equal(t, now.Unix(), claims.IssuedAt.Unix()) assert.Equal(t, now.Unix(), claims.IssuedAt.Unix())
assert.Equal(t, now.Unix(), claims.NotBefore.Unix()) assert.Equal(t, now.Unix(), claims.NotBefore.Unix())

View File

@@ -0,0 +1,71 @@
package services
import (
"context"
"database/sql"
"errors"
"time"
"gitea.badhouseplants.net/softplayer/softplayer-backend/internal/helpers/logger"
"gitea.badhouseplants.net/softplayer/softplayer-backend/internal/repository"
"github.com/google/uuid"
)
type ProjectsController struct {
DB *sql.DB
}
type ProjectData struct {
UUID string
Name string
Slug string
Description string
}
var (
ErrProjectExists = errors.New("project exists")
ErrInvalidProject = errors.New("invalid project data")
)
// Create a new project
func (ctrl *ProjectsController) Create(ctx context.Context, data *ProjectData, userID string) (id string, err error) {
id = uuid.NewString()
log := logger.FromContext(ctx).WithValues("id", id)
log.V(2).Info("Creating a project")
queryData := &repository.ProjectData{
UUID: id,
Name: data.Name,
Slug: data.Slug,
Description: data.Description,
CreatedBy: userID,
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
UpdatedBy: userID,
}
err = repository.CreateProject(ctx, ctrl.DB, queryData)
if err != nil {
if errors.Is(err, repository.ErrAlreadyExists) {
return "", ErrProjectExists
} else if errors.Is(err, repository.ErrCheckNotPassed) {
return "", ErrInvalidProject
}
log.Error(err, "Couldn't create a project")
return "", ErrServerError
}
return id, nil
}
// Update an existing project
func (ctrl *ProjectsController) Update(ctx context.Context, data *ProjectData) error {
return nil
}
// Get an existing project by ID
func (ctrl *ProjectsController) Get(ctx context.Context, projectID string) (data *ProjectData, err error) {
return nil, nil
}
// List projects available for a user
func (ctrl *ProjectsController) List(ctx context.Context) (data []*ProjectData, err error) {
return nil, nil
}

View File

@@ -0,0 +1,38 @@
package services_test
import (
"context"
"testing"
"gitea.badhouseplants.net/softplayer/softplayer-backend/internal/services"
"github.com/google/uuid"
"github.com/stretchr/testify/assert"
)
func newTestProjectsController(ctx context.Context) *services.ProjectsController {
return &services.ProjectsController{
DB: newTestDBConnection(ctx),
}
}
func TestIntegrationCreateProject_Success(t *testing.T) {
ctrlAccount := newTestAccountController(t.Context())
accountData := &services.AccountData{
Password: "qwertyu9",
Email: newTestUniqueEmail("projects"),
}
userID, err := ctrlAccount.Create(t.Context(), accountData)
assert.NoError(t, err)
ctrlProjects := newTestProjectsController(t.Context())
projectData := &services.ProjectData{
UUID: uuid.NewString(),
Name: "Test project",
Slug: uuid.NewString(),
Description: "Test project",
}
projectID, err := ctrlProjects.Create(t.Context(), projectData, userID)
assert.NoError(t, err)
assert.NotEmpty(t, projectID)
}

View File

@@ -1,6 +1,6 @@
// Package controllers for token management // Package controllers for token management
// This a token controller, that implements the logic around tokens // This a token controller, that implements the logic around tokens
package controllers package services
import ( import (
"context" "context"

View File

@@ -1,4 +1,4 @@
package controllers_test package services_test
import ( import (
"context" "context"
@@ -6,13 +6,13 @@ import (
"testing" "testing"
"time" "time"
"gitea.badhouseplants.net/softplayer/softplayer-backend/internal/controllers" "gitea.badhouseplants.net/softplayer/softplayer-backend/internal/services"
"github.com/google/uuid" "github.com/google/uuid"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
) )
func newTestTokensController(ctx context.Context) *controllers.TokenController { func newTestTokensController(ctx context.Context) *services.TokenController {
return &controllers.TokenController{ return &services.TokenController{
DB: newTestDBConnection(ctx), DB: newTestDBConnection(ctx),
Redis: newTestRedisConnection(), Redis: newTestRedisConnection(),
} }
@@ -21,14 +21,14 @@ func newTestTokensController(ctx context.Context) *controllers.TokenController {
func TestIntegrationCreateToken_Success(t *testing.T) { func TestIntegrationCreateToken_Success(t *testing.T) {
// Create a user for the token // Create a user for the token
ctrlAccount := newTestAccountController(t.Context()) ctrlAccount := newTestAccountController(t.Context())
accountData := &controllers.AccountData{ accountData := &services.AccountData{
Password: "qwertyu9", Password: "qwertyu9",
Email: newTestUniqueEmail("accounts"), Email: newTestUniqueEmail("accounts"),
} }
id, err := ctrlAccount.Create(t.Context(), accountData) id, err := ctrlAccount.Create(t.Context(), accountData)
assert.NoError(t, err) assert.NoError(t, err)
tokenData := &controllers.TokenData{ tokenData := &services.TokenData{
Name: "Test Token", Name: "Test Token",
UserID: id, UserID: id,
ExpiresAt: time.Now().Add(time.Second * 5), ExpiresAt: time.Now().Add(time.Second * 5),
@@ -45,7 +45,7 @@ func TestIntegrationCreateToken_Success(t *testing.T) {
} }
func TestIntegrationCreateToken_UserNotExist(t *testing.T) { func TestIntegrationCreateToken_UserNotExist(t *testing.T) {
tokenData := &controllers.TokenData{ tokenData := &services.TokenData{
Name: "Test Token", Name: "Test Token",
UserID: uuid.NewString(), UserID: uuid.NewString(),
ExpiresAt: time.Now().Add(time.Second * 5), ExpiresAt: time.Now().Add(time.Second * 5),
@@ -58,7 +58,7 @@ func TestIntegrationCreateToken_UserNotExist(t *testing.T) {
tokenVal, tokenID, err := ctrl.Create(t.Context(), tokenData) tokenVal, tokenID, err := ctrl.Create(t.Context(), tokenData)
assert.Error(t, err) assert.Error(t, err)
assert.ErrorIs(t, err, controllers.ErrUserNotFound) assert.ErrorIs(t, err, services.ErrUserNotFound)
assert.Empty(t, tokenID) assert.Empty(t, tokenID)
assert.Empty(t, tokenVal) assert.Empty(t, tokenVal)
} }
@@ -66,7 +66,7 @@ func TestIntegrationCreateToken_UserNotExist(t *testing.T) {
func TestIntegrationGetToken_Success(t *testing.T) { func TestIntegrationGetToken_Success(t *testing.T) {
// Create a user for the token // Create a user for the token
ctrlAccount := newTestAccountController(t.Context()) ctrlAccount := newTestAccountController(t.Context())
accountData := &controllers.AccountData{ accountData := &services.AccountData{
Password: "qwertyu9", Password: "qwertyu9",
Email: newTestUniqueEmail("accounts"), Email: newTestUniqueEmail("accounts"),
} }
@@ -75,7 +75,7 @@ func TestIntegrationGetToken_Success(t *testing.T) {
userID, err := ctrlAccount.Create(t.Context(), accountData) userID, err := ctrlAccount.Create(t.Context(), accountData)
assert.NoError(t, err) assert.NoError(t, err)
tokenData := &controllers.TokenData{ tokenData := &services.TokenData{
Name: "Test Token", Name: "Test Token",
UserID: userID, UserID: userID,
ExpiresAt: time.Now().Add(time.Second * 5), ExpiresAt: time.Now().Add(time.Second * 5),
@@ -101,7 +101,7 @@ func TestIntegrationGetToken_NotExists(t *testing.T) {
ctrl := newTestTokensController(t.Context()) ctrl := newTestTokensController(t.Context())
token, err := ctrl.Get(t.Context(), uuid.NewString(), uuid.NewString()) token, err := ctrl.Get(t.Context(), uuid.NewString(), uuid.NewString())
assert.Error(t, err) assert.Error(t, err)
assert.ErrorIs(t, err, controllers.ErrTokenNotFound) assert.ErrorIs(t, err, services.ErrTokenNotFound)
assert.Empty(t, token) assert.Empty(t, token)
} }
@@ -109,14 +109,14 @@ func TestIntegrationGetToken_WrongRequest(t *testing.T) {
ctrl := newTestTokensController(t.Context()) ctrl := newTestTokensController(t.Context())
token, err := ctrl.Get(t.Context(), "test", "test") token, err := ctrl.Get(t.Context(), "test", "test")
assert.Error(t, err) assert.Error(t, err)
assert.ErrorIs(t, err, controllers.ErrServerError) assert.ErrorIs(t, err, services.ErrServerError)
assert.Empty(t, token) assert.Empty(t, token)
} }
func TestIntegrationVerifyTokenOwner_Success(t *testing.T) { func TestIntegrationVerifyTokenOwner_Success(t *testing.T) {
// Create a user for the token // Create a user for the token
ctrlAccount := newTestAccountController(t.Context()) ctrlAccount := newTestAccountController(t.Context())
accountData := &controllers.AccountData{ accountData := &services.AccountData{
Password: "qwertyu9", Password: "qwertyu9",
Email: newTestUniqueEmail("accounts"), Email: newTestUniqueEmail("accounts"),
} }
@@ -124,7 +124,7 @@ func TestIntegrationVerifyTokenOwner_Success(t *testing.T) {
userID, err := ctrlAccount.Create(t.Context(), accountData) userID, err := ctrlAccount.Create(t.Context(), accountData)
assert.NoError(t, err) assert.NoError(t, err)
tokenData := &controllers.TokenData{ tokenData := &services.TokenData{
Name: "Test Token", Name: "Test Token",
UserID: userID, UserID: userID,
ExpiresAt: time.Now().Add(time.Second * 5), ExpiresAt: time.Now().Add(time.Second * 5),
@@ -142,12 +142,12 @@ func TestIntegrationVerifyTokenOwner_Success(t *testing.T) {
func TestIntegrationVerifyTokenOwner_WrongOwner(t *testing.T) { func TestIntegrationVerifyTokenOwner_WrongOwner(t *testing.T) {
// Create a user for the token // Create a user for the token
ctrlAccount := newTestAccountController(t.Context()) ctrlAccount := newTestAccountController(t.Context())
accountData := &controllers.AccountData{ accountData := &services.AccountData{
Password: "qwertyu9", Password: "qwertyu9",
Email: newTestUniqueEmail("accounts"), Email: newTestUniqueEmail("accounts"),
} }
secondAccountData := &controllers.AccountData{ secondAccountData := &services.AccountData{
Password: "qwertyu9", Password: "qwertyu9",
Email: newTestUniqueEmail("accounts"), Email: newTestUniqueEmail("accounts"),
} }
@@ -157,7 +157,7 @@ func TestIntegrationVerifyTokenOwner_WrongOwner(t *testing.T) {
secondUserID, err := ctrlAccount.Create(t.Context(), secondAccountData) secondUserID, err := ctrlAccount.Create(t.Context(), secondAccountData)
assert.NoError(t, err) assert.NoError(t, err)
tokenData := &controllers.TokenData{ tokenData := &services.TokenData{
Name: "Test Token", Name: "Test Token",
UserID: userID, UserID: userID,
ExpiresAt: time.Now().Add(time.Second * 5), ExpiresAt: time.Now().Add(time.Second * 5),
@@ -169,13 +169,13 @@ func TestIntegrationVerifyTokenOwner_WrongOwner(t *testing.T) {
ctrl := newTestTokensController(t.Context()) ctrl := newTestTokensController(t.Context())
_, tokenID, err := ctrl.Create(t.Context(), tokenData) _, tokenID, err := ctrl.Create(t.Context(), tokenData)
assert.NoError(t, err) assert.NoError(t, err)
assert.ErrorIs(t, ctrl.VerifyTokenOwner(t.Context(), secondUserID, tokenID), controllers.ErrUserTokenMismatch) assert.ErrorIs(t, ctrl.VerifyTokenOwner(t.Context(), secondUserID, tokenID), services.ErrUserTokenMismatch)
} }
func TestIntegrationForceExpiration_Success(t *testing.T) { func TestIntegrationForceExpiration_Success(t *testing.T) {
// Create a user for the token // Create a user for the token
ctrlAccount := newTestAccountController(t.Context()) ctrlAccount := newTestAccountController(t.Context())
accountData := &controllers.AccountData{ accountData := &services.AccountData{
Password: "qwertyu9", Password: "qwertyu9",
Email: newTestUniqueEmail("accounts"), Email: newTestUniqueEmail("accounts"),
} }
@@ -183,7 +183,7 @@ func TestIntegrationForceExpiration_Success(t *testing.T) {
userID, err := ctrlAccount.Create(t.Context(), accountData) userID, err := ctrlAccount.Create(t.Context(), accountData)
assert.NoError(t, err) assert.NoError(t, err)
tokenData := &controllers.TokenData{ tokenData := &services.TokenData{
Name: "Test Token", Name: "Test Token",
UserID: userID, UserID: userID,
ExpiresAt: time.Now().Add(time.Second * 5), ExpiresAt: time.Now().Add(time.Second * 5),
@@ -205,7 +205,7 @@ func TestIntegrationForceExpiration_Success(t *testing.T) {
func TestIntegrationRegenerateToken_Success(t *testing.T) { func TestIntegrationRegenerateToken_Success(t *testing.T) {
// Create a user for the token // Create a user for the token
ctrlAccount := newTestAccountController(t.Context()) ctrlAccount := newTestAccountController(t.Context())
accountData := &controllers.AccountData{ accountData := &services.AccountData{
Password: "qwertyu9", Password: "qwertyu9",
Email: newTestUniqueEmail("accounts"), Email: newTestUniqueEmail("accounts"),
} }
@@ -214,7 +214,7 @@ func TestIntegrationRegenerateToken_Success(t *testing.T) {
userID, err := ctrlAccount.Create(t.Context(), accountData) userID, err := ctrlAccount.Create(t.Context(), accountData)
assert.NoError(t, err) assert.NoError(t, err)
tokenData := &controllers.TokenData{ tokenData := &services.TokenData{
Name: "Test Token", Name: "Test Token",
UserID: userID, UserID: userID,
ExpiresAt: time.Now().Add(time.Second * 5), ExpiresAt: time.Now().Add(time.Second * 5),
@@ -255,7 +255,7 @@ func TestIntegrationRegenerateToken_Success(t *testing.T) {
func TestIntegrationListTokens_Success(t *testing.T) { func TestIntegrationListTokens_Success(t *testing.T) {
// Create a user for the token // Create a user for the token
ctrlAccount := newTestAccountController(t.Context()) ctrlAccount := newTestAccountController(t.Context())
accountData := &controllers.AccountData{ accountData := &services.AccountData{
Password: "qwertyu9", Password: "qwertyu9",
Email: newTestUniqueEmail("accounts"), Email: newTestUniqueEmail("accounts"),
} }
@@ -263,7 +263,7 @@ func TestIntegrationListTokens_Success(t *testing.T) {
userID, err := ctrlAccount.Create(t.Context(), accountData) userID, err := ctrlAccount.Create(t.Context(), accountData)
assert.NoError(t, err) assert.NoError(t, err)
tokenDataOne := &controllers.TokenData{ tokenDataOne := &services.TokenData{
Name: "Test Token", Name: "Test Token",
UserID: userID, UserID: userID,
ExpiresAt: time.Now().Add(time.Second * 5), ExpiresAt: time.Now().Add(time.Second * 5),
@@ -271,7 +271,7 @@ func TestIntegrationListTokens_Success(t *testing.T) {
"Test": {"test", "test2"}, "Test": {"test", "test2"},
}, },
} }
tokenDataTwo := &controllers.TokenData{ tokenDataTwo := &services.TokenData{
Name: "Test Token again", Name: "Test Token again",
UserID: userID, UserID: userID,
ExpiresAt: time.Now().Add(time.Second * 5), ExpiresAt: time.Now().Add(time.Second * 5),
@@ -293,14 +293,14 @@ func TestIntegrationListTokens_Success(t *testing.T) {
func TestIntegrationAuthenticateWithToken_Success(t *testing.T) { func TestIntegrationAuthenticateWithToken_Success(t *testing.T) {
ctrlAccount := newTestAccountController(t.Context()) ctrlAccount := newTestAccountController(t.Context())
accountData := &controllers.AccountData{ accountData := &services.AccountData{
Password: "qwertyu9", Password: "qwertyu9",
Email: newTestUniqueEmail("accounts"), Email: newTestUniqueEmail("accounts"),
} }
id, err := ctrlAccount.Create(t.Context(), accountData) id, err := ctrlAccount.Create(t.Context(), accountData)
assert.NoError(t, err) assert.NoError(t, err)
tokenData := &controllers.TokenData{ tokenData := &services.TokenData{
Name: "Test Token", Name: "Test Token",
UserID: id, UserID: id,
ExpiresAt: time.Now().Add(time.Second * 5), ExpiresAt: time.Now().Add(time.Second * 5),
@@ -330,19 +330,19 @@ func TestIntegrationAuthenticateWithToken_UnknownToken(t *testing.T) {
auth, err := ctrl.AuthenticateWithToken(t.Context(), "dummy") auth, err := ctrl.AuthenticateWithToken(t.Context(), "dummy")
assert.Error(t, err) assert.Error(t, err)
assert.Nil(t, auth) assert.Nil(t, auth)
assert.ErrorIs(t, err, controllers.ErrTokenNotFound) assert.ErrorIs(t, err, services.ErrTokenNotFound)
} }
func TestIntegrationAuthenticateWithToken_Expired(t *testing.T) { func TestIntegrationAuthenticateWithToken_Expired(t *testing.T) {
ctrlAccount := newTestAccountController(t.Context()) ctrlAccount := newTestAccountController(t.Context())
accountData := &controllers.AccountData{ accountData := &services.AccountData{
Password: "qwertyu9", Password: "qwertyu9",
Email: newTestUniqueEmail("accounts"), Email: newTestUniqueEmail("accounts"),
} }
id, err := ctrlAccount.Create(t.Context(), accountData) id, err := ctrlAccount.Create(t.Context(), accountData)
assert.NoError(t, err) assert.NoError(t, err)
tokenData := &controllers.TokenData{ tokenData := &services.TokenData{
Name: "Test Token", Name: "Test Token",
UserID: id, UserID: id,
ExpiresAt: time.Now().Add(time.Second * 5), ExpiresAt: time.Now().Add(time.Second * 5),
@@ -365,19 +365,19 @@ func TestIntegrationAuthenticateWithToken_Expired(t *testing.T) {
auth, err = ctrl.AuthenticateWithToken(t.Context(), tokenVal) auth, err = ctrl.AuthenticateWithToken(t.Context(), tokenVal)
assert.Error(t, err) assert.Error(t, err)
assert.Nil(t, auth) assert.Nil(t, auth)
assert.ErrorIs(t, err, controllers.ErrBadToken) assert.ErrorIs(t, err, services.ErrBadToken)
} }
func TestIntegrationAuthenticateWithToken_Revoked(t *testing.T) { func TestIntegrationAuthenticateWithToken_Revoked(t *testing.T) {
ctrlAccount := newTestAccountController(t.Context()) ctrlAccount := newTestAccountController(t.Context())
accountData := &controllers.AccountData{ accountData := &services.AccountData{
Password: "qwertyu9", Password: "qwertyu9",
Email: newTestUniqueEmail("accounts"), Email: newTestUniqueEmail("accounts"),
} }
id, err := ctrlAccount.Create(t.Context(), accountData) id, err := ctrlAccount.Create(t.Context(), accountData)
assert.NoError(t, err) assert.NoError(t, err)
tokenData := &controllers.TokenData{ tokenData := &services.TokenData{
Name: "Test Token", Name: "Test Token",
UserID: id, UserID: id,
ExpiresAt: time.Now().Add(time.Second * 5), ExpiresAt: time.Now().Add(time.Second * 5),
@@ -399,5 +399,5 @@ func TestIntegrationAuthenticateWithToken_Revoked(t *testing.T) {
auth, err = ctrl.AuthenticateWithToken(t.Context(), tokenVal) auth, err = ctrl.AuthenticateWithToken(t.Context(), tokenVal)
assert.Error(t, err) assert.Error(t, err)
assert.Nil(t, auth) assert.Nil(t, auth)
assert.ErrorIs(t, err, controllers.ErrBadToken) assert.ErrorIs(t, err, services.ErrBadToken)
} }

View File

@@ -0,0 +1,3 @@
ALTER TABLE accounts
DROP COLUMN name,
DROP COLUMN surname;

View File

@@ -0,0 +1,14 @@
-- Up migration (safe for existing data)
ALTER TABLE accounts
ADD COLUMN name TEXT,
ADD COLUMN surname TEXT;
UPDATE accounts
SET
name = 'John',
surname = 'Doe'
WHERE name IS NULL OR surname IS NULL;
ALTER TABLE accounts
ALTER COLUMN name SET NOT NULL,
ALTER COLUMN surname SET NOT NULL;

View File

@@ -0,0 +1 @@
DROP TABLE projects;

View File

@@ -0,0 +1,16 @@
CREATE TABLE projects (
uuid UUID PRIMARY KEY,
name VARCHAR(120) NOT NULL,
slug VARCHAR(120) NOT NULL UNIQUE
CHECK (
slug ~ '^[a-z0-9]+(?:-[a-z0-9]+)*$'
),
description TEXT,
owner_user_id UUID NOT NULL,
closed_at TIMESTAMPTZ NULL,
billing_account_id UUID NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
created_by UUID NOT NULL,
updated_by UUID NOT NULL
);

View File

@@ -0,0 +1,7 @@
DROP INDEX idx_project_membership_project;
DROP INDEX idx_project_membership_user;
DROP TABLE project_membership;
DROP TYPE membership_status CASCADE;
DROP TYPE project_role CASCADE;

View File

@@ -0,0 +1,35 @@
CREATE TYPE project_role AS ENUM (
'member',
'admin',
'owner'
);
CREATE TYPE membership_status AS ENUM (
'invited',
'active',
'suspended'
);
CREATE TABLE project_membership (
project_uuid UUID NOT NULL
REFERENCES projects(uuid)
ON DELETE CASCADE,
user_uuid UUID NOT NULL
REFERENCES accounts(uuid)
ON DELETE CASCADE,
role project_role NOT NULL,
status membership_status NOT NULL DEFAULT 'active',
invited_by UUID NULL
REFERENCES accounts(uuid),
joined_at TIMESTAMP NULL,
PRIMARY KEY (project_uuid, user_uuid)
);
CREATE INDEX idx_project_membership_user
ON project_membership(user_uuid);
CREATE INDEX idx_project_membership_project
ON project_membership(project_uuid);