diff --git a/api/v1/accounts.go b/api/v1/accounts.go index ab87953..412763e 100644 --- a/api/v1/accounts.go +++ b/api/v1/accounts.go @@ -4,24 +4,34 @@ import ( "context" "git.badhouseplants.net/softplayer/softplayer-backend/internal/controllers" + "git.badhouseplants.net/softplayer/softplayer-backend/internal/helpers/email" "git.badhouseplants.net/softplayer/softplayer-go-proto/pkg/accounts" + "github.com/golang/protobuf/ptypes/empty" + "google.golang.org/grpc" + "google.golang.org/grpc/metadata" + "google.golang.org/protobuf/types/known/emptypb" ctrl "sigs.k8s.io/controller-runtime" ) -func NewAccountRPCImpl(contoller ctrl.Manager, hashCost int16) *AccountsServer { +func NewAccountRPCImpl(contoller ctrl.Manager, hashCost int16, email *email.EmailConf, devMode bool) *AccountsServer { return &AccountsServer{ Controller: contoller, Params: &controllers.AccountParams{ HashCost: hashCost, }, + emailConfig: *email, + devMode: devMode, } } type AccountsServer struct { accounts.UnimplementedAccountsServer - Controller ctrl.Manager - Params *controllers.AccountParams + Controller ctrl.Manager + Params *controllers.AccountParams + 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) { @@ -64,6 +74,24 @@ func (a *AccountsServer) SignIn(ctx context.Context, in *accounts.AccountWithPas }, nil } +func (a *AccountsServer) ResetPassword(ctx context.Context, in *accounts.AccountData) (*empty.Empty, error) { + data := populateData(in.GetName(), "", in.GetEmail()) + acc := populateAccount(data, a.Controller) + code, err := acc.ResetPassword(ctx, a.emailConfig) + if err != nil { + return nil, err + } + + if a.devMode { + header := metadata.Pairs("code", code) + if err := grpc.SendHeader(ctx, header); err != nil { + return nil, err + } + } + + return &emptypb.Empty{}, nil +} + func populateData(username, password, email string) *controllers.AccountData { return &controllers.AccountData{ Username: username, diff --git a/internal/controllers/accounts.go b/internal/controllers/accounts.go index 6812b2d..25ce3dd 100644 --- a/internal/controllers/accounts.go +++ b/internal/controllers/accounts.go @@ -2,15 +2,22 @@ package controllers import ( "context" + "errors" "fmt" "time" + "git.badhouseplants.net/softplayer/softplayer-backend/internal/consts" + "git.badhouseplants.net/softplayer/softplayer-backend/internal/helpers/email" "git.badhouseplants.net/softplayer/softplayer-backend/internal/helpers/hash" "git.badhouseplants.net/softplayer/softplayer-backend/internal/helpers/kube" "github.com/google/uuid" + "github.com/sirupsen/logrus" corev1 "k8s.io/api/core/v1" rbacv1 "k8s.io/api/rbac/v1" + k8serrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/kubernetes" + "k8s.io/client-go/rest" "k8s.io/apimachinery/pkg/types" ctrl "sigs.k8s.io/controller-runtime" @@ -21,6 +28,7 @@ type Account struct { Params AccountParams Data *AccountData Token string + DevMode bool } type AccountParams struct { @@ -207,6 +215,68 @@ func (acc *Account) Login(ctx context.Context) error { return nil } +func (acc *Account) ResetPassword(ctx context.Context, emailConfig email.EmailConf) (string, error) { + clientset, err := kubernetes.NewForConfig(acc.Controller.GetConfig()) + if err != nil { + return "", err + } + + userdata, err := clientset.CoreV1().Secrets("softplayer-accounts").Get(ctx, acc.Data.Username, metav1.GetOptions{}) + if err != nil { + return "", err + } + if string(userdata.Data["email"]) != acc.Data.Email { + return "", errors.New("user or email not found") + } + + acc.Data.UUID = string(userdata.Data["uuid"]) + + conf := &rest.Config{ + Host: "https://kubernetes.default.svc.cluster.local:443", + BearerToken: acc.Token, + TLSClientConfig: rest.TLSClientConfig{ + Insecure: true, + }, + } + + clientset, err = kubernetes.NewForConfig(conf) + if err != nil { + logrus.Error(err, "Couldn't create a new clientset") + return "", consts.ErrSystemError + } + + secretName := "password-reset-code" + number := encodeToString(6) + secret := corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: secretName, + }, + StringData: map[string]string{ + "code": number, + }, + } + sec, err := clientset.CoreV1().Secrets(acc.Data.UUID).Create(ctx, &secret, metav1.CreateOptions{}) + if !k8serrors.IsAlreadyExists(err) { + return "", err + } else if k8serrors.IsAlreadyExists(err) { + timestamp := sec.CreationTimestamp.Time + now := time.Now() + if timestamp.Add(time.Minute).Before(now) { + return "", errors.New("You can send an email once per minute, please wait") + } + } + + if !acc.DevMode { + emailContent := "Subject: Softplayer verification code\r\n" + "\r\n" + fmt.Sprintf("Your verification code is %s", number) + email := string(userdata.Data["email"]) + if err := emailConfig.SendEmail(email, emailContent); err != nil { + return "", err + } + } + + return number, nil +} + func (acc *Account) getToken(ctx context.Context, saSec *corev1.Secret) (string, error) { client := acc.Controller.GetClient() if err := client.Get(ctx, types.NamespacedName{ diff --git a/main.go b/main.go index a9482aa..0a2f286 100644 --- a/main.go +++ b/main.go @@ -98,7 +98,7 @@ func server(params Serve) error { } environments.RegisterEnvironmentsServer(grpcServer, v1.NewapiGrpcImpl(controller, log)) - accounts.RegisterAccountsServer(grpcServer, v1.NewAccountRPCImpl(controller, params.HashCost)) + accounts.RegisterAccountsServer(grpcServer, v1.NewAccountRPCImpl(controller, params.HashCost, &emailConfig, params.DevMode)) email_proto.RegisterEmailValidationServer(grpcServer, v1.InitEmailServer(controller, &emailConfig, params.DevMode)) applications_proto.RegisterApplicationsServer(grpcServer, v1.NewApplicationsGrpcImpl(controller, log))