Restructure the projec and start adding projects

Signed-off-by: Nikolai Rodionov <iam@allanger.xyz>
This commit is contained in:
2026-05-19 10:55:53 +02:00
parent f04f5b923f
commit 1c3df23bf9
19 changed files with 270 additions and 167 deletions

View File

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

View File

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

View File

@@ -0,0 +1,107 @@
package v1
import (
"context"
"gitea.badhouseplants.net/softplayer/softplayer-backend/internal/services"
accounts "gitea.badhouseplants.net/softplayer/softplayer-go-proto/pkg/accounts/v1"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)
func NewPublicAccountServer(
accountsCtrl *services.AccountController,
authorizationCtrl *services.AuthController,
) *PublicAccountService {
return &PublicAccountService{
accountsCtrl: accountsCtrl,
authorizationCtrl: authorizationCtrl,
}
}
type PublicAccountService struct {
accounts.UnimplementedPublicAccountsServiceServer
accountsCtrl *services.AccountController
authorizationCtrl *services.AuthController
}
func (a *PublicAccountService) SignIn(ctx context.Context, in *accounts.SignInRequest) (*accounts.SignInResponse, error) {
id, err := a.accountsCtrl.Login(ctx, in.GetEmail(), in.GetPassword())
if err != nil {
return nil, status.Error(codes.Aborted, "Couldn't create a user")
}
accessToken, _, err := a.authorizationCtrl.GenerateToken(&services.JWTData{
UserID: id,
TokenType: services.TokenTypeAccess,
TokenAud: services.TokenAudWeb,
})
if err != nil {
return nil, status.Error(codes.Aborted, "Couldn't generate an access token")
}
refreshToken, tokenID, err := a.authorizationCtrl.GenerateToken(&services.JWTData{
UserID: id,
TokenType: services.TokenTypeRefresh,
TokenAud: services.TokenAudWeb,
})
if err != nil {
return nil, status.Error(codes.Aborted, "Couldn't generate an access token")
}
session := &services.Session{UserID: id}
if err := a.authorizationCtrl.SaveSession(ctx, tokenID, session); err != nil {
return nil, status.Error(codes.Aborted, "Couldn't store session")
}
return &accounts.SignInResponse{
TokenPair: &accounts.TokenPair{
AccessToken: accessToken,
RefreshToken: refreshToken,
},
}, nil
}
// Create a new account in Softplayer
func (a *PublicAccountService) SignUp(ctx context.Context, in *accounts.SignUpRequest) (*accounts.SignUpResponse, error) {
data := &services.AccountData{
Password: in.GetPassword(),
Email: in.GetEmail(),
Name: in.PersonalData.GetName(),
Surname: in.PersonalData.GetSurname(),
}
id, err := a.accountsCtrl.Create(ctx, data)
if err != nil {
return nil, status.Error(codes.Aborted, "Couldn't create a user")
}
accessToken, _, err := a.authorizationCtrl.GenerateToken(&services.JWTData{
UserID: id,
TokenType: services.TokenTypeAccess,
TokenAud: services.TokenAudWeb,
})
if err != nil {
return nil, status.Error(codes.Aborted, "Couldn't generate an access token")
}
refreshToken, tokenID, err := a.authorizationCtrl.GenerateToken(&services.JWTData{
UserID: id,
TokenType: services.TokenTypeRefresh,
TokenAud: services.TokenAudWeb,
})
if err != nil {
return nil, status.Error(codes.Aborted, "Couldn't generate an access token")
}
session := &services.Session{UserID: id}
if err := a.authorizationCtrl.SaveSession(ctx, tokenID, session); err != nil {
return nil, status.Error(codes.Aborted, "Couldn't store session")
}
return &accounts.SignUpResponse{
TokenPair: &accounts.TokenPair{
AccessToken: accessToken,
RefreshToken: refreshToken,
},
}, nil
}

View File

