mirror of
https://github.com/zitadel/zitadel.git
synced 2025-08-11 21:37:32 +00:00
multiple tries
This commit is contained in:
105
backend/v3/domain/command.go
Normal file
105
backend/v3/domain/command.go
Normal file
@@ -0,0 +1,105 @@
|
||||
package domain
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/zitadel/zitadel/backend/v3/storage/database"
|
||||
)
|
||||
|
||||
type Commander interface {
|
||||
Execute(ctx context.Context, opts *CommandOpts) (err error)
|
||||
}
|
||||
|
||||
type Invoker interface {
|
||||
Invoke(ctx context.Context, command Commander, opts *CommandOpts) error
|
||||
}
|
||||
|
||||
type CommandOpts struct {
|
||||
DB database.QueryExecutor
|
||||
Invoker Invoker
|
||||
}
|
||||
|
||||
type ensureTxOpts struct {
|
||||
*database.TransactionOptions
|
||||
}
|
||||
|
||||
type EnsureTransactionOpt func(*ensureTxOpts)
|
||||
|
||||
// EnsureTx ensures that the DB is a transaction. If it is not, it will start a new transaction.
|
||||
// The returned close function will end the transaction. If the DB is already a transaction, the close function
|
||||
// will do nothing because another [Commander] is already responsible for ending the transaction.
|
||||
func (o *CommandOpts) EnsureTx(ctx context.Context, opts ...EnsureTransactionOpt) (close func(context.Context, error) error, err error) {
|
||||
beginner, ok := o.DB.(database.Beginner)
|
||||
if !ok {
|
||||
// db is already a transaction
|
||||
return func(_ context.Context, err error) error {
|
||||
return err
|
||||
}, nil
|
||||
}
|
||||
|
||||
txOpts := &ensureTxOpts{
|
||||
TransactionOptions: new(database.TransactionOptions),
|
||||
}
|
||||
for _, opt := range opts {
|
||||
opt(txOpts)
|
||||
}
|
||||
|
||||
tx, err := beginner.Begin(ctx, txOpts.TransactionOptions)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
o.DB = tx
|
||||
|
||||
return func(ctx context.Context, err error) error {
|
||||
return tx.End(ctx, err)
|
||||
}, nil
|
||||
}
|
||||
|
||||
// EnsureClient ensures that the o.DB is a client. If it is not, it will get a new client from the [database.Pool].
|
||||
// The returned close function will release the client. If the o.DB is already a client or transaction, the close function
|
||||
// will do nothing because another [Commander] is already responsible for releasing the client.
|
||||
func (o *CommandOpts) EnsureClient(ctx context.Context) (close func(_ context.Context) error, err error) {
|
||||
pool, ok := o.DB.(database.Pool)
|
||||
if !ok {
|
||||
// o.DB is already a client
|
||||
return func(_ context.Context) error {
|
||||
return nil
|
||||
}, nil
|
||||
}
|
||||
client, err := pool.Acquire(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
o.DB = client
|
||||
return func(ctx context.Context) error {
|
||||
return client.Release(ctx)
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (o *CommandOpts) Invoke(ctx context.Context, command Commander) error {
|
||||
if o.Invoker == nil {
|
||||
return command.Execute(ctx, o)
|
||||
}
|
||||
return o.Invoker.Invoke(ctx, command, o)
|
||||
}
|
||||
|
||||
func DefaultOpts(invoker Invoker) *CommandOpts {
|
||||
if invoker == nil {
|
||||
invoker = &noopInvoker{}
|
||||
}
|
||||
return &CommandOpts{
|
||||
DB: pool,
|
||||
Invoker: invoker,
|
||||
}
|
||||
}
|
||||
|
||||
type noopInvoker struct {
|
||||
next Invoker
|
||||
}
|
||||
|
||||
func (i *noopInvoker) Invoke(ctx context.Context, command Commander, opts *CommandOpts) error {
|
||||
if i.next != nil {
|
||||
return i.next.Invoke(ctx, command, opts)
|
||||
}
|
||||
return command.Execute(ctx, opts)
|
||||
}
|
76
backend/v3/domain/create_user.go
Normal file
76
backend/v3/domain/create_user.go
Normal file
@@ -0,0 +1,76 @@
|
||||
package domain
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
v4 "github.com/zitadel/zitadel/backend/v3/storage/database/repository/stmt/v4"
|
||||
"github.com/zitadel/zitadel/backend/v3/storage/eventstore"
|
||||
)
|
||||
|
||||
type CreateUserCommand struct {
|
||||
user *User
|
||||
email *SetEmailCommand
|
||||
}
|
||||
|
||||
var (
|
||||
_ Commander = (*CreateUserCommand)(nil)
|
||||
_ eventer = (*CreateUserCommand)(nil)
|
||||
)
|
||||
|
||||
func NewCreateHumanCommand(username string, opts ...CreateHumanOpt) *CreateUserCommand {
|
||||
cmd := &CreateUserCommand{
|
||||
user: &User{
|
||||
User: v4.User{
|
||||
Username: username,
|
||||
Traits: &v4.Human{},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, opt := range opts {
|
||||
opt.applyOnCreateHuman(cmd)
|
||||
}
|
||||
return cmd
|
||||
}
|
||||
|
||||
// Events implements [eventer].
|
||||
func (c *CreateUserCommand) Events() []*eventstore.Event {
|
||||
panic("unimplemented")
|
||||
}
|
||||
|
||||
// Execute implements [Commander].
|
||||
func (c *CreateUserCommand) Execute(ctx context.Context, opts *CommandOpts) error {
|
||||
if err := c.ensureUserID(); err != nil {
|
||||
return err
|
||||
}
|
||||
c.email.UserID = c.user.ID
|
||||
if err := opts.Invoke(ctx, c.email); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type CreateHumanOpt interface {
|
||||
applyOnCreateHuman(*CreateUserCommand)
|
||||
}
|
||||
|
||||
type createHumanIDOpt string
|
||||
|
||||
// applyOnCreateHuman implements [CreateHumanOpt].
|
||||
func (c createHumanIDOpt) applyOnCreateHuman(cmd *CreateUserCommand) {
|
||||
cmd.user.ID = string(c)
|
||||
}
|
||||
|
||||
var _ CreateHumanOpt = (*createHumanIDOpt)(nil)
|
||||
|
||||
func CreateHumanWithID(id string) CreateHumanOpt {
|
||||
return createHumanIDOpt(id)
|
||||
}
|
||||
|
||||
func (c *CreateUserCommand) ensureUserID() (err error) {
|
||||
if c.user.ID != "" {
|
||||
return nil
|
||||
}
|
||||
c.user.ID, err = generateID()
|
||||
return err
|
||||
}
|
26
backend/v3/domain/crypto.go
Normal file
26
backend/v3/domain/crypto.go
Normal file
@@ -0,0 +1,26 @@
|
||||
package domain
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/crypto"
|
||||
)
|
||||
|
||||
type generateCodeCommand struct {
|
||||
code string
|
||||
value *crypto.CryptoValue
|
||||
}
|
||||
|
||||
type CryptoRepository interface {
|
||||
GetEncryptionConfig(ctx context.Context) (*crypto.GeneratorConfig, error)
|
||||
}
|
||||
|
||||
func (cmd *generateCodeCommand) Execute(ctx context.Context, opts *CommandOpts) error {
|
||||
config, err := cryptoRepo(opts.DB).GetEncryptionConfig(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
generator := crypto.NewEncryptionGenerator(*config, userCodeAlgorithm)
|
||||
cmd.value, cmd.code, err = crypto.NewCode(generator)
|
||||
return err
|
||||
}
|
52
backend/v3/domain/domain.go
Normal file
52
backend/v3/domain/domain.go
Normal file
@@ -0,0 +1,52 @@
|
||||
package domain
|
||||
|
||||
import (
|
||||
"math/rand/v2"
|
||||
"strconv"
|
||||
|
||||
"github.com/zitadel/zitadel/backend/v3/storage/cache"
|
||||
"github.com/zitadel/zitadel/backend/v3/storage/database"
|
||||
"github.com/zitadel/zitadel/backend/v3/telemetry/tracing"
|
||||
"github.com/zitadel/zitadel/internal/crypto"
|
||||
)
|
||||
|
||||
var (
|
||||
pool database.Pool
|
||||
userCodeAlgorithm crypto.EncryptionAlgorithm
|
||||
tracer tracing.Tracer
|
||||
|
||||
// userRepo func(database.QueryExecutor) UserRepository
|
||||
instanceRepo func(database.QueryExecutor) InstanceRepository
|
||||
cryptoRepo func(database.QueryExecutor) CryptoRepository
|
||||
orgRepo func(database.QueryExecutor) OrgRepository
|
||||
|
||||
instanceCache cache.Cache[string, string, *Instance]
|
||||
|
||||
generateID func() (string, error) = func() (string, error) {
|
||||
return strconv.FormatUint(rand.Uint64(), 10), nil
|
||||
}
|
||||
)
|
||||
|
||||
func SetPool(p database.Pool) {
|
||||
pool = p
|
||||
}
|
||||
|
||||
func SetUserCodeAlgorithm(algorithm crypto.EncryptionAlgorithm) {
|
||||
userCodeAlgorithm = algorithm
|
||||
}
|
||||
|
||||
func SetTracer(t tracing.Tracer) {
|
||||
tracer = t
|
||||
}
|
||||
|
||||
// func SetUserRepository(repo func(database.QueryExecutor) UserRepository) {
|
||||
// userRepo = repo
|
||||
// }
|
||||
|
||||
func SetInstanceRepository(repo func(database.QueryExecutor) InstanceRepository) {
|
||||
instanceRepo = repo
|
||||
}
|
||||
|
||||
func SetCryptoRepository(repo func(database.QueryExecutor) CryptoRepository) {
|
||||
cryptoRepo = repo
|
||||
}
|
45
backend/v3/domain/domain_test.go
Normal file
45
backend/v3/domain/domain_test.go
Normal file
@@ -0,0 +1,45 @@
|
||||
package domain_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"go.opentelemetry.io/otel"
|
||||
"go.opentelemetry.io/otel/exporters/stdout/stdouttrace"
|
||||
sdktrace "go.opentelemetry.io/otel/sdk/trace"
|
||||
|
||||
. "github.com/zitadel/zitadel/backend/v3/domain"
|
||||
"github.com/zitadel/zitadel/backend/v3/storage/database/repository"
|
||||
"github.com/zitadel/zitadel/backend/v3/telemetry/tracing"
|
||||
)
|
||||
|
||||
func TestExample(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
// SetPool(pool)
|
||||
|
||||
exporter, err := stdouttrace.New(stdouttrace.WithPrettyPrint())
|
||||
require.NoError(t, err)
|
||||
tracerProvider := sdktrace.NewTracerProvider(
|
||||
sdktrace.WithSyncer(exporter),
|
||||
)
|
||||
otel.SetTracerProvider(tracerProvider)
|
||||
SetTracer(tracing.Tracer{Tracer: tracerProvider.Tracer("test")})
|
||||
defer func() { assert.NoError(t, tracerProvider.Shutdown(ctx)) }()
|
||||
|
||||
SetUserRepository(repository.User)
|
||||
SetInstanceRepository(repository.Instance)
|
||||
SetCryptoRepository(repository.Crypto)
|
||||
|
||||
t.Run("verified email", func(t *testing.T) {
|
||||
err := Invoke(ctx, NewSetEmailCommand("u1", "test@example.com", NewEmailVerifiedCommand("u1", true)))
|
||||
assert.NoError(t, err)
|
||||
})
|
||||
|
||||
t.Run("unverified email", func(t *testing.T) {
|
||||
err := Invoke(ctx, NewSetEmailCommand("u2", "test2@example.com", NewEmailVerifiedCommand("u2", false)))
|
||||
assert.NoError(t, err)
|
||||
})
|
||||
}
|
155
backend/v3/domain/email_verification.go
Normal file
155
backend/v3/domain/email_verification.go
Normal file
@@ -0,0 +1,155 @@
|
||||
package domain
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
v4 "github.com/zitadel/zitadel/backend/v3/storage/database/repository/stmt/v4"
|
||||
)
|
||||
|
||||
type EmailVerifiedCommand struct {
|
||||
UserID string `json:"userId"`
|
||||
Email *Email `json:"email"`
|
||||
}
|
||||
|
||||
func NewEmailVerifiedCommand(userID string, isVerified bool) *EmailVerifiedCommand {
|
||||
return &EmailVerifiedCommand{
|
||||
UserID: userID,
|
||||
Email: &Email{
|
||||
IsVerified: isVerified,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
var (
|
||||
_ Commander = (*EmailVerifiedCommand)(nil)
|
||||
_ SetEmailOpt = (*EmailVerifiedCommand)(nil)
|
||||
)
|
||||
|
||||
// Execute implements [Commander]
|
||||
func (cmd *EmailVerifiedCommand) Execute(ctx context.Context, opts *CommandOpts) error {
|
||||
return userRepo(opts.DB).Human().ByID(cmd.UserID).Exec().SetEmailVerified(ctx, cmd.Email.Address)
|
||||
}
|
||||
|
||||
// applyOnSetEmail implements [SetEmailOpt]
|
||||
func (cmd *EmailVerifiedCommand) applyOnSetEmail(setEmailCmd *SetEmailCommand) {
|
||||
cmd.UserID = setEmailCmd.UserID
|
||||
cmd.Email.Address = setEmailCmd.Email
|
||||
setEmailCmd.verification = cmd
|
||||
}
|
||||
|
||||
type SendCodeCommand struct {
|
||||
UserID string `json:"userId"`
|
||||
Email string `json:"email"`
|
||||
URLTemplate *string `json:"urlTemplate"`
|
||||
generator *generateCodeCommand
|
||||
}
|
||||
|
||||
var (
|
||||
_ Commander = (*SendCodeCommand)(nil)
|
||||
_ SetEmailOpt = (*SendCodeCommand)(nil)
|
||||
)
|
||||
|
||||
func NewSendCodeCommand(userID string, urlTemplate *string) *SendCodeCommand {
|
||||
return &SendCodeCommand{
|
||||
UserID: userID,
|
||||
generator: &generateCodeCommand{},
|
||||
URLTemplate: urlTemplate,
|
||||
}
|
||||
}
|
||||
|
||||
// Execute implements [Commander]
|
||||
func (cmd *SendCodeCommand) Execute(ctx context.Context, opts *CommandOpts) error {
|
||||
if err := cmd.ensureEmail(ctx, opts); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := cmd.ensureURL(ctx, opts); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := opts.Invoker.Invoke(ctx, cmd.generator, opts); err != nil {
|
||||
return err
|
||||
}
|
||||
// TODO: queue notification
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (cmd *SendCodeCommand) ensureEmail(ctx context.Context, opts *CommandOpts) error {
|
||||
if cmd.Email != "" {
|
||||
return nil
|
||||
}
|
||||
email, err := userRepo(opts.DB).Human().ByID(cmd.UserID).Exec().GetEmail(ctx)
|
||||
if err != nil || email.IsVerified {
|
||||
return err
|
||||
}
|
||||
cmd.Email = email.Address
|
||||
return nil
|
||||
}
|
||||
|
||||
func (cmd *SendCodeCommand) ensureURL(ctx context.Context, opts *CommandOpts) error {
|
||||
if cmd.URLTemplate != nil && *cmd.URLTemplate != "" {
|
||||
return nil
|
||||
}
|
||||
_, _ = ctx, opts
|
||||
// TODO: load default template
|
||||
return nil
|
||||
}
|
||||
|
||||
// applyOnSetEmail implements [SetEmailOpt]
|
||||
func (cmd *SendCodeCommand) applyOnSetEmail(setEmailCmd *SetEmailCommand) {
|
||||
cmd.UserID = setEmailCmd.UserID
|
||||
cmd.Email = setEmailCmd.Email
|
||||
setEmailCmd.verification = cmd
|
||||
}
|
||||
|
||||
type ReturnCodeCommand struct {
|
||||
UserID string `json:"userId"`
|
||||
Email string `json:"email"`
|
||||
Code string `json:"code"`
|
||||
generator *generateCodeCommand
|
||||
}
|
||||
|
||||
var (
|
||||
_ Commander = (*ReturnCodeCommand)(nil)
|
||||
_ SetEmailOpt = (*ReturnCodeCommand)(nil)
|
||||
)
|
||||
|
||||
func NewReturnCodeCommand(userID string) *ReturnCodeCommand {
|
||||
return &ReturnCodeCommand{
|
||||
UserID: userID,
|
||||
generator: &generateCodeCommand{},
|
||||
}
|
||||
}
|
||||
|
||||
// Execute implements [Commander]
|
||||
func (cmd *ReturnCodeCommand) Execute(ctx context.Context, opts *CommandOpts) error {
|
||||
if err := cmd.ensureEmail(ctx, opts); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := opts.Invoker.Invoke(ctx, cmd.generator, opts); err != nil {
|
||||
return err
|
||||
}
|
||||
cmd.Code = cmd.generator.code
|
||||
return nil
|
||||
}
|
||||
|
||||
func (cmd *ReturnCodeCommand) ensureEmail(ctx context.Context, opts *CommandOpts) error {
|
||||
if cmd.Email != "" {
|
||||
return nil
|
||||
}
|
||||
user := v4.UserRepository(opts.DB)
|
||||
user.WithCondition(user.IDCondition(cmd.UserID))
|
||||
email, err := user.he.GetEmail(ctx)
|
||||
if err != nil || email.IsVerified {
|
||||
return err
|
||||
}
|
||||
cmd.Email = email.Address
|
||||
return nil
|
||||
}
|
||||
|
||||
// applyOnSetEmail implements [SetEmailOpt]
|
||||
func (cmd *ReturnCodeCommand) applyOnSetEmail(setEmailCmd *SetEmailCommand) {
|
||||
cmd.UserID = setEmailCmd.UserID
|
||||
cmd.Email = setEmailCmd.Email
|
||||
setEmailCmd.verification = cmd
|
||||
}
|
7
backend/v3/domain/errors.go
Normal file
7
backend/v3/domain/errors.go
Normal file
@@ -0,0 +1,7 @@
|
||||
package domain
|
||||
|
||||
import "errors"
|
||||
|
||||
var (
|
||||
ErrNoAdminSpecified = errors.New("at least one admin must be specified")
|
||||
)
|
36
backend/v3/domain/instance.go
Normal file
36
backend/v3/domain/instance.go
Normal file
@@ -0,0 +1,36 @@
|
||||
package domain
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Instance struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
CreatedAt time.Time `json:"-"`
|
||||
UpdatedAt time.Time `json:"-"`
|
||||
DeletedAt time.Time `json:"-"`
|
||||
}
|
||||
|
||||
// Keys implements the [cache.Entry].
|
||||
func (i *Instance) Keys(index string) (key []string) {
|
||||
// TODO: Return the correct keys for the instance cache, e.g., i.ID, i.Domain
|
||||
return []string{}
|
||||
}
|
||||
|
||||
type InstanceRepository interface {
|
||||
ByID(ctx context.Context, id string) (*Instance, error)
|
||||
Create(ctx context.Context, instance *Instance) error
|
||||
On(id string) InstanceOperation
|
||||
}
|
||||
|
||||
type InstanceOperation interface {
|
||||
AdminRepository
|
||||
Update(ctx context.Context, instance *Instance) error
|
||||
Delete(ctx context.Context) error
|
||||
}
|
||||
|
||||
type CreateInstance struct {
|
||||
Name string `json:"name"`
|
||||
}
|
94
backend/v3/domain/invoke.go
Normal file
94
backend/v3/domain/invoke.go
Normal file
@@ -0,0 +1,94 @@
|
||||
package domain
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/zitadel/zitadel/backend/v3/storage/eventstore"
|
||||
)
|
||||
|
||||
var defaultInvoker = newEventStoreInvoker(newTraceInvoker(nil))
|
||||
|
||||
func Invoke(ctx context.Context, cmd Commander) error {
|
||||
invoker := newEventStoreInvoker(newTraceInvoker(nil))
|
||||
opts := &CommandOpts{
|
||||
Invoker: invoker.collector,
|
||||
}
|
||||
return invoker.Invoke(ctx, cmd, opts)
|
||||
}
|
||||
|
||||
type eventStoreInvoker struct {
|
||||
collector *eventCollector
|
||||
}
|
||||
|
||||
func newEventStoreInvoker(next Invoker) *eventStoreInvoker {
|
||||
return &eventStoreInvoker{collector: &eventCollector{next: next}}
|
||||
}
|
||||
|
||||
func (i *eventStoreInvoker) Invoke(ctx context.Context, command Commander, opts *CommandOpts) (err error) {
|
||||
err = i.collector.Invoke(ctx, command, opts)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(i.collector.events) > 0 {
|
||||
err = eventstore.Publish(ctx, i.collector.events, opts.DB)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type eventCollector struct {
|
||||
next Invoker
|
||||
events []*eventstore.Event
|
||||
}
|
||||
|
||||
type eventer interface {
|
||||
Events() []*eventstore.Event
|
||||
}
|
||||
|
||||
func (i *eventCollector) Invoke(ctx context.Context, command Commander, opts *CommandOpts) (err error) {
|
||||
if e, ok := command.(eventer); ok && len(e.Events()) > 0 {
|
||||
// we need to ensure all commands are executed in the same transaction
|
||||
close, err := opts.EnsureTx(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() { err = close(ctx, err) }()
|
||||
|
||||
i.events = append(i.events, e.Events()...)
|
||||
}
|
||||
if i.next != nil {
|
||||
err = i.next.Invoke(ctx, command, opts)
|
||||
} else {
|
||||
err = command.Execute(ctx, opts)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type traceInvoker struct {
|
||||
next Invoker
|
||||
}
|
||||
|
||||
func newTraceInvoker(next Invoker) *traceInvoker {
|
||||
return &traceInvoker{next: next}
|
||||
}
|
||||
|
||||
func (i *traceInvoker) Invoke(ctx context.Context, command Commander, opts *CommandOpts) (err error) {
|
||||
ctx, span := tracer.Start(ctx, fmt.Sprintf("%T", command))
|
||||
defer span.End()
|
||||
|
||||
if i.next != nil {
|
||||
err = i.next.Invoke(ctx, command, opts)
|
||||
} else {
|
||||
err = command.Execute(ctx, opts)
|
||||
}
|
||||
if err != nil {
|
||||
span.RecordError(err)
|
||||
}
|
||||
return err
|
||||
}
|
39
backend/v3/domain/org.go
Normal file
39
backend/v3/domain/org.go
Normal file
@@ -0,0 +1,39 @@
|
||||
package domain
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Org struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
UpdatedAt time.Time `json:"updatedAt"`
|
||||
}
|
||||
|
||||
type OrgRepository interface {
|
||||
ByID(ctx context.Context, orgID string) (*Org, error)
|
||||
Create(ctx context.Context, org *Org) error
|
||||
On(id string) OrgOperation
|
||||
}
|
||||
|
||||
type OrgOperation interface {
|
||||
AdminRepository
|
||||
DomainRepository
|
||||
Update(ctx context.Context, org *Org) error
|
||||
Delete(ctx context.Context) error
|
||||
}
|
||||
|
||||
type AdminRepository interface {
|
||||
AddAdmin(ctx context.Context, userID string, roles []string) error
|
||||
SetAdminRoles(ctx context.Context, userID string, roles []string) error
|
||||
RemoveAdmin(ctx context.Context, userID string) error
|
||||
}
|
||||
|
||||
type DomainRepository interface {
|
||||
AddDomain(ctx context.Context, domain string) error
|
||||
SetDomainVerified(ctx context.Context, domain string) error
|
||||
RemoveDomain(ctx context.Context, domain string) error
|
||||
}
|
74
backend/v3/domain/org_add.go
Normal file
74
backend/v3/domain/org_add.go
Normal file
@@ -0,0 +1,74 @@
|
||||
package domain
|
||||
|
||||
import (
|
||||
"context"
|
||||
)
|
||||
|
||||
type AddOrgCommand struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Admins []AddAdminCommand `json:"admins"`
|
||||
}
|
||||
|
||||
func NewAddOrgCommand(name string, admins ...AddAdminCommand) *AddOrgCommand {
|
||||
return &AddOrgCommand{
|
||||
Name: name,
|
||||
Admins: admins,
|
||||
}
|
||||
}
|
||||
|
||||
// Execute implements Commander.
|
||||
func (cmd *AddOrgCommand) Execute(ctx context.Context, opts *CommandOpts) (err error) {
|
||||
if len(cmd.Admins) == 0 {
|
||||
return ErrNoAdminSpecified
|
||||
}
|
||||
if err = cmd.ensureID(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
close, err := opts.EnsureTx(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() { err = close(ctx, err) }()
|
||||
err = orgRepo(opts.DB).Create(ctx, &Org{
|
||||
ID: cmd.ID,
|
||||
Name: cmd.Name,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
var (
|
||||
_ Commander = (*AddOrgCommand)(nil)
|
||||
)
|
||||
|
||||
func (cmd *AddOrgCommand) ensureID() (err error) {
|
||||
if cmd.ID != "" {
|
||||
return nil
|
||||
}
|
||||
cmd.ID, err = generateID()
|
||||
return err
|
||||
}
|
||||
|
||||
type AddAdminCommand struct {
|
||||
UserID string `json:"userId"`
|
||||
Roles []string `json:"roles"`
|
||||
}
|
||||
|
||||
// Execute implements Commander.
|
||||
func (a *AddAdminCommand) Execute(ctx context.Context, opts *CommandOpts) (err error) {
|
||||
close, err := opts.EnsureTx(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() { err = close(ctx, err) }()
|
||||
return nil
|
||||
}
|
||||
|
||||
var (
|
||||
_ Commander = (*AddAdminCommand)(nil)
|
||||
)
|
82
backend/v3/domain/repository.go
Normal file
82
backend/v3/domain/repository.go
Normal file
@@ -0,0 +1,82 @@
|
||||
package domain
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"golang.org/x/exp/constraints"
|
||||
)
|
||||
|
||||
type Operation interface {
|
||||
// TextOperation |
|
||||
// NumberOperation |
|
||||
// BoolOperation
|
||||
|
||||
op()
|
||||
}
|
||||
|
||||
type clause[F ~uint8, Op Operation] struct {
|
||||
field F
|
||||
op Op
|
||||
}
|
||||
|
||||
func (c *clause[F, Op]) Field() F {
|
||||
return c.field
|
||||
}
|
||||
|
||||
func (c *clause[F, Op]) Operation() Op {
|
||||
return c.op
|
||||
}
|
||||
|
||||
type Text interface {
|
||||
~string | ~[]byte
|
||||
}
|
||||
|
||||
type TextOperation uint8
|
||||
|
||||
const (
|
||||
TextOperationEqual TextOperation = iota
|
||||
TextOperationNotEqual
|
||||
TextOperationStartsWith
|
||||
TextOperationStartsWithIgnoreCase
|
||||
)
|
||||
|
||||
func (TextOperation) op() {}
|
||||
|
||||
type Number interface {
|
||||
constraints.Integer | constraints.Float | constraints.Complex | time.Time
|
||||
}
|
||||
|
||||
type NumberOperation uint8
|
||||
|
||||
const (
|
||||
NumberOperationEqual NumberOperation = iota
|
||||
NumberOperationNotEqual
|
||||
NumberOperationLessThan
|
||||
NumberOperationLessThanOrEqual
|
||||
NumberOperationGreaterThan
|
||||
NumberOperationGreaterThanOrEqual
|
||||
)
|
||||
|
||||
func (NumberOperation) op() {}
|
||||
|
||||
type Bool interface {
|
||||
~bool
|
||||
}
|
||||
|
||||
type BoolOperation uint8
|
||||
|
||||
const (
|
||||
BoolOperationIs BoolOperation = iota
|
||||
BoolOperationNot
|
||||
)
|
||||
|
||||
func (BoolOperation) op() {}
|
||||
|
||||
type ListOperation uint8
|
||||
|
||||
const (
|
||||
ListOperationContains ListOperation = iota
|
||||
ListOperationNotContains
|
||||
)
|
||||
|
||||
func (ListOperation) op() {}
|
64
backend/v3/domain/set_email.go
Normal file
64
backend/v3/domain/set_email.go
Normal file
@@ -0,0 +1,64 @@
|
||||
package domain
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/zitadel/zitadel/backend/v3/storage/eventstore"
|
||||
)
|
||||
|
||||
type SetEmailCommand struct {
|
||||
UserID string `json:"userId"`
|
||||
Email string `json:"email"`
|
||||
verification Commander
|
||||
}
|
||||
|
||||
var (
|
||||
_ Commander = (*SetEmailCommand)(nil)
|
||||
_ eventer = (*SetEmailCommand)(nil)
|
||||
_ CreateHumanOpt = (*SetEmailCommand)(nil)
|
||||
)
|
||||
|
||||
type SetEmailOpt interface {
|
||||
applyOnSetEmail(*SetEmailCommand)
|
||||
}
|
||||
|
||||
func NewSetEmailCommand(userID, email string, verificationType SetEmailOpt) *SetEmailCommand {
|
||||
cmd := &SetEmailCommand{
|
||||
UserID: userID,
|
||||
Email: email,
|
||||
}
|
||||
verificationType.applyOnSetEmail(cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (cmd *SetEmailCommand) Execute(ctx context.Context, opts *CommandOpts) error {
|
||||
close, err := opts.EnsureTx(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() { err = close(ctx, err) }()
|
||||
// userStatement(opts.DB).Human().ByID(cmd.UserID).SetEmail(ctx, cmd.Email)
|
||||
err = userRepo(opts.DB).Human().ByID(cmd.UserID).Exec().SetEmail(ctx, cmd.Email)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return opts.Invoke(ctx, cmd.verification)
|
||||
}
|
||||
|
||||
// Events implements [eventer].
|
||||
func (cmd *SetEmailCommand) Events() []*eventstore.Event {
|
||||
return []*eventstore.Event{
|
||||
{
|
||||
AggregateType: "user",
|
||||
AggregateID: cmd.UserID,
|
||||
Type: "user.email.set",
|
||||
Payload: cmd,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// applyOnCreateHuman implements [CreateHumanOpt].
|
||||
func (cmd *SetEmailCommand) applyOnCreateHuman(createUserCmd *CreateUserCommand[Human]) {
|
||||
createUserCmd.email = cmd
|
||||
}
|
193
backend/v3/domain/user.go
Normal file
193
backend/v3/domain/user.go
Normal file
@@ -0,0 +1,193 @@
|
||||
package domain
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
v4 "github.com/zitadel/zitadel/backend/v3/storage/database/repository/stmt/v4"
|
||||
)
|
||||
|
||||
type userColumns interface {
|
||||
// TODO: move v4.columns to domain
|
||||
InstanceIDColumn() column
|
||||
OrgIDColumn() column
|
||||
IDColumn() column
|
||||
usernameColumn() column
|
||||
CreatedAtColumn() column
|
||||
UpdatedAtColumn() column
|
||||
DeletedAtColumn() column
|
||||
}
|
||||
|
||||
type userConditions interface {
|
||||
InstanceIDCondition(instanceID string) v4.Condition
|
||||
OrgIDCondition(orgID string) v4.Condition
|
||||
IDCondition(userID string) v4.Condition
|
||||
UsernameCondition(op v4.TextOperator, username string) v4.Condition
|
||||
CreatedAtCondition(op v4.NumberOperator, createdAt time.Time) v4.Condition
|
||||
UpdatedAtCondition(op v4.NumberOperator, updatedAt time.Time) v4.Condition
|
||||
DeletedCondition(isDeleted bool) v4.Condition
|
||||
DeletedAtCondition(op v4.NumberOperator, deletedAt time.Time) v4.Condition
|
||||
}
|
||||
|
||||
type UserRepository interface {
|
||||
userColumns
|
||||
userConditions
|
||||
// TODO: move condition to domain
|
||||
WithCondition(condition v4.Condition) UserRepository
|
||||
Get(ctx context.Context) (*User, error)
|
||||
List(ctx context.Context) ([]*User, error)
|
||||
Create(ctx context.Context, user *User) error
|
||||
Delete(ctx context.Context) error
|
||||
|
||||
Human() HumanRepository
|
||||
Machine() MachineRepository
|
||||
}
|
||||
|
||||
type humanColumns interface {
|
||||
FirstNameColumn() column
|
||||
LastNameColumn() column
|
||||
EmailAddressColumn() column
|
||||
EmailVerifiedAtColumn() column
|
||||
PhoneNumberColumn() column
|
||||
PhoneVerifiedAtColumn() column
|
||||
}
|
||||
|
||||
type humanConditions interface {
|
||||
FirstNameCondition(op v4.TextOperator, firstName string) v4.Condition
|
||||
LastNameCondition(op v4.TextOperator, lastName string) v4.Condition
|
||||
EmailAddressCondition(op v4.TextOperator, email string) v4.Condition
|
||||
EmailAddressVerifiedCondition(isVerified bool) v4.Condition
|
||||
EmailVerifiedAtCondition(op v4.TextOperator, emailVerifiedAt string) v4.Condition
|
||||
PhoneNumberCondition(op v4.TextOperator, phoneNumber string) v4.Condition
|
||||
PhoneNumberVerifiedCondition(isVerified bool) v4.Condition
|
||||
PhoneVerifiedAtCondition(op v4.TextOperator, phoneVerifiedAt string) v4.Condition
|
||||
}
|
||||
|
||||
type HumanRepository interface {
|
||||
humanColumns
|
||||
humanConditions
|
||||
|
||||
GetEmail(ctx context.Context) (*Email, error)
|
||||
// TODO: replace any with add email update columns
|
||||
SetEmail(ctx context.Context, columns ...any) error
|
||||
}
|
||||
|
||||
type machineColumns interface {
|
||||
DescriptionColumn() column
|
||||
}
|
||||
|
||||
type machineConditions interface {
|
||||
DescriptionCondition(op v4.TextOperator, description string) v4.Condition
|
||||
}
|
||||
|
||||
type MachineRepository interface {
|
||||
machineColumns
|
||||
machineConditions
|
||||
}
|
||||
|
||||
// type UserRepository interface {
|
||||
// // Get(ctx context.Context, clauses ...UserClause) (*User, error)
|
||||
// // Search(ctx context.Context, clauses ...UserClause) ([]*User, error)
|
||||
|
||||
// UserQuery[UserOperation]
|
||||
// Human() HumanQuery
|
||||
// Machine() MachineQuery
|
||||
// }
|
||||
|
||||
// type UserQuery[Op UserOperation] interface {
|
||||
// ByID(id string) UserQuery[Op]
|
||||
// Username(username string) UserQuery[Op]
|
||||
// Exec() Op
|
||||
// }
|
||||
|
||||
// type HumanQuery interface {
|
||||
// UserQuery[HumanOperation]
|
||||
// Email(op TextOperation, email string) HumanQuery
|
||||
// HumanOperation
|
||||
// }
|
||||
|
||||
// type MachineQuery interface {
|
||||
// UserQuery[MachineOperation]
|
||||
// MachineOperation
|
||||
// }
|
||||
|
||||
// type UserClause interface {
|
||||
// Field() UserField
|
||||
// Operation() Operation
|
||||
// Args() []any
|
||||
// }
|
||||
|
||||
// type UserField uint8
|
||||
|
||||
// const (
|
||||
// // Fields used for all users
|
||||
// UserFieldInstanceID UserField = iota + 1
|
||||
// UserFieldOrgID
|
||||
// UserFieldID
|
||||
// UserFieldUsername
|
||||
|
||||
// // Fields used for human users
|
||||
// UserHumanFieldEmail
|
||||
// UserHumanFieldEmailVerified
|
||||
|
||||
// // Fields used for machine users
|
||||
// UserMachineFieldDescription
|
||||
// )
|
||||
|
||||
// type userByIDClause struct {
|
||||
// id string
|
||||
// }
|
||||
|
||||
// func (c *userByIDClause) Field() UserField {
|
||||
// return UserFieldID
|
||||
// }
|
||||
|
||||
// func (c *userByIDClause) Operation() Operation {
|
||||
// return TextOperationEqual
|
||||
// }
|
||||
|
||||
// func (c *userByIDClause) Args() []any {
|
||||
// return []any{c.id}
|
||||
// }
|
||||
|
||||
// type UserOperation interface {
|
||||
// Delete(ctx context.Context) error
|
||||
// SetUsername(ctx context.Context, username string) error
|
||||
// }
|
||||
|
||||
// type HumanOperation interface {
|
||||
// UserOperation
|
||||
// SetEmail(ctx context.Context, email string) error
|
||||
// SetEmailVerified(ctx context.Context, email string) error
|
||||
// GetEmail(ctx context.Context) (*Email, error)
|
||||
// }
|
||||
|
||||
// type MachineOperation interface {
|
||||
// UserOperation
|
||||
// SetDescription(ctx context.Context, description string) error
|
||||
// }
|
||||
|
||||
type User struct {
|
||||
v4.User
|
||||
}
|
||||
|
||||
// type userTraits interface {
|
||||
// isUserTraits()
|
||||
// }
|
||||
|
||||
// type Human struct {
|
||||
// Email *Email `json:"email"`
|
||||
// }
|
||||
|
||||
// func (*Human) isUserTraits() {}
|
||||
|
||||
// type Machine struct {
|
||||
// Description string `json:"description"`
|
||||
// }
|
||||
|
||||
// func (*Machine) isUserTraits() {}
|
||||
|
||||
// type Email struct {
|
||||
// Address string `json:"address"`
|
||||
// IsVerified bool `json:"isVerified"`
|
||||
// }
|
Reference in New Issue
Block a user