Verification emails
This commit is contained in:
		@@ -2,12 +2,11 @@ package controllers
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"context"
 | 
			
		||||
	"errors"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"log"
 | 
			
		||||
	"time"
 | 
			
		||||
 | 
			
		||||
	"git.badhouseplants.net/softplayer/softplayer-backend/internal/helpers/hash"
 | 
			
		||||
	"git.badhouseplants.net/softplayer/softplayer-backend/internal/helpers/kube"
 | 
			
		||||
	"github.com/google/uuid"
 | 
			
		||||
	corev1 "k8s.io/api/core/v1"
 | 
			
		||||
	rbacv1 "k8s.io/api/rbac/v1"
 | 
			
		||||
@@ -15,7 +14,6 @@ import (
 | 
			
		||||
 | 
			
		||||
	"k8s.io/apimachinery/pkg/types"
 | 
			
		||||
	ctrl "sigs.k8s.io/controller-runtime"
 | 
			
		||||
	"sigs.k8s.io/controller-runtime/pkg/client"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type Account struct {
 | 
			
		||||
@@ -36,67 +34,43 @@ type AccountData struct {
 | 
			
		||||
	UUID     string
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func waitUntilCreated(ctx context.Context, client client.Client, obj client.Object, attemps int, timeout time.Duration) error {
 | 
			
		||||
	log.Printf("Waiting %d", attemps)
 | 
			
		||||
	if err := client.Get(ctx, types.NamespacedName{
 | 
			
		||||
		Namespace: obj.GetNamespace(),
 | 
			
		||||
		Name:      obj.GetName(),
 | 
			
		||||
	}, obj); err != nil {
 | 
			
		||||
		if attemps > 0 {
 | 
			
		||||
			time.Sleep(timeout)
 | 
			
		||||
			waitUntilCreated(ctx, client, obj, attemps-1, timeout)
 | 
			
		||||
		} else {
 | 
			
		||||
			return err
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (acc *Account) Create(ctx context.Context) error {
 | 
			
		||||
	client := acc.Controller.GetClient()
 | 
			
		||||
 | 
			
		||||
	acc.Data.UUID = uuid.New().String()
 | 
			
		||||
	log.Println(acc.Data.UUID)
 | 
			
		||||
 | 
			
		||||
	passwordHash, err := hash.HashPassword(acc.Data.Password, int(acc.Params.HashCost))
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	namespace := corev1.Namespace{
 | 
			
		||||
	namespace := &corev1.Namespace{
 | 
			
		||||
		ObjectMeta: metav1.ObjectMeta{
 | 
			
		||||
			Name: acc.Data.UUID,
 | 
			
		||||
			Labels: map[string]string{
 | 
			
		||||
				"username": acc.Data.Username,
 | 
			
		||||
				"email-verified": "false",
 | 
			
		||||
			},
 | 
			
		||||
		},
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if err := client.Create(ctx, &namespace); err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if err := waitUntilCreated(ctx, client, &namespace, 10, time.Millisecond*50); err != nil {
 | 
			
		||||
	
 | 
			
		||||
	if err := kube.Create(ctx,client, namespace, true); err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if err := client.Get(ctx, types.NamespacedName{
 | 
			
		||||
		Name: acc.Data.UUID,
 | 
			
		||||
	}, &namespace); err != nil {
 | 
			
		||||
		if err := client.Delete(ctx, &namespace); err != nil {
 | 
			
		||||
	}, namespace); err != nil {
 | 
			
		||||
		if err := client.Delete(ctx, namespace); err != nil {
 | 
			
		||||
			return err
 | 
			
		||||
		}
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Create a secret with the account data
 | 
			
		||||
	secret := corev1.Secret{
 | 
			
		||||
	secret := &corev1.Secret{
 | 
			
		||||
		ObjectMeta: metav1.ObjectMeta{
 | 
			
		||||
			Name:      acc.Data.Username,
 | 
			
		||||
			Namespace: "softplayer-accounts",
 | 
			
		||||
			OwnerReferences: []metav1.OwnerReference{
 | 
			
		||||
				metav1.OwnerReference{
 | 
			
		||||
					APIVersion: "v1",
 | 
			
		||||
					Kind:       "Namespace",
 | 
			
		||||
					Name:       acc.Data.UUID,
 | 
			
		||||
					UID:        namespace.UID,
 | 
			
		||||
				},
 | 
			
		||||
			},
 | 
			
		||||
		},
 | 
			
		||||
		StringData: map[string]string{
 | 
			
		||||
			"uuid":     acc.Data.UUID,
 | 
			
		||||
@@ -104,19 +78,22 @@ func (acc *Account) Create(ctx context.Context) error {
 | 
			
		||||
			"password": passwordHash,
 | 
			
		||||
		},
 | 
			
		||||
	}
 | 
			
		||||
	if err := client.Create(ctx, &secret); err != nil {
 | 
			
		||||
		if err := client.Delete(ctx, &namespace); err != nil {
 | 
			
		||||
 | 
			
		||||
	if err := client.Create(ctx, kube.SetOwnerRef(ctx, client, secret, namespace)); err != nil {
 | 
			
		||||
		if err := client.Delete(ctx, namespace); err != nil {
 | 
			
		||||
			return err
 | 
			
		||||
		}
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
	// Create a namespace to be managed by the account
 | 
			
		||||
	
 | 
			
		||||
	// Prepare RBAC resources for the account
 | 
			
		||||
	role := &rbacv1.Role{
 | 
			
		||||
		ObjectMeta: metav1.ObjectMeta{Name: acc.Data.Username, Namespace: acc.Data.UUID},
 | 
			
		||||
		Rules:      []rbacv1.PolicyRule{{Verbs: []string{"get", "watch", "list", "create", "patch", "delete"}, APIGroups: []string{""}, Resources: []string{"configmaps", "secrets"}}},
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if err := client.Create(ctx, role); err != nil {
 | 
			
		||||
		if err := client.Delete(ctx, &namespace); err != nil {
 | 
			
		||||
		if err := client.Delete(ctx, namespace); err != nil {
 | 
			
		||||
			return err
 | 
			
		||||
		}
 | 
			
		||||
		return err
 | 
			
		||||
@@ -128,6 +105,15 @@ func (acc *Account) Create(ctx context.Context) error {
 | 
			
		||||
			Namespace: acc.Data.UUID,
 | 
			
		||||
		},
 | 
			
		||||
	}
 | 
			
		||||
	
 | 
			
		||||
	if err := client.Create(ctx, sa); err != nil {
 | 
			
		||||
		if err := client.Delete(ctx, namespace); err != nil {
 | 
			
		||||
			return err
 | 
			
		||||
		}
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
	rb := &rbacv1.RoleBinding{
 | 
			
		||||
		ObjectMeta: metav1.ObjectMeta{
 | 
			
		||||
			Name:      acc.Data.UUID,
 | 
			
		||||
@@ -148,14 +134,7 @@ func (acc *Account) Create(ctx context.Context) error {
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if err := client.Create(ctx, rb); err != nil {
 | 
			
		||||
		if err := client.Delete(ctx, &namespace); err != nil {
 | 
			
		||||
			return err
 | 
			
		||||
		}
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if err := client.Create(ctx, sa); err != nil {
 | 
			
		||||
		if err := client.Delete(ctx, &namespace); err != nil {
 | 
			
		||||
		if err := client.Delete(ctx, namespace); err != nil {
 | 
			
		||||
			return err
 | 
			
		||||
		}
 | 
			
		||||
		return err
 | 
			
		||||
@@ -174,18 +153,18 @@ func (acc *Account) Create(ctx context.Context) error {
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if err := client.Create(ctx, saSec); err != nil {
 | 
			
		||||
		if err := client.Delete(ctx, &namespace); err != nil {
 | 
			
		||||
		if err := client.Delete(ctx, namespace); err != nil {
 | 
			
		||||
			return err
 | 
			
		||||
		}
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
	if err := waitUntilCreated(ctx, client, saSec, 10, time.Millisecond*50); err != nil {
 | 
			
		||||
	if err := kube.WaitUntilCreated(ctx, client, saSec, 10, time.Millisecond*50); err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	acc.Token, err = acc.getToken(ctx, saSec)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		if err := client.Delete(ctx, &namespace); err != nil {
 | 
			
		||||
		if err := client.Delete(ctx, namespace); err != nil {
 | 
			
		||||
			return err
 | 
			
		||||
		}
 | 
			
		||||
		return err
 | 
			
		||||
@@ -196,16 +175,18 @@ func (acc *Account) Create(ctx context.Context) error {
 | 
			
		||||
func (acc *Account) Login(ctx context.Context) error {
 | 
			
		||||
	client := acc.Controller.GetClient()
 | 
			
		||||
	sec := &corev1.Secret{}
 | 
			
		||||
 | 
			
		||||
	if err := client.Get(ctx, types.NamespacedName{
 | 
			
		||||
		Namespace: "softplayer-accounts",
 | 
			
		||||
		Name:      acc.Data.Username,
 | 
			
		||||
	}, sec); err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
	if !hash.CheckPasswordHash(acc.Data.Password, string(sec.Data["password"])) {
 | 
			
		||||
		err := errors.New("wrong password")
 | 
			
		||||
 | 
			
		||||
	if err := hash.CheckPasswordHash(acc.Data.Password, string(sec.Data["password"])); err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	acc.Data.UUID = string(sec.Data["uuid"])
 | 
			
		||||
	tokenName := fmt.Sprintf("sa-%s", acc.Data.UUID)
 | 
			
		||||
	saSec := &corev1.Secret{
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										116
									
								
								internal/controllers/email.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										116
									
								
								internal/controllers/email.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,116 @@
 | 
			
		||||
package controllers
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"context"
 | 
			
		||||
	"crypto/rand"
 | 
			
		||||
	"errors"
 | 
			
		||||
	"io"
 | 
			
		||||
	"log"
 | 
			
		||||
 | 
			
		||||
	"git.badhouseplants.net/softplayer/softplayer-backend/internal/helpers/email"
 | 
			
		||||
	"git.badhouseplants.net/softplayer/softplayer-backend/internal/helpers/kube"
 | 
			
		||||
	ctrl "sigs.k8s.io/controller-runtime"
 | 
			
		||||
 | 
			
		||||
	corev1 "k8s.io/api/core/v1"
 | 
			
		||||
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 | 
			
		||||
	"k8s.io/apimachinery/pkg/types"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type EmailSvc struct {
 | 
			
		||||
		Controller ctrl.Manager
 | 
			
		||||
		Data EmailData
 | 
			
		||||
		EmailConfig email.EmailConf
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type EmailData struct {
 | 
			
		||||
	UserID string
 | 
			
		||||
	Code string
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (svc *EmailSvc) SendVerification(ctx context.Context) error {
 | 
			
		||||
	client 	:= svc.Controller.GetClient()
 | 
			
		||||
	userns := &corev1.Namespace{}
 | 
			
		||||
	if err := client.Get(ctx, types.NamespacedName{
 | 
			
		||||
		Name:      svc.Data.UserID,
 | 
			
		||||
	}, userns); err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	userName, ok := userns.Labels["username"]
 | 
			
		||||
	if !ok {
 | 
			
		||||
		return errors.New("user not found")
 | 
			
		||||
	}
 | 
			
		||||
	accountData := &corev1.Secret{}
 | 
			
		||||
	if err := client.Get(ctx, types.NamespacedName{
 | 
			
		||||
		Namespace: "softplayer-accounts",
 | 
			
		||||
		Name:      userName,
 | 
			
		||||
	}, accountData); err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
	number := encodeToString(6)
 | 
			
		||||
	email := string(accountData.Data["email"])
 | 
			
		||||
	if err := svc.EmailConfig.SendEmail(email, number); err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	emailCode := corev1.ConfigMap{
 | 
			
		||||
		ObjectMeta: metav1.ObjectMeta{
 | 
			
		||||
			Name: "email-verification-code",
 | 
			
		||||
			Namespace:                  svc.Data.UserID,
 | 
			
		||||
		},
 | 
			
		||||
		Data:       map[string]string{
 | 
			
		||||
			"code": number,
 | 
			
		||||
		},
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if err := kube.Create(ctx, client, &emailCode, true); err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
	return nil	
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (svc *EmailSvc) ConfirmVerification(ctx context.Context) error {
 | 
			
		||||
	client := svc.Controller.GetClient()
 | 
			
		||||
	emailCode := &corev1.ConfigMap{}
 | 
			
		||||
	if err := client.Get(ctx, types.NamespacedName{
 | 
			
		||||
		Namespace: svc.Data.UserID,
 | 
			
		||||
		Name: "email-verification-code",
 | 
			
		||||
	}, emailCode); err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if svc.Data.Code != emailCode.Data["code"] {
 | 
			
		||||
		log.Println(svc.Data.Code)
 | 
			
		||||
		log.Println(emailCode.Data["code"])
 | 
			
		||||
		return errors.New("wrong verification code")
 | 
			
		||||
	}
 | 
			
		||||
	if err := client.Delete(ctx, emailCode); err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	userns := &corev1.Namespace{}
 | 
			
		||||
	if err := client.Get(ctx, types.NamespacedName{
 | 
			
		||||
		Name:      svc.Data.UserID,
 | 
			
		||||
	}, userns); err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
	userns.Labels["email-verified"] = "true"
 | 
			
		||||
	if err := client.Update(ctx, userns); err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func encodeToString(max int) string {
 | 
			
		||||
    b := make([]byte, max)
 | 
			
		||||
    n, err := io.ReadAtLeast(rand.Reader, b, max)
 | 
			
		||||
    if n != max {
 | 
			
		||||
        panic(err)
 | 
			
		||||
    }
 | 
			
		||||
    for i := 0; i < len(b); i++ {
 | 
			
		||||
        b[i] = table[int(b[i])%len(table)]
 | 
			
		||||
    }
 | 
			
		||||
    return string(b)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
var table = [...]byte{'1', '2', '3', '4', '5', '6', '7', '8', '9', '0'}
 | 
			
		||||
@@ -1,13 +0,0 @@
 | 
			
		||||
# package controllers
 | 
			
		||||
 | 
			
		||||
import "context"
 | 
			
		||||
 | 
			
		||||
type EmailSvc struct {}
 | 
			
		||||
 | 
			
		||||
type EmailData strict {
 | 
			
		||||
	UserID string
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (svc *EmailSvc) SendVerification(ctx context.Context) {
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
		Reference in New Issue
	
	Block a user