@@ -0,0 +1,64 @@
package v1
import (
"context"
"errors"
"gitea.badhouseplants.net/softplayer/softplayer-backend/internal/services"
tokens "gitea.badhouseplants.net/softplayer/softplayer-go-proto/pkg/tokens/v1"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/metadata"
"google.golang.org/grpc/status"
"google.golang.org/protobuf/types/known/emptypb"
)
// var _ tokens.PublicTokensServiceServer = (*PublicTokensServer)(nil)
type PublicTokensServer struct {
tokens.UnimplementedPublicTokensServiceServer
tokenCtrl *services.TokenController
authorizationCtrl *services.AuthController
}
func NewPublicTokensServer(
tokenCtrl *services.TokenController,
authorizationCtrl *services.AuthController,
) *PublicTokensServer {
return &PublicTokensServer{
tokenCtrl: tokenCtrl,
authorizationCtrl: authorizationCtrl,
}
}
func (srv *PublicTokensServer) AuthenticateWithToken(ctx context.Context, in *tokens.AuthenticateWithTokenRequest) (*emptypb.Empty, error) {
tokenAuthRes, err := srv.tokenCtrl.AuthenticateWithToken(ctx, in.TokenValue.Token)
if err != nil {
if errors.Is(err, services.ErrBadToken) {
return nil, status.Error(codes.Unauthenticated, "Token is not valid")
}
if errors.Is(err, services.ErrServerError) {
return nil, status.Error(codes.Internal, "Something is broken on our side")
}
return nil, status.Error(codes.Aborted, "Couldn't authorize")
}
jwtData := &services.JWTData{
UserID: tokenAuthRes.UserID,
TokenType: services.TokenTypeAccess,
TokenAud: services.TokenAudToken,
Scope: tokenAuthRes.Scope,
}
accessToken, _, err := srv.authorizationCtrl.GenerateToken(jwtData)
if err != nil {
return nil, status.Error(codes.Aborted, "Couldn't generate an access token")
}
header := metadata.New(map[string]string{
"X-Access-Token": accessToken,
})
if err := grpc.SetHeader(ctx, header); err != nil {
return nil, status.Error(codes.Aborted, "Couldn't set metadata")
}
return &emptypb.Empty{}, nil
}

View File

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

51
internal/api/v1/test.go Normal file
View File

@@ -0,0 +1,51 @@
package v1
import (
"context"
test "gitea.badhouseplants.net/softplayer/softplayer-go-proto/pkg/test/v1"
"google.golang.org/grpc"
"google.golang.org/protobuf/types/known/emptypb"
)
func NewTestServer() *TestServer {
return &TestServer{}
}
type TestServer struct {
test.UnimplementedTestServiceServer
}
func (t *TestServer) Pong(ctx context.Context, in *test.PongRequest) (*test.PongResponse, error) {
return &test.PongResponse{}, nil
}
func (t *TestServer) PongStream(in *emptypb.Empty, stream grpc.ServerStreamingServer[test.PongStreamResponse]) error {
for i := 0; i < 10; i++ {
if err := stream.Send(&test.PongStreamResponse{Dummy: "test"}); err != nil {
return err
}
}
return nil
}
func NewPublicTestServer() *PublicTestServer {
return &PublicTestServer{}
}
type PublicTestServer struct {
test.UnimplementedPublicTestServiceServer
}
func (t *PublicTestServer) Ping(ctx context.Context, in *test.PingRequest) (*test.PingResponse, error) {
return &test.PingResponse{}, nil
}
func (t *PublicTestServer) PingStream(in *emptypb.Empty, stream grpc.ServerStreamingServer[test.PingStreamResponse]) error {
for i := 0; i < 10; i++ {
if err := stream.Send(&test.PingStreamResponse{Dummy: "test"}); err != nil {
return err
}
}
return nil
}

267
internal/api/v1/tokens.go Normal file
View File

