Merge pull request 'implement-account-services' (#4) from implement-account-services into main

Reviewed-on: softplayer/softplayer-backend#4
This commit was merged in pull request #4.
This commit is contained in:
2026-04-30 17:24:43 +00:00
17 changed files with 534 additions and 596 deletions

View File

@@ -10,4 +10,5 @@ RUN CGO_ENABLED=0 GOOS=linux go build -o backend
FROM scratch
COPY --from=0 /app/backend /app
COPY --from=0 /etc/ssl /etc/ssl
COPY migrations /migrations
ENTRYPOINT ["/app"]

40
Taskfile.yml Normal file
View File

@@ -0,0 +1,40 @@
# yaml-language-server: $schema=https://taskfile.dev/schema.json
version: "3"
tasks:
build:
desc: Build go code
cmd: go build
silent: true
run-migrations-dev:
desc: Execute database migrations
env:
SOFTPLAYER_DB_CONNECTION_STRING: postgres://softplayer:qwertyu9@localhost:30432/softplayer?sslmode=disable
cmd: go run main.go migrate --migrations-path=file://migrations
run-server-dev:
desc: Run the local dev server
env:
SOFTPLAYER_DB_CONNECTION_STRING: postgres://softplayer:qwertyu9@localhost:30432/softplayer?sslmode=disable
SOFTPLAYER_REDIS_HOST: localhost:30379
cmd: go run main.go serve --dev-mode --reflection
deploy-local-env:
desc: Run a kind cluster and deploy deps
deps:
- kind-cluster
- helmfile-deploy
kind-cluster:
desc: Run a kind cluster
cmd: kind create cluster --config ./kind-config.yaml
kind-cluster-remove:
desc: Remove the kind cluster
cmd: kind delete cluster
helmfile-deploy:
desc: Deploy the helmfile for the local dev
cmd: helmfile apply

View File

@@ -1,68 +0,0 @@
package v1
import (
"context"
"database/sql"
"gitea.badhouseplants.net/softplayer/softplayer-backend/internal/controllers"
"gitea.badhouseplants.net/softplayer/softplayer-backend/internal/helpers/email"
"gitea.badhouseplants.net/softplayer/softplayer-go-proto/pkg/accounts/v1"
"github.com/golang/protobuf/ptypes/empty"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)
func NewAccountRPCImpl(db *sql.DB, hashCost int16, email *email.EmailConf, devMode bool) *AccountsServer {
return &AccountsServer{
Params: &controllers.AccountParams{
HashCost: hashCost,
},
SQLDriver: db,
emailConfig: *email,
devMode: devMode,
}
}
type AccountsServer struct {
accounts.UnimplementedAccountsServer
Params *controllers.AccountParams
SQLDriver *sql.DB
emailConfig email.EmailConf
// When dev mode is enabled, email won't be sent, instead the code will be returned in metadata
devMode bool
}
func (a *AccountsServer) SignUp(ctx context.Context, in *accounts.AccountWithPassword) (*accounts.AccountFullWithToken, error) {
accountCtrl := controllers.AccountController{
Params: controllers.AccountParams{
HashCost: a.Params.HashCost,
},
DB: a.SQLDriver,
DevMode: a.devMode,
}
if err := accountCtrl.Create(ctx, &controllers.AccountData{
Username: in.Data.GetName(),
Password: in.Password.GetPassword(),
Email: in.Data.GetEmail(),
}); err != nil {
return nil, status.Error(codes.Aborted, "Couldn't create a user")
}
return &accounts.AccountFullWithToken{
Id: &accounts.AccountId{},
Data: &accounts.AccountData{},
Token: "",
}, nil
}
func (a *AccountsServer) SignIn(ctx context.Context, in *accounts.AccountWithPassword) (*accounts.AccountFullWithToken, error) {
return nil, status.Error(codes.Unimplemented, "Endpoint is not implemented")
}
func (a *AccountsServer) ResetPassword(ctx context.Context, in *accounts.AccountData) (*empty.Empty, error) {
return nil, status.Error(codes.Unimplemented, "Endpoint is not implemented")
}
func (acc *AccountsServer) NewPassword(ctx context.Context, in *accounts.AccountWithPasswordAndCode) (*empty.Empty, error) {
return nil, status.Error(codes.Unimplemented, "Endpoint is not implemented")
}

59
api/v1/accounts_auth.go Normal file
View File

@@ -0,0 +1,59 @@
package v1
import (
"context"
"gitea.badhouseplants.net/softplayer/softplayer-backend/internal/controllers"
"gitea.badhouseplants.net/softplayer/softplayer-backend/internal/tools/logger"
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 NewAccountAuthRPCImpl(ctrl *controllers.AccountController) *AccountsAuthServer {
return &AccountsAuthServer{
ctrl: ctrl,
}
}
type AccountsAuthServer struct {
accounts.UnimplementedAccountsAuthServiceServer
ctrl *controllers.AccountController
}
func (a *AccountsAuthServer) RefreshToken(ctx context.Context, in *empty.Empty) (*empty.Empty, error) {
tokenID := ctx.Value("token_id").(string)
userID := ctx.Value("user_id").(string)
log := logger.FromContext(ctx)
uuid, err := a.ctrl.ValidateRefreshToken(ctx, tokenID, userID)
if err != nil {
return nil, status.Error(codes.Unauthenticated, "refresh token is invalid")
}
accessToken, err := a.ctrl.GenerateAccessToken(uuid)
if err != nil {
log.Error(err, "Couldn't generate an access token")
return nil, status.Error(codes.Aborted, "Couldn't generate Access Token")
}
refreshToken, err := a.ctrl.GenerateRefreshToken(ctx, uuid)
if err != nil {
log.Error(err, "Couldn't generate a refresh token")
return nil, status.Error(codes.Aborted, "Couldn't generate Access Token")
}
header := metadata.Pairs(
"access-token", accessToken,
"refreshToken", refreshToken,
)
if err := grpc.SetHeader(ctx, header); err != nil {
log.Error(err, "Couldn't set headers")
return nil, status.Error(codes.Unknown, "Couldn't set headers")
}
return &emptypb.Empty{}, nil
}

View File

@@ -0,0 +1,73 @@
package v1
import (
"context"
"gitea.badhouseplants.net/softplayer/softplayer-backend/internal/controllers"
accounts "gitea.badhouseplants.net/softplayer/softplayer-go-proto/pkg/accounts/v1"
"github.com/coreos/go-oidc/v3/oidc"
"github.com/golang/protobuf/ptypes/empty"
"golang.org/x/oauth2"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"google.golang.org/protobuf/types/known/emptypb"
)
func NewAccountNoAuthRPCImpl(ctrl *controllers.AccountController) *AccountsNoAuthServer {
return &AccountsNoAuthServer{
ctrl: ctrl,
}
}
type AccountsNoAuthServer struct {
accounts.UnimplementedAccountsNoAuthServiceServer
ctrl *controllers.AccountController
}
func (a *AccountsNoAuthServer) SignIn(ctx context.Context, in *accounts.SignInRequest) (*empty.Empty, error) {
provider, err := oidc.NewProvider(ctx, "https://authentik.badhouseplants.net")
if err != nil {
return nil, err
}
// Configure an OpenID Connect aware OAuth2 client.
oauth2Config := oauth2.Config{
ClientID: "softplayer-localhost",
ClientSecret: "pRpe3scGUE2jNH6t5rqI9R4OROeQHs4eO6ku957mYjDumKhQGX8QJcO0BMJ2FG4sUpvFrqccEqWgc3wKMp94tC8LyvTnkPF0Tg0CaldAEHuoQQdNKAzXVxwrHE6kNyBC",
RedirectURL: "http://localhost:8080/#/auth/callback",
// Discovery returns the OAuth2 endpoints.
Endpoint: provider.Endpoint(),
// "openid" is a required scope for OpenID Connect flows.
Scopes: []string{oidc.ScopeOpenID, "profile", "email"},
}
verifier := provider.Verifier(&oidc.Config{ClientID: "softplayer-localhost"})
oauth2Token, err := oauth2Config.Exchange(ctx, in.Code)
if err != nil {
return nil, err
}
// Extract the ID Token from OAuth2 token.
rawIDToken, ok := oauth2Token.Extra("id_token").(string)
if !ok {
return nil, status.Error(codes.Unauthenticated, "Couldn't parse oauth token")
}
// Parse and verify ID Token payload.
idToken, err := verifier.Verify(ctx, rawIDToken)
if err != nil {
return nil, status.Error(codes.Unauthenticated, "Couldn't verify oauth token")
}
// Extract custom claims
var claims struct {
Email string `json:"email"`
Verified bool `json:"email_verified"`
}
if err := idToken.Claims(&claims); err != nil {
// handle error
}
return &emptypb.Empty{}, nil
}

View File

@@ -1,84 +0,0 @@
package v1
import (
"context"
"gitea.badhouseplants.net/softplayer/softplayer-backend/internal/controllers"
proto "gitea.badhouseplants.net/softplayer/softplayer-go-proto/pkg/applications/v1"
"github.com/go-logr/logr"
"github.com/golang/protobuf/ptypes/empty"
"google.golang.org/protobuf/types/known/emptypb"
ctrl "sigs.k8s.io/controller-runtime"
)
func NewApplicationsGrpcImpl(controller ctrl.Manager, log logr.Logger) *ApplicationServer {
return &ApplicationServer{
controller: controller,
logInstance: log,
}
}
type ApplicationServer struct {
proto.UnimplementedApplicationsServer
controller ctrl.Manager
logInstance logr.Logger
}
// Create an environment
func (app *ApplicationServer) Create(ctx context.Context, in *proto.CreateOptions) (*proto.ApplicationFull, error) {
log := app.logInstance
log.WithValues("user_id", in.GetOwnerId().GetUuid(), "environment_id", in.GetSpec().GetEnvironemntId(), "app_name", in.GetSpec().GetApplication())
ctx = logr.NewContext(ctx, log)
data := &controllers.ApplicationData{
Name: in.Metadata.Name,
Application: in.Spec.Application,
Version: in.Spec.Version,
Environemnt: in.Spec.EnvironemntId,
Config: in.Spec.Config,
RawConfig: "",
}
application := &controllers.Application{
UserID: in.GetOwnerId().GetUuid(),
Controller: app.controller,
Data: data,
Token: in.GetToken().GetToken(),
}
err := application.Create(ctx)
if err != nil {
return nil, err
}
return &proto.ApplicationFull{
Metadata: in.GetMetadata(),
Id: &proto.ApplicationId{
Uuid: application.Data.UUID,
},
Spec: in.GetSpec(),
}, nil
}
func (app *ApplicationServer) Delete(ctx context.Context, in *proto.DeleteOptions) (*empty.Empty, error) {
log := app.logInstance
log.WithValues("user_id", in.GetOwnerId().GetUuid(), "app_id", in.GetId().GetUuid())
ctx = logr.NewContext(ctx, log)
data := &controllers.ApplicationData{
Name: in.Metadata.Name,
UUID: in.GetId().GetUuid(),
}
application := &controllers.Application{
UserID: in.GetOwnerId().GetUuid(),
Controller: app.controller,
Data: data,
Token: in.GetToken().GetToken(),
}
err := application.Delete(ctx)
if err != nil {
return nil, err
}
return &emptypb.Empty{}, nil
}

View File

@@ -1,86 +0,0 @@
package v1
import (
"context"
"fmt"
"gitea.badhouseplants.net/softplayer/softplayer-backend/internal/controllers"
"gitea.badhouseplants.net/softplayer/softplayer-backend/internal/helpers/email"
proto_email "gitea.badhouseplants.net/softplayer/softplayer-go-proto/pkg/email/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"
ctrl "sigs.k8s.io/controller-runtime"
)
type EmailServer struct {
proto_email.UnimplementedEmailValidationServer
emailConfig email.EmailConf
controller ctrl.Manager
// When dev mode is enabled, email won't be sent, instead the code will be returned in metadata
devMode bool
}
func InitEmailServer(controller ctrl.Manager, emailConfig *email.EmailConf, devMode bool) *EmailServer {
return &EmailServer{
controller: controller,
emailConfig: *emailConfig,
devMode: devMode,
}
}
// Send the validation code to email
func (c *EmailServer) SendRequest(ctx context.Context, in *proto_email.RequestValidation) (*emptypb.Empty, error) {
// Validation
if len(in.GetUserId()) == 0 {
return nil, status.Error(codes.InvalidArgument, "user id must not be empty")
}
// Body
emailSvc := controllers.EmailSvc{
Data: controllers.EmailData{
UserID: in.GetUserId(),
},
EmailConfig: c.emailConfig,
Controller: c.controller,
DevMode: c.devMode,
}
err := emailSvc.SendVerification(ctx)
if err != nil {
return nil, err
}
if c.devMode {
header := metadata.Pairs("code", emailSvc.Data.Code)
if err := grpc.SendHeader(ctx, header); err != nil {
return nil, err
}
}
return &emptypb.Empty{}, nil
}
func (c *EmailServer) ValidateEmail(ctx context.Context, in *proto_email.ConfirmValidation) (*emptypb.Empty, error) {
// Validation
if len(in.GetUserId()) == 0 {
return nil, status.Error(codes.InvalidArgument, "user id must not be empty")
}
if in.GetCode() == 0 {
return nil, status.Error(codes.InvalidArgument, "code must not be empty")
}
// Body
emailSvc := controllers.EmailSvc{
Data: controllers.EmailData{
UserID: in.GetUserId(),
Code: fmt.Sprintf("%d", in.GetCode()),
},
EmailConfig: c.emailConfig,
Controller: c.controller,
}
err := emailSvc.ConfirmVerification(ctx)
if err != nil {
return nil, err
}
return &emptypb.Empty{}, nil
}

View File

@@ -1,261 +0,0 @@
package v1
import (
"context"
"gitea.badhouseplants.net/softplayer/softplayer-backend/internal/controllers"
"gitea.badhouseplants.net/softplayer/softplayer-backend/internal/providers/infra"
"gitea.badhouseplants.net/softplayer/softplayer-backend/internal/providers/kubernetes"
proto "gitea.badhouseplants.net/softplayer/softplayer-go-proto/pkg/environments/v1"
"github.com/go-logr/logr"
"github.com/golang/protobuf/ptypes/empty"
ctrl "sigs.k8s.io/controller-runtime"
)
func NewapiGrpcImpl(controller ctrl.Manager, log logr.Logger) *EnvironmentsServer {
return &EnvironmentsServer{
controller: controller,
logInstance: log,
}
}
type EnvironmentsServer struct {
proto.UnimplementedEnvironmentsServer
controller ctrl.Manager
logInstance logr.Logger
}
// Create an environment
func (e *EnvironmentsServer) Create(ctx context.Context, in *proto.CreateOptions) (*proto.EnvironmentFull, error) {
log := e.logInstance
log.WithValues("user_id", in.GetOwnerId().GetUuid())
ctx = logr.NewContext(ctx, log)
provider, err := infra.GetProvider(in.GetSpec().GetProvider().String())
if err != nil {
return nil, err
}
k8s, err := kubernetes.GetKubernetes(in.GetSpec().GetKubernetes().String())
if err != nil {
return nil, err
}
location, err := provider.GetServerLocation(in.GetSpec().GetServerLocation().String())
if err != nil {
return nil, err
}
serverType, err := provider.GetServerType(in.Spec.ServerType.String())
if err != nil {
return nil, err
}
data := &controllers.EnvironemntData{
Name: in.GetMetadata().GetName(),
Description: in.GetMetadata().GetDescription(),
Provider: provider.GetProviderName(),
Kubernetes: k8s.GetKubernetesName(),
Location: location,
ServerType: serverType,
DiskSize: int(in.GetSpec().GetDiskSize()),
}
environment := &controllers.Environemnt{
UserID: in.GetOwnerId().GetUuid(),
Config: ctrl.GetConfigOrDie(),
Controller: e.controller,
Data: data,
Token: in.GetToken().GetToken(),
}
err = environment.Create(ctx)
if err != nil {
return nil, err
}
return &proto.EnvironmentFull{
Metadata: in.GetMetadata(),
Id: &proto.EnvironmentId{
Uuid: environment.Data.UUID,
},
Spec: in.GetSpec(),
}, nil
}
func (e *EnvironmentsServer) Update(ctx context.Context, in *proto.UpdateOptions) (*proto.EnvironmentFull, error) {
log := e.logInstance
log.WithValues("user_id", in.GetOwnerId().GetUuid())
ctx = logr.NewContext(ctx, log)
provider, err := infra.GetProvider(in.GetSpec().GetProvider().String())
if err != nil {
return nil, err
}
k8s, err := kubernetes.GetKubernetes(in.GetSpec().GetKubernetes().String())
if err != nil {
return nil, err
}
location, err := provider.GetServerLocation(in.GetSpec().GetServerLocation().String())
if err != nil {
return nil, err
}
serverType, err := provider.GetServerType(in.Spec.ServerType.String())
if err != nil {
return nil, err
}
data := &controllers.EnvironemntData{
Name: in.GetMetadata().GetName(),
UUID: in.GetId().GetUuid(),
Description: in.GetMetadata().GetDescription(),
Provider: provider.GetProviderName(),
Kubernetes: k8s.GetKubernetesName(),
Location: location,
ServerType: serverType,
DiskSize: int(in.GetSpec().GetDiskSize()),
}
environment := &controllers.Environemnt{
Config: ctrl.GetConfigOrDie(),
UserID: in.GetOwnerId().GetUuid(),
Controller: e.controller,
Data: data,
Token: in.GetToken().GetToken(),
}
err = environment.Update(ctx)
if err != nil {
return nil, err
}
return &proto.EnvironmentFull{
Metadata: in.GetMetadata(),
Id: in.GetId(),
Spec: in.GetSpec(),
}, nil
}
func (e *EnvironmentsServer) Delete(ctx context.Context, in *proto.DeleteOptions) (*empty.Empty, error) {
log := e.logInstance
log.WithValues("user_id", in.GetOwnerId().GetUuid())
ctx = logr.NewContext(ctx, log)
data := &controllers.EnvironemntData{
Name: in.GetMetadata().GetName(),
UUID: in.GetId().GetUuid(),
}
environment := &controllers.Environemnt{
Config: ctrl.GetConfigOrDie(),
UserID: in.GetOwnerId().GetUuid(),
Controller: e.controller,
Data: data,
Token: in.GetToken().GetToken(),
}
err := environment.Delete(ctx)
if err != nil {
return nil, err
}
return nil, nil
}
func (e *EnvironmentsServer) Get(ctx context.Context, in *proto.GetOptions) (*proto.EnvironmentFull, error) {
log := e.logInstance
log.WithValues("user_id", in.GetOwnerId().GetUuid())
ctx = logr.NewContext(ctx, log)
data := &controllers.EnvironemntData{
UUID: in.GetId().GetUuid(),
}
environment := &controllers.Environemnt{
Config: ctrl.GetConfigOrDie(),
UserID: in.GetOwnerId().GetUuid(),
Controller: e.controller,
Data: data,
Token: in.GetToken().GetToken(),
}
if err := environment.Get(ctx); err != nil {
return nil, err
}
provider, err := infra.GetProvider(environment.Data.Provider)
if err != nil {
return nil, err
}
k8s, err := kubernetes.GetKubernetes(environment.Data.Kubernetes)
if err != nil {
return nil, err
}
return &proto.EnvironmentFull{
Spec: &proto.EnvironmentSpec{
Provider: proto.Provider(proto.Provider_value[provider.RawProviderName()]),
Kubernetes: proto.Kubernetes(proto.Kubernetes_value[k8s.RawKubernetesName()]),
ServerLocation: proto.Location(proto.Location_value[provider.RawServerLocation(environment.Data.Location)]),
ServerType: proto.ServerType(proto.ServerType_value[provider.RawServerType(environment.Data.ServerType)]),
DiskSize: int32(environment.Data.DiskSize),
},
Id: in.GetId(),
Metadata: &proto.EnvironmentMetadata{
Name: environment.Data.Name,
Description: environment.Data.Description,
},
}, nil
}
func (e *EnvironmentsServer) List(in *proto.ListOptions, stream proto.Environments_ListServer) error {
logInstance := e.logInstance
log := logInstance.WithValues("user_id", in.GetOwnerId().GetUuid())
ctx := logr.NewContext(stream.Context(), log)
environment := &controllers.Environemnt{
Config: ctrl.GetConfigOrDie(),
UserID: in.GetOwnerId().GetUuid(),
Controller: e.controller,
Token: in.GetToken().GetToken(),
}
envs, err := environment.List(ctx, in.GetSearchString())
if err != nil {
return err
}
for _, env := range envs {
provider, err := infra.GetProvider(env.Data.Provider)
if err != nil {
return err
}
k8s, err := kubernetes.GetKubernetes(env.Data.Kubernetes)
if err != nil {
return err
}
if err := stream.Send(&proto.EnvironmentFull{
Metadata: &proto.EnvironmentMetadata{
Name: env.Data.Name,
Description: env.Data.Description,
},
Id: &proto.EnvironmentId{
Uuid: env.Data.UUID,
},
Spec: &proto.EnvironmentSpec{
Provider: proto.Provider(proto.Provider_value[provider.RawProviderName()]),
Kubernetes: proto.Kubernetes(proto.Kubernetes_value[k8s.RawKubernetesName()]),
ServerLocation: proto.Location(proto.Location_value[provider.RawServerLocation(env.Data.Location)]),
ServerType: proto.ServerType(proto.ServerType_value[provider.RawServerType(env.Data.ServerType)]),
DiskSize: int32(env.Data.DiskSize),
},
}); err != nil {
return err
}
}
return nil
}

22
api/v1/test_auth.go Normal file
View File

@@ -0,0 +1,22 @@
package v1
import (
"context"
"gitea.badhouseplants.net/softplayer/softplayer-backend/internal/tools/logger"
test "gitea.badhouseplants.net/softplayer/softplayer-go-proto/pkg/test/v1"
)
func NewTestAuthRPCImpl() *TestAuthServer {
return &TestAuthServer{}
}
type TestAuthServer struct {
test.UnimplementedTestAuthServiceServer
}
func (t *TestAuthServer) Pong(ctx context.Context, in *test.PongRequest) (*test.PongResponse, error) {
log := logger.FromContext(ctx)
log.Info("Pong")
return &test.PongResponse{}, nil
}

22
api/v1/test_no_auth.go Normal file
View File

@@ -0,0 +1,22 @@
package v1
import (
"context"
"gitea.badhouseplants.net/softplayer/softplayer-backend/internal/tools/logger"
test "gitea.badhouseplants.net/softplayer/softplayer-go-proto/pkg/test/v1"
)
func NewTestNoAuthRPCImpl() *TestNoAuthServer {
return &TestNoAuthServer{}
}
type TestNoAuthServer struct {
test.UnimplementedTestNoAuthServiceServer
}
func (t *TestNoAuthServer) Ping(ctx context.Context, in *test.PingRequest) (*test.PingResponse, error) {
log := logger.FromContext(ctx)
log.Info("Ping")
return &test.PingResponse{}, nil
}

11
go.mod
View File

@@ -5,17 +5,22 @@ go 1.25.9
require (
github.com/alecthomas/assert/v2 v2.11.0
github.com/alecthomas/kong v1.15.0
github.com/coreos/go-oidc/v3 v3.18.0
github.com/go-logr/logr v1.4.3
github.com/go-logr/zapr v1.3.0
github.com/golang-jwt/jwt/v5 v5.2.2
github.com/golang-migrate/migrate/v4 v4.19.1
github.com/google/uuid v1.6.0
github.com/grpc-ecosystem/go-grpc-middleware v1.3.0
github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.0
github.com/joho/godotenv v1.5.1
github.com/lib/pq v1.10.9
github.com/mattn/go-colorable v0.1.13
github.com/redis/go-redis/v9 v9.18.0
github.com/sirupsen/logrus v1.9.3
go.uber.org/zap v1.27.0
golang.org/x/crypto v0.47.0
golang.org/x/oauth2 v0.36.0
gopkg.in/yaml.v2 v2.4.0
helm.sh/helm/v3 v3.20.2
k8s.io/api v0.35.1
@@ -45,6 +50,7 @@ require (
github.com/containerd/platforms v0.2.1 // indirect
github.com/cyphar/filepath-securejoin v0.6.1 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
github.com/emicklei/go-restful/v3 v3.12.2 // indirect
github.com/evanphx/json-patch v5.9.11+incompatible // indirect
github.com/evanphx/json-patch/v5 v5.9.11 // indirect
@@ -54,6 +60,7 @@ require (
github.com/fxamacker/cbor/v2 v2.9.0 // indirect
github.com/go-errors/errors v1.4.2 // indirect
github.com/go-gorp/gorp/v3 v3.1.0 // indirect
github.com/go-jose/go-jose/v4 v4.1.4 // indirect
github.com/go-openapi/jsonpointer v0.21.0 // indirect
github.com/go-openapi/jsonreference v0.21.0 // indirect
github.com/go-openapi/swag v0.23.0 // indirect
@@ -104,10 +111,10 @@ require (
github.com/spf13/pflag v1.0.10 // indirect
github.com/x448/float16 v0.8.4 // indirect
github.com/xlab/treeprint v1.2.0 // indirect
go.uber.org/atomic v1.11.0 // indirect
go.uber.org/multierr v1.11.0 // indirect
go.yaml.in/yaml/v2 v2.4.3 // indirect
go.yaml.in/yaml/v3 v3.0.4 // indirect
golang.org/x/oauth2 v0.34.0 // indirect
golang.org/x/sync v0.19.0 // indirect
golang.org/x/term v0.39.0 // indirect
golang.org/x/time v0.12.0 // indirect
@@ -133,7 +140,7 @@ require (
)
require (
gitea.badhouseplants.net/softplayer/softplayer-go-proto v0.0.0-20260423180238-53a8a976b540
gitea.badhouseplants.net/softplayer/softplayer-go-proto v0.0.0-20260430152421-88c087f0cea0
github.com/golang/protobuf v1.5.4
golang.org/x/net v0.49.0 // indirect
golang.org/x/sys v0.40.0 // indirect

33
go.sum
View File

@@ -1,10 +1,13 @@
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
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/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10=
dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s=
dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk=
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
gitea.badhouseplants.net/softplayer/softplayer-go-proto v0.0.0-20260423180238-53a8a976b540 h1:DX/lMTetxHz4ezEyBI4bKaJwxhO0uXUyGv9pQTV9NYI=
gitea.badhouseplants.net/softplayer/softplayer-go-proto v0.0.0-20260423180238-53a8a976b540/go.mod h1:zgX1KfGcHue8TjuXWN0onGpg3pQPek/lKMfdT6S7TQM=
gitea.badhouseplants.net/softplayer/softplayer-go-proto v0.0.0-20260430152421-88c087f0cea0 h1:2UggBAWgOJ1MYgkk+RTaWhfTGtzAZ0B9MriZMoHqnq4=
gitea.badhouseplants.net/softplayer/softplayer-go-proto v0.0.0-20260430152421-88c087f0cea0/go.mod h1:AgOh1lkPHyRgBf3/s1btKcAqke/33LbKYarTD13qeAg=
github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24 h1:bvDV9vkmnHYOMsOr4WLk+Vo07yKIzd94sVoIqshQ4bU=
github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8=
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg=
@@ -40,6 +43,10 @@ github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM
github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ=
github.com/bshuster-repo/logrus-logstash-hook v1.0.0 h1:e+C0SB5R1pu//O4MQ3f9cFuPGoOVeF2fE4Og9otCc70=
github.com/bshuster-repo/logrus-logstash-hook v1.0.0/go.mod h1:zsTqEiSzDgAa/8GZR7E1qaXrhYNDKBYy5/dWPTIflbk=
github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c=
github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0=
github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
@@ -59,6 +66,8 @@ github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I=
github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo=
github.com/containerd/platforms v0.2.1 h1:zvwtM3rz2YHPQsF2CHYM8+KtB5dvhISiXh5ZpSBQv6A=
github.com/containerd/platforms v0.2.1/go.mod h1:XHCb+2/hzowdiut9rkudds9bE5yJ7npe7dG/wG+uFPw=
github.com/coreos/go-oidc/v3 v3.18.0 h1:V9orjXynvu5wiC9SemFTWnG4F45v403aIcjWo0d41+A=
github.com/coreos/go-oidc/v3 v3.18.0/go.mod h1:DYCf24+ncYi+XkIH97GY1+dqoRlbaSI26KVTCI9SrY4=
github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs=
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
@@ -120,6 +129,8 @@ github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxI
github.com/go-errors/errors v1.4.2/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og=
github.com/go-gorp/gorp/v3 v3.1.0 h1:ItKF/Vbuj31dmV4jxA1qblpSwkl9g1typ24xoe70IGs=
github.com/go-gorp/gorp/v3 v3.1.0/go.mod h1:dLEjIyyRNiXvNZ8PSmzpt1GsWAUK8kjVhEpjH8TixEw=
github.com/go-jose/go-jose/v4 v4.1.4 h1:moDMcTHmvE6Groj34emNPLs/qtYXRVcd6S7NHbHz3kA=
github.com/go-jose/go-jose/v4 v4.1.4/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08=
github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
@@ -143,6 +154,8 @@ github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y=
github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8=
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8=
github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
github.com/golang-migrate/migrate/v4 v4.19.1 h1:OCyb44lFuQfYXYLx1SCxPZQGU7mcaZ7gH9yH4jSFbBA=
github.com/golang-migrate/migrate/v4 v4.19.1/go.mod h1:CTcgfjxhaUtsLipnLoQRWCrjYXycRz/g5+RWDuYgPrE=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
@@ -176,6 +189,8 @@ github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79 h1:+ngKgrYPPJr
github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA=
github.com/grpc-ecosystem/go-grpc-middleware v1.3.0 h1:+9834+KizmvFV7pXQGSXQTsaWhq2GjuNUt0aUU0YBYw=
github.com/grpc-ecosystem/go-grpc-middleware v1.3.0/go.mod h1:z0ButlSOZa5vEBq9m2m2hlwIgKw+rp3sdCBRoJY+30Y=
github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.0 h1:FbSCl+KggFl+Ocym490i/EyXF4lPgLoUtcSWquBM0Rs=
github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.0/go.mod h1:qOchhhIlmRcqk/O9uCo/puJlyo07YINaIqdZfZG3Jkc=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 h1:5ZPtiqj0JL5oKWmcsq4VMaAW5ukBEgSGXEN89zeH1Jo=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3/go.mod h1:ndYquD05frm2vACXE1nsccT4oJzjhw2arTS2cpUD1PI=
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
@@ -205,6 +220,8 @@ github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
github.com/klauspost/cpuid/v2 v2.0.9 h1:lgaqFMSdTdQYdZ04uHyN2d/eKdOMyi2YLSvlQIBFYa4=
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
@@ -294,8 +311,8 @@ github.com/redis/go-redis/extra/rediscmd/v9 v9.0.5 h1:EaDatTxkdHG+U3Bk4EUr+DZ7fO
github.com/redis/go-redis/extra/rediscmd/v9 v9.0.5/go.mod h1:fyalQWdtzDBECAQFBJuQe5bzQ02jGd5Qcbgb97Flm7U=
github.com/redis/go-redis/extra/redisotel/v9 v9.0.5 h1:EfpWLLCyXw8PSM2/XNJLjI3Pb27yVE+gIAfeqp8LUCc=
github.com/redis/go-redis/extra/redisotel/v9 v9.0.5/go.mod h1:WZjPDy7VNzn77AAfnAfVjZNvfJTYfPetfZk5yoSTLaQ=
github.com/redis/go-redis/v9 v9.7.3 h1:YpPyAayJV+XErNsatSElgRZZVCwXX9QzkKYNvO7x0wM=
github.com/redis/go-redis/v9 v9.7.3/go.mod h1:bGUrSggJ9X9GUmZpZNEOQKaANxSGgOEBRltRTZHSvrA=
github.com/redis/go-redis/v9 v9.18.0 h1:pMkxYPkEbMPwRdenAzUNyFNrDgHx9U+DrBabWNfSRQs=
github.com/redis/go-redis/v9 v9.18.0/go.mod h1:k3ufPphLU5YXwNTUcCRXGxUoF1fqxnhFQmscfkCoDA0=
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
github.com/rubenv/sql-migrate v1.8.1 h1:EPNwCvjAowHI3TnZ+4fQu3a915OpnQoPAjTXCGOy2U0=
@@ -335,6 +352,8 @@ github.com/xlab/treeprint v1.2.0 h1:HzHnuAF1plUN2zGlAFHbSQP2qJ0ZAD3XF5XD7OesXRQ=
github.com/xlab/treeprint v1.2.0/go.mod h1:gj5Gd3gPdKtR1ikdDK6fnFLdmIS0X30kTTuNd/WEJu0=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/zeebo/xxh3 v1.0.2 h1:xZmwmqxHZA8AI603jOQ0tMqmBr9lPeFwGg6d+xy9DC0=
github.com/zeebo/xxh3 v1.0.2/go.mod h1:5NWz9Sef7zIDm2JHfFlcQvNekmcEl9ekUZQQKCYaDcA=
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
go.opentelemetry.io/contrib/bridges/prometheus v0.57.0 h1:UW0+QyeyBVhn+COBec3nGhfnFe5lwB0ic1JBVjzhk0w=
@@ -382,6 +401,8 @@ go.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4Etq
go.opentelemetry.io/proto/otlp v1.5.0 h1:xJvq7gMzB31/d406fB8U5CBdyQGw4P399D1aQWU/3i4=
go.opentelemetry.io/proto/otlp v1.5.0/go.mod h1:keN8WnHxOy8PG0rQZjJJ5A2ebUoafqWp0eVQ4yIXvJ4=
go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
@@ -418,8 +439,8 @@ golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwY
golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o=
golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.34.0 h1:hqK/t4AKgbqWkdkcAeI8XLmbK+4m4G5YeQRrmiotGlw=
golang.org/x/oauth2 v0.34.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs=
golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=

View File

@@ -1,5 +1,6 @@
environments:
default:
kubeContext: kind-kind
values:
- databases:
postgres:
@@ -19,7 +20,7 @@ repositories:
releases:
- name: postgres-instance
namespace: databases
namespace: softplayer
chart: cloudpirates/postgres
version: 0.19.1
installed: true
@@ -35,7 +36,7 @@ releases:
enabled: true
whenDeleted: Delete
- name: dragonfly
namespace: databases
namespace: softplayer
chart: dragonfly/dragonfly
version: v1.38.0
installed: true
@@ -55,20 +56,18 @@ releases:
readOnlyRootFilesystem: true
runAsNonRoot: true
runAsUser: 1000
redis:
# -- Enable redis cluster with docker container.
enable: true
image:
repository: bitnamilegacy/redis
# -- Cluster domain.
clusterDomain: 'cluster.local'
auth:
# -- Enable password authentication.
enabled: true
# -- Redis password.
password: dragonfly
master:
service:
ports:
# -- Redis master service port.
redis: 6379
service:
port: 30379
type: NodePort
strategicMergePatches:
- apiVersion: v1
kind: Service
metadata:
name: dragonfly
namespace: softplayer
spec:
ports:
- name: dragonfly
port: 30379
protocol: TCP
nodePort: 30379

View File

@@ -3,21 +3,34 @@ package controllers
import (
"context"
"database/sql"
"errors"
"fmt"
"time"
"gitea.badhouseplants.net/softplayer/softplayer-backend/internal/helpers/hash"
"gitea.badhouseplants.net/softplayer/softplayer-backend/internal/tools/logger"
"github.com/golang-jwt/jwt/v5"
"github.com/google/uuid"
"github.com/redis/go-redis/v9"
)
type AccountController struct {
Params AccountParams
DB *sql.DB
DevMode bool
DB *sql.DB
Redis *redis.Client
DevMode bool
HashCost int16
AccessTokenTTL time.Duration
RefreshTokenTTL time.Duration
JWTSecret []byte
}
type AccountParams struct {
HashCost int16
type JWT struct {
RefreshToken string
AccessToken string
}
type AccountParams struct{}
type AccountData struct {
Username string
Password string
@@ -25,16 +38,67 @@ type AccountData struct {
UUID string
}
func (c *AccountController) Create(ctx context.Context, data *AccountData) error {
func (c *AccountController) Create(ctx context.Context, data *AccountData) (string, error) {
data.UUID = uuid.New().String()
passwordHash, err := hash.HashPassword(data.Password, int(c.Params.HashCost))
passwordHash, err := hash.HashPassword(data.Password, int(c.HashCost))
if err != nil {
return nil
return "", nil
}
query := "INSERT INTO users (uuid, username, email, password_hash) VALUES ($1, $2, $3, $4)"
if _, err := c.DB.Query(query, data.UUID, data.Username, data.Email, passwordHash); err != nil {
return err
return "", err
}
return nil
return data.UUID, nil
}
func (c *AccountController) GenerateAccessToken(userID string) (string, error) {
claims := jwt.MapClaims{
"user_id": userID,
"type": "access",
"exp": time.Now().Add(c.AccessTokenTTL).Unix(),
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
return token.SignedString(c.JWTSecret)
}
func redisKey(id string) string {
return fmt.Sprintf("refresh:%s", id)
}
func (c *AccountController) GenerateRefreshToken(ctx context.Context, userID string) (string, error) {
tokenID := uuid.New().String()
claims := jwt.MapClaims{
"user_id": userID,
"token_id": tokenID,
"type": "refresh",
"exp": time.Now().Add(c.RefreshTokenTTL).Unix(),
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
if err := c.Redis.Set(ctx, redisKey(tokenID), userID, c.RefreshTokenTTL).Err(); err != nil {
return "", err
}
return token.SignedString(c.JWTSecret)
}
// It must validate the refresh token
// Get it's id from the content
// Find a corresponding token in redis, and if it's found, remove it and create a new one
func (c *AccountController) ValidateRefreshToken(ctx context.Context, tokenID, userID string) (string, error) {
log := logger.FromContext(ctx)
userIDRedis := c.Redis.Get(ctx, redisKey(tokenID)).Val()
if err := c.Redis.Del(ctx, redisKey(tokenID)).Err(); err != nil {
log.Error(err, "Couldn't delete redis entry")
return "", err
}
log.Info(userIDRedis)
log.Info(userID)
if userID != userIDRedis {
return "", errors.New("user id doesn't match")
}
return userIDRedis, nil
}

View File

@@ -0,0 +1,64 @@
package interceptors
import (
"context"
"fmt"
"strings"
"gitea.badhouseplants.net/softplayer/softplayer-backend/internal/tools/logger"
"github.com/golang-jwt/jwt/v5"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/metadata"
"google.golang.org/grpc/status"
)
type JWTVerifier struct {
secret []byte
serverCtx context.Context
}
func NewJWTVerifier(ctx context.Context, secret []byte) *JWTVerifier {
return &JWTVerifier{
serverCtx: ctx,
secret: secret,
}
}
// This is an interceptors that should verify that a user is authorized
func (v *JWTVerifier) JWTAuthInterceptor(
ctx context.Context,
req interface{},
info *grpc.UnaryServerInfo,
handler grpc.UnaryHandler,
) (interface{}, error) {
log := logger.FromContext(v.serverCtx).WithValues("method", info.FullMethod)
if !strings.Contains(info.FullMethod, "NoAuth") {
log.Info("Checking the JWT token")
md, ok := metadata.FromIncomingContext(ctx)
if !ok {
return nil, status.Error(codes.Unauthenticated, "User is not authorized")
}
tokenString := md.Get("token")[0]
token, err := jwt.Parse(tokenString, func(token *jwt.Token) (any, error) {
// hmacSampleSecret is a []byte containing your secret, e.g. []byte("my_secret_key")
return v.secret, nil
}, jwt.WithValidMethods([]string{jwt.SigningMethodHS256.Alg()}))
if err != nil {
return nil, status.Error(codes.Unauthenticated, "User is not authorized")
}
if claims, ok := token.Claims.(jwt.MapClaims); ok {
fmt.Println(claims["userID"])
} else {
fmt.Println(err)
}
// Get the token from the metadata
// Validate the token
// Get the user id from the token
} else {
log.Info("Auth is not required for this request")
}
return handler(ctx, req)
}

View File

@@ -5,5 +5,5 @@ nodes:
extraPortMappings:
- containerPort: 30432
hostPort: 30432
- containerPort: 30431
hostPort: 30431
- containerPort: 30379
hostPort: 30379

181
main.go
View File

@@ -6,42 +6,62 @@ import (
"errors"
"fmt"
"net"
"os"
"strings"
"time"
v1 "gitea.badhouseplants.net/softplayer/softplayer-backend/api/v1"
"gitea.badhouseplants.net/softplayer/softplayer-backend/internal/helpers/email"
"gitea.badhouseplants.net/softplayer/softplayer-backend/internal/controllers"
"gitea.badhouseplants.net/softplayer/softplayer-backend/internal/tools/logger"
"gitea.badhouseplants.net/softplayer/softplayer-go-proto/pkg/accounts/v1"
applications_proto "gitea.badhouseplants.net/softplayer/softplayer-go-proto/pkg/applications/v1"
email_proto "gitea.badhouseplants.net/softplayer/softplayer-go-proto/pkg/email/v1"
"gitea.badhouseplants.net/softplayer/softplayer-go-proto/pkg/environments/v1"
accounts "gitea.badhouseplants.net/softplayer/softplayer-go-proto/pkg/accounts/v1"
test "gitea.badhouseplants.net/softplayer/softplayer-go-proto/pkg/test/v1"
"github.com/alecthomas/kong"
"github.com/golang-jwt/jwt/v5"
"github.com/golang-migrate/migrate/v4"
"github.com/golang-migrate/migrate/v4/database/postgres"
_ "github.com/golang-migrate/migrate/v4/source/file"
grpc_zap "github.com/grpc-ecosystem/go-grpc-middleware/logging/zap"
"github.com/grpc-ecosystem/go-grpc-middleware/v2/interceptors"
"github.com/grpc-ecosystem/go-grpc-middleware/v2/interceptors/auth"
"github.com/grpc-ecosystem/go-grpc-middleware/v2/interceptors/selector"
_ "github.com/lib/pq"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/reflection"
ctrl "sigs.k8s.io/controller-runtime"
"google.golang.org/grpc/status"
"github.com/redis/go-redis/v9"
)
type Serve struct {
Port int64 `short:"p" env:"SOFTPLAYER_PORT" default:"4020"`
Host string `env:"SOFTPLAYER_HOST" default:"0.0.0.0"`
HashCost int16 `env:"SOFTPLAYER_HASH_COST" default:"1"`
Reflection bool `env:"SOFTPLAYER_REFLECTION" default:"false"`
DevMode bool `env:"SOFTPLAYER_DEV_MODE" default:"false"`
SmtpHost string `env:"SOFTPLAYER_SMTP_HOST"`
SmtpPort string `env:"SOFTPLAYER_SMTP_PORT" default:"587"`
SmtpFrom string `env:"SOFTPLAYER_SMTP_FROM" default:"overlord@badhouseplants.net"`
SmtpPassword string `env:"SOFTPLAYER_SMTP_PASSWORD"`
DownloadDir string `env:"SOFTPLAYER_DOWNLOAD_DIR" default:"/tmp/softplayer"`
DBConnectionString string `env:"SOFTPLAYER_DB_CONNECTION_STRING"`
var CLI struct {
Serve Serve `cmd:"" help:"Start the grpc server"`
Migrate Migrate `cmd:"" help:"Run the database migrations"`
}
var CLI struct {
Serve Serve `cmd:"" help:"Start the grpc server"`
type Serve struct {
// Service related
Port int64 `short:"p" env:"SOFTPLAYER_PORT" default:"4020"`
Host string `env:"SOFTPLAYER_HOST" default:"0.0.0.0"`
HashCost int16 `env:"SOFTPLAYER_HASH_COST" default:"1"`
// SMTP Config
SMTPHost string `env:"SOFTPLAYER_SMTP_HOST"`
SMTPPort string `env:"SOFTPLAYER_SMTP_PORT" default:"587"`
SMTPFrom string `env:"SOFTPLAYER_SMTP_FROM" default:"overlord@badhouseplants.net"`
SMTPPassword string `env:"SOFTPLAYER_SMTP_PASSWORD"`
// Database and redis
DBConnectionString string `env:"SOFTPLAYER_DB_CONNECTION_STRING"`
RedisHost string `env:"SOFTPLAYER_REDIS_HOST"`
// JWT parameters
RefrestTokenTTL time.Duration `default:"8h"`
AccessTokenTTL time.Duration `default:"15m"`
JWTSecret string `default:"qwertyu9"`
// Dev and logging
Reflection bool `env:"SOFTPLAYER_REFLECTION" default:"false"`
DevMode bool `env:"SOFTPLAYER_DEV_MODE" default:"false"`
}
type Migrate struct {
DBConnectionString string `env:"SOFTPLAYER_DB_CONNECTION_STRING"`
MigrationsPath string `env:"SOFTPLAYER_DB_MIGRATIOON_PATH" default:"file://migrations"`
}
func main() {
@@ -52,15 +72,25 @@ func main() {
if err := server(ctx, CLI.Serve); err != nil {
panic(err)
}
case "migrate":
if err := migrateDB(ctx, CLI.Migrate); err != nil {
panic(err)
}
default:
panic(kongCtx.Command())
}
}
func migrateDB(ctx context.Context, db *sql.DB) error {
// Migrate the database to the latest version
func migrateDB(ctx context.Context, params Migrate) error {
log := logger.FromContext(ctx)
log.Info("Starting a database migration driver")
db, err := sql.Open("postgres", params.DBConnectionString)
if err != nil {
log.Error(err, "Couldn't start a database driver")
return err
}
driver, err := postgres.WithInstance(db, &postgres.Config{})
if err != nil {
log.Error(err, "Couldn't start a database migration driver")
@@ -69,7 +99,7 @@ func migrateDB(ctx context.Context, db *sql.DB) error {
log.Info("Preparing database migrations")
m, err := migrate.NewWithDatabaseInstance(
"file://migrations",
params.MigrationsPath,
"postgres", driver)
if err != nil {
log.Error(err, "Couldn't perform database migrations")
@@ -77,7 +107,7 @@ func migrateDB(ctx context.Context, db *sql.DB) error {
}
log.Info("Starting database migrations")
err = m.Up() // or m.Steps(2) if you want to explicitly set the number of migrations to r
err = m.Up()
if err != nil {
if errors.Is(err, migrate.ErrNoChange) {
log.Info("Database is already up-to-date")
@@ -90,67 +120,102 @@ func migrateDB(ctx context.Context, db *sql.DB) error {
return nil
}
// Run the grpc backend server
func server(ctx context.Context, params Serve) error {
// Make sure the download dir exists
log := logger.FromContext(ctx)
if err := os.MkdirAll(params.DownloadDir, 0o777); err != nil {
log.Error(err, "Coulnd't create a download dir")
return err
}
// Init a logger
log.Info("Starting a kubernetes manager")
controller, err := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{})
if err != nil {
log.Error(err, "Coulnd't start a kube manager")
return err
}
//log.Info("Starting a kubernetes manager")
//controller, err := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{})
//if err != nil {
// log.Error(err, "Coulnd't start a kube manager")
// return err
//}
// TODO: Handle the error
go func() {
if err := controller.Start(ctx); err != nil {
panic(err)
}
}()
//go func() {
// if err := controller.Start(ctx); err != nil {
// panic(err)
// }
//}()
log.Info("Opening a database connection")
db, err := sql.Open("postgres", "postgres://softplayer:qwertyu9@localhost:30432/softplayer?sslmode=disable")
db, err := sql.Open("postgres", params.DBConnectionString)
if err != nil {
log.Error(err, "Couldn't start a database driver")
return err
}
if err := migrateDB(ctx, db); err != nil {
log.Error(err, "Error occured while executing migrations")
return err
}
emailConfig := email.EmailConf{
From: params.SmtpFrom,
Password: params.SmtpPassword,
SmtpHost: params.SmtpHost,
SmtpPort: params.SmtpPort,
}
//emailConfig := email.EmailConf{
// From: params.SmtpFrom,
// Password: params.SmtpPassword,
// SmtpHost: params.SmtpHost,
// SmtpPort: params.SmtpPort,
//}
address := fmt.Sprintf("%s:%d", params.Host, params.Port)
lis, err := net.Listen("tcp", address)
if err != nil {
return err
}
// jwtVerifier := interceptors.NewJWTVerifier(ctx, []byte(params.JWTSecret))
authFn := func(ctx context.Context) (context.Context, error) {
tokenString, err := auth.AuthFromMD(ctx, "bearer")
if err != nil {
return nil, err
}
token, err := jwt.Parse(tokenString, func(token *jwt.Token) (any, error) {
return []byte(params.JWTSecret), nil
}, jwt.WithValidMethods([]string{jwt.SigningMethodHS256.Alg()}))
if err != nil {
return nil, status.Error(codes.Unauthenticated, "User is not authorized")
}
if claims, ok := token.Claims.(jwt.MapClaims); ok {
ctx = context.WithValue(ctx, "token_id", claims["token_id"].(string))
ctx = context.WithValue(ctx, "user_id", claims["user_id"].(string))
} else {
return ctx, errors.New("claims are missing int the token")
}
return ctx, nil
}
authReqServices := func(ctx context.Context, callMeta interceptors.CallMeta) bool {
return !strings.Contains(callMeta.Service, "NoAuth")
}
grpcServer := grpc.NewServer(
grpc.UnaryInterceptor(grpc_zap.UnaryServerInterceptor(logger.SetupLogger("info"))),
grpc.ChainUnaryInterceptor(
grpc_zap.UnaryServerInterceptor(logger.SetupLogger("info")),
// jwtVerifier.JWTAuthInterceptor,
selector.UnaryServerInterceptor(auth.UnaryServerInterceptor(authFn), selector.MatchFunc(authReqServices)),
),
grpc.StreamInterceptor(grpc_zap.StreamServerInterceptor(logger.SetupLogger("info"))),
)
rdb := redis.NewClient(&redis.Options{
Addr: params.RedisHost,
})
if params.Reflection {
reflection.Register(grpcServer)
}
environments.RegisterEnvironmentsServer(grpcServer, v1.NewapiGrpcImpl(controller, log))
accounts.RegisterAccountsServer(grpcServer, v1.NewAccountRPCImpl(db, params.HashCost, &emailConfig, params.DevMode))
email_proto.RegisterEmailValidationServer(grpcServer, v1.InitEmailServer(controller, &emailConfig, params.DevMode))
applications_proto.RegisterApplicationsServer(grpcServer, v1.NewApplicationsGrpcImpl(controller, log))
accountCtrl := &controllers.AccountController{
HashCost: params.HashCost,
DB: db,
DevMode: params.DevMode,
RefreshTokenTTL: params.RefrestTokenTTL,
AccessTokenTTL: params.AccessTokenTTL,
JWTSecret: []byte(params.JWTSecret),
Redis: rdb,
}
accounts.RegisterAccountsNoAuthServiceServer(grpcServer, v1.NewAccountNoAuthRPCImpl(accountCtrl))
accounts.RegisterAccountsAuthServiceServer(grpcServer, v1.NewAccountAuthRPCImpl(accountCtrl))
test.RegisterTestAuthServiceServer(grpcServer, v1.NewTestAuthRPCImpl())
test.RegisterTestNoAuthServiceServer(grpcServer, v1.NewTestNoAuthRPCImpl())
if err := grpcServer.Serve(lis); err != nil {
return err
}