Compare commits
12 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
41c58b400f
|
|||
|
21a4c53e0f
|
|||
|
4e617c90ef
|
|||
|
e132e143bd
|
|||
|
a4ab4fa6c6
|
|||
|
891b9854b5
|
|||
|
d1d4cba05b
|
|||
|
9c558d601b
|
|||
|
2285bb10ae
|
|||
|
28235765cd
|
|||
|
c8d3112fbd
|
|||
|
25ebb4805e
|
35
Taskfile.yml
35
Taskfile.yml
@@ -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"
|
||||||
|
|||||||
@@ -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"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -44,6 +48,8 @@ type Server struct {
|
|||||||
// 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,7 +74,7 @@ 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.RefrestTokenTTL,
|
cmd.RefrestTokenTTL,
|
||||||
@@ -97,12 +103,12 @@ 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,
|
||||||
@@ -120,6 +126,33 @@ func (cmd *Server) Run(ctx context.Context) error {
|
|||||||
tokens.RegisterPublicTokensServiceServer(grpcServer, v1.NewPublicTokensServer(tokenCtrl, authController))
|
tokens.RegisterPublicTokensServiceServer(grpcServer, v1.NewPublicTokensServer(tokenCtrl, authController))
|
||||||
accounts.RegisterPublicAccountsServiceServer(grpcServer, v1.NewPublicAccountServer(accountCtrl, 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)
|
||||||
tokenCtrl.SetRules()
|
tokenCtrl.SetRules()
|
||||||
@@ -144,6 +177,10 @@ func selectorRequireAuth(ctx context.Context, callMeta interceptors.CallMeta) bo
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if serviceName == "Health" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
if strings.Contains(serviceName, "ServerReflection") {
|
if strings.Contains(serviceName, "ServerReflection") {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
@@ -154,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
|
||||||
|
}
|
||||||
|
|||||||
2
go.mod
2
go.mod
@@ -43,7 +43,7 @@ require (
|
|||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
gitea.badhouseplants.net/softplayer/softplayer-go-proto v0.0.0-20260517200845-22f1b32dfad9
|
gitea.badhouseplants.net/softplayer/softplayer-go-proto v0.0.0-20260518175130-4b27db42e21e
|
||||||
github.com/golang/protobuf v1.5.4
|
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
|
||||||
|
|||||||
4
go.sum
4
go.sum
@@ -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-20260517200845-22f1b32dfad9 h1:RP73i+SOZYmc61F+gZjO/rvUlpPP0Za4MLJKAgS+1YI=
|
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-20260517200845-22f1b32dfad9/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=
|
||||||
|
|||||||
@@ -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"
|
||||||
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"
|
"github.com/golang/protobuf/ptypes/empty"
|
||||||
"google.golang.org/grpc"
|
"google.golang.org/grpc"
|
||||||
@@ -15,8 +15,8 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func NewAccountServer(
|
func NewAccountServer(
|
||||||
accountsCtrl *controllers.AccountController,
|
accountsCtrl *services.AccountController,
|
||||||
authorizationCtrl *controllers.AuthController,
|
authorizationCtrl *services.AuthController,
|
||||||
) *AccountsServer {
|
) *AccountsServer {
|
||||||
return &AccountsServer{
|
return &AccountsServer{
|
||||||
accountsCtrl: accountsCtrl,
|
accountsCtrl: accountsCtrl,
|
||||||
@@ -26,23 +26,23 @@ func NewAccountServer(
|
|||||||
|
|
||||||
type AccountsServer struct {
|
type AccountsServer struct {
|
||||||
accounts.UnimplementedAccountsServiceServer
|
accounts.UnimplementedAccountsServiceServer
|
||||||
accountsCtrl *controllers.AccountController
|
accountsCtrl *services.AccountController
|
||||||
authorizationCtrl *controllers.AuthController
|
authorizationCtrl *services.AuthController
|
||||||
}
|
}
|
||||||
|
|
||||||
func (srv *AccountsServer) RefreshToken(ctx context.Context, in *empty.Empty) (*empty.Empty, error) {
|
func (srv *AccountsServer) RefreshToken(ctx context.Context, in *empty.Empty) (*empty.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")
|
||||||
}
|
}
|
||||||
|
|
||||||
if claims.TokenType != controllers.TokenTypeRefresh {
|
if claims.TokenType != services.TokenTypeRefresh {
|
||||||
return nil, status.Error(codes.Unauthenticated, "Invalid token")
|
return nil, status.Error(codes.Unauthenticated, "Invalid token")
|
||||||
}
|
}
|
||||||
|
|
||||||
session, err := srv.authorizationCtrl.GetSession(ctx, claims.TokenID)
|
session, err := srv.authorizationCtrl.GetSession(ctx, claims.TokenID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, controllers.ErrSessionNotFound) {
|
if errors.Is(err, services.ErrSessionNotFound) {
|
||||||
return nil, status.Error(codes.Unauthenticated, "Session doesn't exists")
|
return nil, status.Error(codes.Unauthenticated, "Session doesn't exists")
|
||||||
}
|
}
|
||||||
return nil, status.Error(codes.Internal, "Somethings is broken on our side")
|
return nil, status.Error(codes.Internal, "Somethings is broken on our side")
|
||||||
@@ -52,24 +52,24 @@ func (srv *AccountsServer) RefreshToken(ctx context.Context, in *empty.Empty) (*
|
|||||||
return nil, status.Error(codes.Unauthenticated, "Invalid session")
|
return nil, status.Error(codes.Unauthenticated, "Invalid session")
|
||||||
}
|
}
|
||||||
|
|
||||||
accessToken, _, err := srv.authorizationCtrl.GenerateToken(&controllers.JWTData{
|
accessToken, _, err := srv.authorizationCtrl.GenerateToken(&services.JWTData{
|
||||||
UserID: claims.UserID,
|
UserID: claims.UserID,
|
||||||
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 := srv.authorizationCtrl.GenerateToken(&controllers.JWTData{
|
refreshToken, tokenID, err := srv.authorizationCtrl.GenerateToken(&services.JWTData{
|
||||||
UserID: claims.UserID,
|
UserID: claims.UserID,
|
||||||
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")
|
||||||
}
|
}
|
||||||
newSession := &controllers.Session{UserID: session.UserID}
|
newSession := &services.Session{UserID: session.UserID}
|
||||||
|
|
||||||
if err := srv.authorizationCtrl.SaveSession(ctx, tokenID, newSession); err != nil {
|
if err := srv.authorizationCtrl.SaveSession(ctx, tokenID, newSession); err != nil {
|
||||||
return nil, status.Error(codes.Aborted, "Couldn't store session")
|
return nil, status.Error(codes.Aborted, "Couldn't store session")
|
||||||
38
internal/api/v1/projects.go
Normal file
38
internal/api/v1/projects.go
Normal 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")
|
||||||
|
}
|
||||||
@@ -3,7 +3,7 @@ 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"
|
"github.com/golang/protobuf/ptypes/empty"
|
||||||
"google.golang.org/grpc"
|
"google.golang.org/grpc"
|
||||||
@@ -14,8 +14,8 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
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,
|
||||||
@@ -25,8 +25,8 @@ 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) (*empty.Empty, error) {
|
func (a *PublicAccountService) SignIn(ctx context.Context, in *accounts.SignInRequest) (*empty.Empty, error) {
|
||||||
@@ -34,25 +34,25 @@ func (a *PublicAccountService) SignIn(ctx context.Context, in *accounts.SignInRe
|
|||||||
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")
|
||||||
@@ -69,34 +69,36 @@ func (a *PublicAccountService) SignIn(ctx context.Context, in *accounts.SignInRe
|
|||||||
|
|
||||||
// Create a new account in Softplayer
|
// Create a new account in Softplayer
|
||||||
func (a *PublicAccountService) SignUp(ctx context.Context, in *accounts.SignUpRequest) (*empty.Empty, 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")
|
||||||
@@ -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)
|
||||||
@@ -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")
|
||||||
@@ -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 {
|
||||||
|
|||||||
169
internal/repository/projects.go
Normal file
169
internal/repository/projects.go
Normal 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
|
||||||
|
}
|
||||||
246
internal/repository/projects_test.go
Normal file
246
internal/repository/projects_test.go
Normal 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)
|
||||||
|
}
|
||||||
22
internal/repository/transaction.go
Normal file
22
internal/repository/transaction.go
Normal 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()
|
||||||
|
}
|
||||||
95
internal/repository/transaction_test.go
Normal file
95
internal/repository/transaction_test.go
Normal 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)
|
||||||
|
}
|
||||||
@@ -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 {
|
||||||
@@ -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)
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package controllers
|
package services
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
@@ -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())
|
||||||
71
internal/services/projects.go
Normal file
71
internal/services/projects.go
Normal 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
|
||||||
|
}
|
||||||
38
internal/services/projects_test.go
Normal file
38
internal/services/projects_test.go
Normal 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)
|
||||||
|
}
|
||||||
@@ -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"
|
||||||
@@ -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)
|
||||||
}
|
}
|
||||||
3
migrations/20260518174322_users_personal_data.down.sql
Normal file
3
migrations/20260518174322_users_personal_data.down.sql
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
ALTER TABLE accounts
|
||||||
|
DROP COLUMN name,
|
||||||
|
DROP COLUMN surname;
|
||||||
14
migrations/20260518174322_users_personal_data.up.sql
Normal file
14
migrations/20260518174322_users_personal_data.up.sql
Normal 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;
|
||||||
1
migrations/20260518193048_projects_init.down.sql
Normal file
1
migrations/20260518193048_projects_init.down.sql
Normal file
@@ -0,0 +1 @@
|
|||||||
|
DROP TABLE projects;
|
||||||
16
migrations/20260518193048_projects_init.up.sql
Normal file
16
migrations/20260518193048_projects_init.up.sql
Normal 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
|
||||||
|
);
|
||||||
@@ -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;
|
||||||
35
migrations/20260518193057_project_membership_init.up.sql
Normal file
35
migrations/20260518193057_project_membership_init.up.sql
Normal 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);
|
||||||
Reference in New Issue
Block a user