@@ -0,0 +1,267 @@
package v1
import (
"context"
"errors"
"gitea.badhouseplants.net/softplayer/softplayer-backend/internal/services"
tokens "gitea.badhouseplants.net/softplayer/softplayer-go-proto/pkg/tokens/v1"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"google.golang.org/protobuf/types/known/emptypb"
"google.golang.org/protobuf/types/known/timestamppb"
)
// var _ tokens.TokensServiceServer = (*TokensServer)(nil)
// TokensServer implements the Token Service
type TokensServer struct {
tokens.UnimplementedTokensServiceServer
tokenCtrl *services.TokenController
authorizationCtrl *services.AuthController
}
func NewTokensServer(
tokenCtrl *services.TokenController,
authorizationCtrl *services.AuthController,
) *TokensServer {
return &TokensServer{
tokenCtrl: tokenCtrl,
authorizationCtrl: authorizationCtrl,
}
}
// CreateToken implements [v1.TokensServiceServer].
func (srv *TokensServer) CreateToken(ctx context.Context, in *tokens.CreateTokenRequest) (*tokens.CreateTokenResponse, error) {
claims, err := services.ClaimsFromContext(ctx)
if err != nil {
return nil, status.Error(codes.Aborted, "Context is invalid")
}
if claims.UserID == "" {
return nil, status.Error(codes.Aborted, "Context is invalid")
}
if in.TokenPermissions == nil {
return nil, status.Error(codes.InvalidArgument, "Permissions must be set")
}
permissions := map[string][]string{}
for service, methods := range in.TokenPermissions.Permissions {
permissions[service] = methods.GetMethods()
}
tokenData := &services.TokenData{
Name: in.TokenMetadata.GetName(),
UserID: claims.UserID,
ExpiresAt: in.TokenMetadata.ExpiresAt.AsTime(),
Scopes: permissions,
}
token, tokenID, err := srv.tokenCtrl.Create(ctx, tokenData)
if err != nil {
if errors.Is(err, services.ErrServerError) {
return nil, status.Error(codes.Internal, "Something is broken on our side")
}
return nil, status.Error(codes.Aborted, "Couldn't create a token")
}
return &tokens.CreateTokenResponse{
TokenUuid: &tokens.TokenUUID{
Uuid: tokenID,
},
TokenValue: &tokens.TokenValue{Token: token},
}, nil
}
// ForceTokenExpiration implements [v1.TokensServiceServer].
func (srv *TokensServer) ForceTokenExpiration(ctx context.Context, in *tokens.ForceTokenExpirationRequest) (*emptypb.Empty, error) {
claims, err := services.ClaimsFromContext(ctx)
if err != nil {
return nil, status.Error(codes.Aborted, "Context is invalid")
}
if claims.UserID == "" {
return nil, status.Error(codes.Aborted, "Context is invalid")
}
if err := srv.tokenCtrl.VerifyTokenOwner(ctx, claims.UserID, in.TokenUuid.Uuid); err != nil {
if errors.Is(err, services.ErrServerError) {
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")
}
if err := srv.tokenCtrl.ForceExpiration(ctx, in.TokenUuid.GetUuid()); err != nil {
if errors.Is(err, services.ErrServerError) {
return nil, status.Error(codes.Internal, "Something is broken on our side")
}
return nil, status.Error(codes.Aborted, "Couldn't create a token")
}
return &emptypb.Empty{}, nil
}
// GetToken implements [v1.TokensServiceServer].
func (srv *TokensServer) GetToken(ctx context.Context, in *tokens.GetTokenRequest) (*tokens.GetTokenResponse, error) {
claims, err := services.ClaimsFromContext(ctx)
if err != nil {
return nil, status.Error(codes.Aborted, "Context is invalid")
}
if claims.UserID == "" {
return nil, status.Error(codes.Aborted, "Context is invalid")
}
if err := srv.tokenCtrl.VerifyTokenOwner(ctx, claims.UserID, in.TokenUuid.Uuid); err != nil {
if errors.Is(err, services.ErrServerError) {
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")
}
token, err := srv.tokenCtrl.Get(ctx, in.TokenUuid.Uuid, claims.UserID)
if err != nil {
if errors.Is(err, services.ErrServerError) {
return nil, status.Error(codes.Internal, "Something is broken on our side")
}
return nil, status.Error(codes.Aborted, "Couldn't list tokens")
}
return &tokens.GetTokenResponse{
TokenUuid: &tokens.TokenUUID{
Uuid: token.UUID,
},
TokenMetadata: &tokens.TokenMetadata{
Name: token.Name,
ExpiresAt: timestamppb.New(token.ExpiresAt),
LastUsedAt: timestamppb.New(token.LastUsedAt),
GeneratedAt: timestamppb.New(token.GeneratedAt),
CreatedAt: timestamppb.New(token.CreatedAt),
RevokedAt: timestamppb.New(token.RevokedAt),
},
TokenPermissions: &tokens.TokenPermissions{},
}, nil
}
// ListTokens implements [v1.TokensServiceServer].
func (srv *TokensServer) ListTokens(in *emptypb.Empty, stream grpc.ServerStreamingServer[tokens.ListTokensResponse]) error {
claims, err := services.ClaimsFromContext(stream.Context())
if err != nil {
return status.Error(codes.Aborted, "Context is invalid")
}
if claims.UserID == "" {
return status.Error(codes.Aborted, "Context is invalid")
}
tokensRes, err := srv.tokenCtrl.List(stream.Context(), claims.UserID)
if err != nil {
if errors.Is(err, services.ErrServerError) {
return status.Error(codes.Internal, "Something is broken on our side")
}
return status.Error(codes.Aborted, "Couldn't list tokens")
}
for _, tokenRes := range tokensRes {
if err := stream.Send(&tokens.ListTokensResponse{
TokenUuid: &tokens.TokenUUID{
Uuid: tokenRes.UUID,
},
TokenMetadata: &tokens.TokenMetadata{
Name: tokenRes.Name,
ExpiresAt: timestamppb.New(tokenRes.ExpiresAt),
LastUsedAt: timestamppb.New(tokenRes.LastUsedAt),
GeneratedAt: timestamppb.New(tokenRes.GeneratedAt),
CreatedAt: timestamppb.New(tokenRes.CreatedAt),
RevokedAt: timestamppb.New(tokenRes.RevokedAt),
},
}); err != nil {
return status.Error(codes.Aborted, "Couldn't send data")
}
}
return nil
}
// RegenerateToken implements [v1.TokensServiceServer].
func (srv *TokensServer) RegenerateToken(ctx context.Context, in *tokens.RegenerateTokenRequest) (*tokens.RegenerateTokenResponse, error) {
claims, err := services.ClaimsFromContext(ctx)
if err != nil {
return nil, status.Error(codes.Aborted, "Context is invalid")
}
if claims.UserID == "" {
return nil, status.Error(codes.Aborted, "Context is invalid")
}
if err := srv.tokenCtrl.VerifyTokenOwner(ctx, claims.UserID, in.TokenUuid.Uuid); err != nil {
if errors.Is(err, services.ErrServerError) {
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")
}
tokenVal, err := srv.tokenCtrl.Regenerate(ctx, in.TokenUuid.GetUuid())
if err != nil {
if errors.Is(err, services.ErrServerError) {
return nil, status.Error(codes.Internal, "Something is broken on our side")
}
return nil, status.Error(codes.Aborted, "Couldn't list tokens")
}
return &tokens.RegenerateTokenResponse{
TokenValue: &tokens.TokenValue{
Token: tokenVal,
},
}, nil
}
// UpdateToken implements [v1.TokensServiceServer].
func (srv *TokensServer) UpdateToken(ctx context.Context, in *tokens.UpdateTokenRequest) (*tokens.UpdateTokenResponse, error) {
claims, err := services.ClaimsFromContext(ctx)
if err != nil {
return nil, status.Error(codes.Aborted, "Context is invalid")
}
if claims.UserID == "" {
return nil, status.Error(codes.Aborted, "Context is invalid")
}
if err := srv.tokenCtrl.VerifyTokenOwner(ctx, claims.UserID, in.TokenUuid.Uuid); err != nil {
if errors.Is(err, services.ErrServerError) {
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")
}
if in.TokenPermissions == nil {
return nil, status.Error(codes.InvalidArgument, "Permissions must be set")
}
permissions := map[string][]string{}
for service, methods := range in.TokenPermissions.Permissions {
permissions[service] = methods.GetMethods()
}
tokenData := &services.TokenData{
Name: in.TokenMetadata.Name,
Scopes: permissions,
}
if err := srv.tokenCtrl.Update(ctx, tokenData); err != nil {
if errors.Is(err, services.ErrServerError) {
return nil, status.Error(codes.Internal, "Something is broken on our side")
}
return nil, status.Error(codes.Aborted, "Couldn't list tokens")
}
return &tokens.UpdateTokenResponse{
TokenUuid: &tokens.TokenUUID{},
TokenMetadata: &tokens.TokenMetadata{},
TokenPermissions: &tokens.TokenPermissions{},
}, nil
}
// ListPermissions implements [v1.TokensServiceServer].
func (srv *TokensServer) ListPermissions(in *emptypb.Empty, stream grpc.ServerStreamingServer[tokens.ListPermissionsResponse]) error {
data := srv.tokenCtrl.ListPermissions(stream.Context())
for key, data := range data {
result := &tokens.ListPermissionsResponse{
Permissions: &tokens.TokenPermissions{
Permissions: map[string]*tokens.MethodList{
key: {Methods: data},
},
},
}
if err := stream.Send(result); err != nil {
return status.Error(codes.Aborted, "Couldn't send data to the client")
}
}
return nil
}

View File

@@ -11,9 +11,9 @@ type ProjectData struct {
Name string
Slug string
Description string
OwnerID string
CreatedAt string
ArchivedAt time.Time
CreatedBy string
CreatedAt time.Time
ArchivedAt sql.NullTime
Blocked bool
UpdatedAt time.Time
UpdatedBy string

View File

@@ -0,0 +1,26 @@
package repository_test
import (
"database/sql"
"testing"
"time"
"gitea.badhouseplants.net/softplayer/softplayer-backend/internal/repository"
"github.com/google/uuid"
)
func TestProjectsRepository_Success(t *testing.T) {
createdBy := uuid.NewString()
_ = &repository.ProjectData{
UUID: uuid.NewString(),
Name: "test-1",
Slug: "test_1",
Description: "Test Project Number 1",
CreatedBy: createdBy,
CreatedAt: time.Now(),
ArchivedAt: sql.NullTime{Time: time.Now()},
Blocked: false,
UpdatedAt: time.Now(),
UpdatedBy: createdBy,
}
}

View File

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

View File

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

View File

@@ -1,4 +1,4 @@
package controllers_test
package services_test
import (
"context"
@@ -8,8 +8,8 @@ import (
"testing"
"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/services"
"github.com/google/uuid"
_ "github.com/jackc/pgx/v5/stdlib"
"github.com/redis/go-redis/v9"
@@ -40,8 +40,8 @@ func newTestRedisConnection() *redis.Client {
})
}
func newTestAccountController(ctx context.Context) *controllers.AccountController {
return &controllers.AccountController{
func newTestAccountController(ctx context.Context) *services.AccountController {
return &services.AccountController{
DB: newTestDBConnection(ctx),
Redis: newTestRedisConnection(),
DevMode: true,
@@ -67,7 +67,7 @@ func newTestUniqueEmail(prefix string) string {
func TestIntegrationAccountCreate_Success(t *testing.T) {
ctrl := newTestAccountController(t.Context())
accountData := &controllers.AccountData{
accountData := &services.AccountData{
Password: "qwertyu9",
Email: newTestUniqueEmail("accounts"),
Surname: "Doe",
@@ -81,7 +81,7 @@ func TestIntegrationAccountCreate_Success(t *testing.T) {
func TestIntegrationAccountCreate_ExistingAccountErr(t *testing.T) {
ctrl := newTestAccountController(t.Context())
email := newTestUniqueEmail("accounts")
accountData := &controllers.AccountData{
accountData := &services.AccountData{
Password: "qwertyu9",
Email: email,
Surname: "Doe",
@@ -94,13 +94,13 @@ func TestIntegrationAccountCreate_ExistingAccountErr(t *testing.T) {
id, err = ctrl.Create(t.Context(), accountData)
assert.Empty(t, id)
assert.Error(t, err)
assert.ErrorIs(t, err, controllers.ErrEmailUsed)
assert.ErrorIs(t, err, services.ErrEmailUsed)
}
func TestIntegrationAccountLogin_Success(t *testing.T) {
ctrl := newTestAccountController(t.Context())
email := newTestUniqueEmail("accounts")
accountData := &controllers.AccountData{
accountData := &services.AccountData{
Password: "qwertyu9",
Email: email,
}
@@ -117,7 +117,7 @@ func TestIntegrationAccountLogin_Success(t *testing.T) {
func TestIntegrationAccountLogin_WrongPassword(t *testing.T) {
ctrl := newTestAccountController(t.Context())
email := newTestUniqueEmail("accounts")
accountData := &controllers.AccountData{
accountData := &services.AccountData{
Password: "qwertyu9",
Email: email,
}
@@ -129,13 +129,13 @@ func TestIntegrationAccountLogin_WrongPassword(t *testing.T) {
id, err = ctrl.Login(t.Context(), accountData.Email, "Wrong Password")
assert.Empty(t, id)
assert.Error(t, err)
assert.ErrorIs(t, err, controllers.ErrWrongPassword)
assert.ErrorIs(t, err, services.ErrWrongPassword)
}
func TestIntegrationAccountLogin_WrongEmail(t *testing.T) {
ctrl := newTestAccountController(t.Context())
email := newTestUniqueEmail("accounts")
accountData := &controllers.AccountData{
accountData := &services.AccountData{
Password: "qwertyu9",
Email: email,
}
@@ -147,5 +147,5 @@ func TestIntegrationAccountLogin_WrongEmail(t *testing.T) {
id, err = ctrl.Login(t.Context(), "some@email.com", "Wrong Password")
assert.Empty(t, id)
assert.Error(t, err)
assert.ErrorIs(t, err, controllers.ErrUserNotFound)
assert.ErrorIs(t, err, services.ErrUserNotFound)
}

View File

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

View File

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

View File

@@ -1,13 +1,21 @@
package controllers
package services
import "context"
import (
"context"
"gitea.badhouseplants.net/softplayer/softplayer-backend/internal/helpers/logger"
)
type ProjectsController struct{}
type ProjectData struct{}
// Create a new project
// It should create a project and set an owner to it
func (ctrl *ProjectsController) Create(ctx context.Context, data *ProjectData) (id string, err error) {
log := logger.FromContext(ctx)
log.V(2).Info("Creating a project")
return "", nil
}

View File

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

View File

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