Merge branch 'instance_table_2' into org_table

This commit is contained in:
Iraq Jaber
2025-06-17 09:34:34 +02:00
569 changed files with 37453 additions and 7223 deletions

View File

@@ -72,7 +72,7 @@ jobs:
with:
node_version: "18"
buf_version: "latest"
go_lint_version: "v1.62.2"
go_lint_version: "v1.64.8"
core_cache_key: ${{ needs.core.outputs.cache_key }}
core_cache_path: ${{ needs.core.outputs.cache_path }}

View File

@@ -48,6 +48,52 @@ When creating a new service, start with version `2`, as version `1` is reserved
Please check out the structure Buf style guide for more information about the folder and package structure: https://buf.build/docs/best-practices/style-guide/
### Deprecations
As a rule of thumb, redundant API methods are deprecated.
- The proto option `grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation.deprecated` MUST be set to true.
- One or more links to recommended replacement methods MUST be added to the deprecation message as a proto comment above the rpc spec.
- Guidance for switching to the recommended methods for common use cases SHOULD be added as a proto comment above the rpc spec.
#### Example
```protobuf
// Delete the user phone
//
// Deprecated: [Update the user's phone field](apis/resources/user_service_v2/user-service-update-user.api.mdx) to remove the phone number.
//
// Delete the phone number of a user.
rpc RemovePhone(RemovePhoneRequest) returns (RemovePhoneResponse) {
option (google.api.http) = {
delete: "/v2/users/{user_id}/phone"
body: "*"
};
option (zitadel.protoc_gen_zitadel.v2.options) = {
auth_option: {
permission: "authenticated"
}
};
option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = {
deprecated: true;
responses: {
key: "200"
value: {
description: "OK";
}
};
responses: {
key: "404";
value: {
description: "User ID does not exist.";
}
}
};
}
```
### Explicitness
Make the handling of the API as explicit as possible. Do not make assumptions about the client's knowledge of the system or the API.
@@ -73,6 +119,8 @@ For example, use `organization_id` instead of **org_id** or **resource_owner** f
#### Resources and Fields
##### Context information in Requests
When a context is required for creating a resource, the context is added as a field to the resource.
For example, when creating a new user, the organization's id is required. The `organization_id` is added as a field to the `CreateUserRequest`.
@@ -90,6 +138,65 @@ Only allow providing a context where it is required. The context MUST not be pro
For example, when retrieving or updating a user, the `organization_id` is not required, since the user can be determined by the user's id.
However, it is possible to provide the `organization_id` as a filter to retrieve a list of users of a specific organization.
##### Context information in Responses
When the action of creation, update or deletion of a resource was successful, the returned response has to include the time of the operation and the generated identifiers.
This is achieved through the addition of a timestamp attribute with the operation as a prefix, and the generated information as separate attributes.
```protobuf
message SetExecutionResponse {
// The timestamp of the execution set.
google.protobuf.Timestamp set_date = 1 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
example: "\"2024-12-18T07:50:47.492Z\"";
}
];
}
message CreateTargetResponse {
// The unique identifier of the newly created target.
string id = 1 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
example: "\"69629012906488334\"";
}
];
// The timestamp of the target creation.
google.protobuf.Timestamp creation_date = 2 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
example: "\"2024-12-18T07:50:47.492Z\"";
}
];
// Key used to sign and check payload sent to the target.
string signing_key = 3 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
example: "\"98KmsU67\""
}
];
}
message UpdateProjectGrantResponse {
// The timestamp of the change of the project grant.
google.protobuf.Timestamp change_date = 1 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
example: "\"2025-01-23T10:34:18.051Z\"";
}
];
}
message DeleteProjectGrantResponse {
// The timestamp of the deletion of the project grant.
// Note that the deletion date is only guaranteed to be set if the deletion was successful during the request.
// In case the deletion occurred in a previous request, the deletion date might be empty.
google.protobuf.Timestamp deletion_date = 1 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
example: "\"2025-01-23T10:34:18.051Z\"";
}
];
}
```
##### Global messages
Prevent the creation of global messages that are used in multiple resources unless they always follow the same pattern.
Use dedicated fields as described above or create a separate message for the specific context, that is only used in the boundary of the same resource.
For example, settings might be set as a default on the instance level, but might be overridden on the organization level.
@@ -99,6 +206,10 @@ The same applies to messages that are returned by multiple resources.
For example, information about the `User` might be different when managing the user resource itself than when it's returned
as part of an authorization or a manager role, where only limited information is needed.
On the other hand, types that always follow the same pattern and are used in multiple resources, such as `IDFilter`, `TimestampFilter` or `InIDsFilter` SHOULD be globalized and reused.
##### Re-using messages
Prevent reusing messages for the creation and the retrieval of a resource.
Returning messages might contain additional information that is not required or even not available for the creation of the resource.
What might sound obvious when designing the CreateUserRequest for example, where only an `organization_id` but not the
@@ -162,7 +273,7 @@ Additionally, state changes, specific actions or operations that do not fit into
The API uses OAuth 2 for authorization. There are corresponding middlewares that check the access token for validity and
automatically return an error if the token is invalid.
Permissions grated to the user might be organization specific and can therefore only be checked based on the queried resource.
Permissions granted to the user might be organization specific and can therefore only be checked based on the queried resource.
In such case, the API does not check the permissions itself but relies on the checks of the functions that are called by the API.
If the permission can be checked by the API itself, e.g. if the permission is instance wide, it can be annotated on the endpoint in the proto file (see below).
In any case, the required permissions need to be documented in the [API documentation](#documentation).
@@ -190,33 +301,54 @@ In case the permission cannot be checked by the API itself, but all requests nee
};
```
## Pagination
## Listing resources
The API uses pagination for listing resources. The client can specify a limit and an offset to retrieve a subset of the resources.
Additionally, the client can specify sorting options to sort the resources by a specific field.
Most listing methods SHOULD provide use the `ListQuery` message to allow the client to specify the limit, offset, and sorting options.
```protobuf
### Pagination
// ListQuery is a general query object for lists to allow pagination and sorting.
message ListQuery {
uint64 offset = 1;
// limit is the maximum amount of objects returned. The default is set to 100
// with a maximum of 1000 in the runtime configuration.
// If the limit exceeds the maximum configured ZITADEL will throw an error.
// If no limit is present the default is taken.
uint32 limit = 2;
// Asc is the sorting order. If true the list is sorted ascending, if false
// the list is sorted descending. The default is descending.
bool asc = 3;
Most listing methods SHOULD use the `PaginationRequest` message to allow the client to specify the limit, offset, and sorting options.
```protobuf
message ListTargetsRequest {
// List limitations and ordering.
optional zitadel.filter.v2beta.PaginationRequest pagination = 1;
// The field the result is sorted by. The default is the creation date. Beware that if you change this, your result pagination might be inconsistent.
optional TargetFieldName sorting_column = 2 [
(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
default: "\"TARGET_FIELD_NAME_CREATION_DATE\""
}
];
// Define the criteria to query for.
repeated TargetSearchFilter filters = 3;
option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_schema) = {
example: "{\"pagination\":{\"offset\":0,\"limit\":0,\"asc\":true},\"sortingColumn\":\"TARGET_FIELD_NAME_CREATION_DATE\",\"filters\":[{\"targetNameFilter\":{\"targetName\":\"ip_allow_list\",\"method\":\"TEXT_FILTER_METHOD_EQUALS\"}},{\"inTargetIdsFilter\":{\"targetIds\":[\"69629023906488334\",\"69622366012355662\"]}}]}";
};
}
```
On the corresponding responses the `ListDetails` can be used to return the total count of the resources
On the corresponding responses the `PaginationResponse` can be used to return the total count of the resources
and allow the user to handle their offset and limit accordingly.
The API MUST enforce a reasonable maximum limit for the number of resources that can be retrieved and returned in a single request.
The default limit is set to 100 and the maximum limit is set to 1000. If the client requests a limit that exceeds the maximum limit, an error is returned.
### Filter method
All filters in List operations SHOULD provide a method if not already specified by the filters name.
```protobuf
message TargetNameFilter {
// Defines the name of the target to query for.
string target_name = 1 [
(validate.rules).string = {max_len: 200}
];
// Defines which text comparison method used for the name query.
zitadel.filter.v2beta.TextFilterMethod method = 2 [
(validate.rules).enum.defined_only = true
];
}
```
## Error Handling
The API returns machine-readable errors in the response body. This includes a status code, an error code and possibly

View File

@@ -193,7 +193,7 @@ Use [Console](https://zitadel.com/docs/guides/manage/console/overview) or our [A
### Login V2
Check out our new Login V2 version in our [typescript repository](https://github.com/zitadel/typescript) or in our [documentation](https://zitadel.com/docs/guides/integrate/login/hosted-login#hosted-login-version-2-beta)
[![New Login Showcase](https://github.com/user-attachments/assets/cb5c5212-128b-4dc9-b11d-cabfd3f73e26)]
![New Login Showcase](https://github.com/user-attachments/assets/cb5c5212-128b-4dc9-b11d-cabfd3f73e26)
## Security

View File

@@ -1,19 +1,21 @@
package v2
import (
"github.com/zitadel/zitadel/backend/v3/telemetry/logging"
"github.com/zitadel/zitadel/backend/v3/telemetry/tracing"
)
// this file has been commented out to pass the linter
var (
logger logging.Logger
tracer tracing.Tracer
)
// import (
// "github.com/zitadel/zitadel/backend/v3/telemetry/logging"
// "github.com/zitadel/zitadel/backend/v3/telemetry/tracing"
// )
func SetLogger(l logging.Logger) {
logger = l
}
// var (
// logger logging.Logger
// tracer tracing.Tracer
// )
func SetTracer(t tracing.Tracer) {
tracer = t
}
// func SetLogger(l logging.Logger) {
// logger = l
// }
// func SetTracer(t tracing.Tracer) {
// tracer = t
// }

View File

@@ -1,33 +1,33 @@
package orgv2
import (
"context"
// import (
// "context"
"github.com/zitadel/zitadel/backend/v3/domain"
"github.com/zitadel/zitadel/pkg/grpc/org/v2"
)
// "github.com/zitadel/zitadel/backend/v3/domain"
// "github.com/zitadel/zitadel/pkg/grpc/org/v2"
// )
func CreateOrg(ctx context.Context, req *org.AddOrganizationRequest) (resp *org.AddOrganizationResponse, err error) {
cmd := domain.NewAddOrgCommand(
req.GetName(),
addOrgAdminToCommand(req.GetAdmins()...)...,
)
err = domain.Invoke(ctx, cmd)
if err != nil {
return nil, err
}
return &org.AddOrganizationResponse{
OrganizationId: cmd.ID,
}, nil
}
// func CreateOrg(ctx context.Context, req *org.AddOrganizationRequest) (resp *org.AddOrganizationResponse, err error) {
// cmd := domain.NewAddOrgCommand(
// req.GetName(),
// addOrgAdminToCommand(req.GetAdmins()...)...,
// )
// err = domain.Invoke(ctx, cmd)
// if err != nil {
// return nil, err
// }
// return &org.AddOrganizationResponse{
// OrganizationId: cmd.ID,
// }, nil
// }
func addOrgAdminToCommand(admins ...*org.AddOrganizationRequest_Admin) []*domain.AddMemberCommand {
cmds := make([]*domain.AddMemberCommand, len(admins))
for i, admin := range admins {
cmds[i] = &domain.AddMemberCommand{
UserID: admin.GetUserId(),
Roles: admin.GetRoles(),
}
}
return cmds
}
// func addOrgAdminToCommand(admins ...*org.AddOrganizationRequest_Admin) []*domain.AddMemberCommand {
// cmds := make([]*domain.AddMemberCommand, len(admins))
// for i, admin := range admins {
// cmds[i] = &domain.AddMemberCommand{
// UserID: admin.GetUserId(),
// Roles: admin.GetRoles(),
// }
// }
// return cmds
// }

View File

@@ -1,19 +1,21 @@
package orgv2
import (
"github.com/zitadel/zitadel/backend/v3/telemetry/logging"
"github.com/zitadel/zitadel/backend/v3/telemetry/tracing"
)
// this file has been commented out to pass the linter
var (
logger logging.Logger
tracer tracing.Tracer
)
// import (
// "github.com/zitadel/zitadel/backend/v3/telemetry/logging"
// "github.com/zitadel/zitadel/backend/v3/telemetry/tracing"
// )
func SetLogger(l logging.Logger) {
logger = l
}
// var (
// logger logging.Logger
// tracer tracing.Tracer
// )
func SetTracer(t tracing.Tracer) {
tracer = t
}
// func SetLogger(l logging.Logger) {
// logger = l
// }
// func SetTracer(t tracing.Tracer) {
// tracer = t
// }

View File

@@ -1,93 +1,93 @@
package userv2
import (
"context"
// import (
// "context"
"github.com/zitadel/zitadel/backend/v3/domain"
"github.com/zitadel/zitadel/pkg/grpc/user/v2"
)
// "github.com/zitadel/zitadel/backend/v3/domain"
// "github.com/zitadel/zitadel/pkg/grpc/user/v2"
// )
func SetEmail(ctx context.Context, req *user.SetEmailRequest) (resp *user.SetEmailResponse, err error) {
var (
verification domain.SetEmailOpt
returnCode *domain.ReturnCodeCommand
)
// func SetEmail(ctx context.Context, req *user.SetEmailRequest) (resp *user.SetEmailResponse, err error) {
// var (
// verification domain.SetEmailOpt
// returnCode *domain.ReturnCodeCommand
// )
switch req.GetVerification().(type) {
case *user.SetEmailRequest_IsVerified:
verification = domain.NewEmailVerifiedCommand(req.GetUserId(), req.GetIsVerified())
case *user.SetEmailRequest_SendCode:
verification = domain.NewSendCodeCommand(req.GetUserId(), req.GetSendCode().UrlTemplate)
case *user.SetEmailRequest_ReturnCode:
returnCode = domain.NewReturnCodeCommand(req.GetUserId())
verification = returnCode
default:
verification = domain.NewSendCodeCommand(req.GetUserId(), nil)
}
// switch req.GetVerification().(type) {
// case *user.SetEmailRequest_IsVerified:
// verification = domain.NewEmailVerifiedCommand(req.GetUserId(), req.GetIsVerified())
// case *user.SetEmailRequest_SendCode:
// verification = domain.NewSendCodeCommand(req.GetUserId(), req.GetSendCode().UrlTemplate)
// case *user.SetEmailRequest_ReturnCode:
// returnCode = domain.NewReturnCodeCommand(req.GetUserId())
// verification = returnCode
// default:
// verification = domain.NewSendCodeCommand(req.GetUserId(), nil)
// }
err = domain.Invoke(ctx, domain.NewSetEmailCommand(req.GetUserId(), req.GetEmail(), verification))
if err != nil {
return nil, err
}
// err = domain.Invoke(ctx, domain.NewSetEmailCommand(req.GetUserId(), req.GetEmail(), verification))
// if err != nil {
// return nil, err
// }
var code *string
if returnCode != nil && returnCode.Code != "" {
code = &returnCode.Code
}
// var code *string
// if returnCode != nil && returnCode.Code != "" {
// code = &returnCode.Code
// }
return &user.SetEmailResponse{
VerificationCode: code,
}, nil
}
// return &user.SetEmailResponse{
// VerificationCode: code,
// }, nil
// }
func SendEmailCode(ctx context.Context, req *user.SendEmailCodeRequest) (resp *user.SendEmailCodeResponse, err error) {
var (
returnCode *domain.ReturnCodeCommand
cmd domain.Commander
)
// func SendEmailCode(ctx context.Context, req *user.SendEmailCodeRequest) (resp *user.SendEmailCodeResponse, err error) {
// var (
// returnCode *domain.ReturnCodeCommand
// cmd domain.Commander
// )
switch req.GetVerification().(type) {
case *user.SendEmailCodeRequest_SendCode:
cmd = domain.NewSendCodeCommand(req.GetUserId(), req.GetSendCode().UrlTemplate)
case *user.SendEmailCodeRequest_ReturnCode:
returnCode = domain.NewReturnCodeCommand(req.GetUserId())
cmd = returnCode
default:
cmd = domain.NewSendCodeCommand(req.GetUserId(), req.GetSendCode().UrlTemplate)
}
err = domain.Invoke(ctx, cmd)
if err != nil {
return nil, err
}
resp = new(user.SendEmailCodeResponse)
if returnCode != nil {
resp.VerificationCode = &returnCode.Code
}
return resp, nil
}
// switch req.GetVerification().(type) {
// case *user.SendEmailCodeRequest_SendCode:
// cmd = domain.NewSendCodeCommand(req.GetUserId(), req.GetSendCode().UrlTemplate)
// case *user.SendEmailCodeRequest_ReturnCode:
// returnCode = domain.NewReturnCodeCommand(req.GetUserId())
// cmd = returnCode
// default:
// cmd = domain.NewSendCodeCommand(req.GetUserId(), req.GetSendCode().UrlTemplate)
// }
// err = domain.Invoke(ctx, cmd)
// if err != nil {
// return nil, err
// }
// resp = new(user.SendEmailCodeResponse)
// if returnCode != nil {
// resp.VerificationCode = &returnCode.Code
// }
// return resp, nil
// }
func ResendEmailCode(ctx context.Context, req *user.ResendEmailCodeRequest) (resp *user.SendEmailCodeResponse, err error) {
var (
returnCode *domain.ReturnCodeCommand
cmd domain.Commander
)
// func ResendEmailCode(ctx context.Context, req *user.ResendEmailCodeRequest) (resp *user.SendEmailCodeResponse, err error) {
// var (
// returnCode *domain.ReturnCodeCommand
// cmd domain.Commander
// )
switch req.GetVerification().(type) {
case *user.ResendEmailCodeRequest_SendCode:
cmd = domain.NewSendCodeCommand(req.GetUserId(), req.GetSendCode().UrlTemplate)
case *user.ResendEmailCodeRequest_ReturnCode:
returnCode = domain.NewReturnCodeCommand(req.GetUserId())
cmd = returnCode
default:
cmd = domain.NewSendCodeCommand(req.GetUserId(), req.GetSendCode().UrlTemplate)
}
err = domain.Invoke(ctx, cmd)
if err != nil {
return nil, err
}
resp = new(user.SendEmailCodeResponse)
if returnCode != nil {
resp.VerificationCode = &returnCode.Code
}
return resp, nil
}
// switch req.GetVerification().(type) {
// case *user.ResendEmailCodeRequest_SendCode:
// cmd = domain.NewSendCodeCommand(req.GetUserId(), req.GetSendCode().UrlTemplate)
// case *user.ResendEmailCodeRequest_ReturnCode:
// returnCode = domain.NewReturnCodeCommand(req.GetUserId())
// cmd = returnCode
// default:
// cmd = domain.NewSendCodeCommand(req.GetUserId(), req.GetSendCode().UrlTemplate)
// }
// err = domain.Invoke(ctx, cmd)
// if err != nil {
// return nil, err
// }
// resp = new(user.SendEmailCodeResponse)
// if returnCode != nil {
// resp.VerificationCode = &returnCode.Code
// }
// return resp, nil
// }

View File

@@ -1,19 +1,19 @@
package userv2
import (
"github.com/zitadel/zitadel/backend/v3/telemetry/logging"
"github.com/zitadel/zitadel/backend/v3/telemetry/tracing"
)
// this file has been commented out to pass the linter
var (
logger logging.Logger
tracer tracing.Tracer
)
// import (
// "github.com/zitadel/zitadel/backend/v3/telemetry/logging"
// "github.com/zitadel/zitadel/backend/v3/telemetry/tracing"
// )
func SetLogger(l logging.Logger) {
logger = l
}
// logger logging.Logger
// var tracer tracing.Tracer
func SetTracer(t tracing.Tracer) {
tracer = t
}
// func SetLogger(l logging.Logger) {
// logger = l
// }
// func SetTracer(t tracing.Tracer) {
// tracer = t
// }

View File

@@ -1,131 +1,131 @@
package domain
import (
"context"
"fmt"
// import (
// "context"
// "fmt"
"github.com/zitadel/zitadel/backend/v3/storage/database"
)
// "github.com/zitadel/zitadel/backend/v3/storage/database"
// )
// Commander is the all it needs to implement the command pattern.
// It is the interface all manipulations need to implement.
// If possible it should also be used for queries. We will find out if this is possible in the future.
type Commander interface {
Execute(ctx context.Context, opts *CommandOpts) (err error)
fmt.Stringer
}
// // Commander is the all it needs to implement the command pattern.
// // It is the interface all manipulations need to implement.
// // If possible it should also be used for queries. We will find out if this is possible in the future.
// type Commander interface {
// Execute(ctx context.Context, opts *CommandOpts) (err error)
// fmt.Stringer
// }
// Invoker is part of the command pattern.
// It is the interface that is used to execute commands.
type Invoker interface {
Invoke(ctx context.Context, command Commander, opts *CommandOpts) error
}
// // Invoker is part of the command pattern.
// // It is the interface that is used to execute commands.
// type Invoker interface {
// Invoke(ctx context.Context, command Commander, opts *CommandOpts) error
// }
// CommandOpts are passed to each command
// the provide common fields used by commands like the database client.
type CommandOpts struct {
DB database.QueryExecutor
Invoker Invoker
}
// // CommandOpts are passed to each command
// // the provide common fields used by commands like the database client.
// type CommandOpts struct {
// DB database.QueryExecutor
// Invoker Invoker
// }
type ensureTxOpts struct {
*database.TransactionOptions
}
// type ensureTxOpts struct {
// *database.TransactionOptions
// }
type EnsureTransactionOpt func(*ensureTxOpts)
// 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
}
// // 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)
}
// 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
// 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
}
// 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
}
// // 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 (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,
}
}
// func DefaultOpts(invoker Invoker) *CommandOpts {
// if invoker == nil {
// invoker = &noopInvoker{}
// }
// return &CommandOpts{
// DB: pool,
// Invoker: invoker,
// }
// }
// commandBatch is a batch of commands.
// It uses the [Invoker] provided by the opts to execute each command.
type commandBatch struct {
Commands []Commander
}
// // commandBatch is a batch of commands.
// // It uses the [Invoker] provided by the opts to execute each command.
// type commandBatch struct {
// Commands []Commander
// }
func BatchCommands(cmds ...Commander) *commandBatch {
return &commandBatch{
Commands: cmds,
}
}
// func BatchCommands(cmds ...Commander) *commandBatch {
// return &commandBatch{
// Commands: cmds,
// }
// }
// String implements [Commander].
func (cmd *commandBatch) String() string {
return "commandBatch"
}
// // String implements [Commander].
// func (cmd *commandBatch) String() string {
// return "commandBatch"
// }
func (b *commandBatch) Execute(ctx context.Context, opts *CommandOpts) (err error) {
for _, cmd := range b.Commands {
if err = opts.Invoke(ctx, cmd); err != nil {
return err
}
}
return nil
}
// func (b *commandBatch) Execute(ctx context.Context, opts *CommandOpts) (err error) {
// for _, cmd := range b.Commands {
// if err = opts.Invoke(ctx, cmd); err != nil {
// return err
// }
// }
// return nil
// }
var _ Commander = (*commandBatch)(nil)
// var _ Commander = (*commandBatch)(nil)

View File

@@ -1,90 +1,90 @@
package domain
import (
"context"
// import (
// "context"
"github.com/zitadel/zitadel/backend/v3/storage/eventstore"
)
// "github.com/zitadel/zitadel/backend/v3/storage/eventstore"
// )
// CreateUserCommand adds a new user including the email verification for humans.
// In the future it might make sense to separate the command into two commands:
// - CreateHumanCommand: creates a new human user
// - CreateMachineCommand: creates a new machine user
type CreateUserCommand struct {
user *User
email *SetEmailCommand
}
// // CreateUserCommand adds a new user including the email verification for humans.
// // In the future it might make sense to separate the command into two commands:
// // - CreateHumanCommand: creates a new human user
// // - CreateMachineCommand: creates a new machine user
// type CreateUserCommand struct {
// user *User
// email *SetEmailCommand
// }
var (
_ Commander = (*CreateUserCommand)(nil)
_ eventer = (*CreateUserCommand)(nil)
)
// var (
// _ Commander = (*CreateUserCommand)(nil)
// _ eventer = (*CreateUserCommand)(nil)
// )
// opts heavily reduces the complexity for email verification because each type of verification is a simple option which implements the [Commander] interface.
func NewCreateHumanCommand(username string, opts ...CreateHumanOpt) *CreateUserCommand {
cmd := &CreateUserCommand{
user: &User{
Username: username,
Traits: &Human{},
},
}
// // opts heavily reduces the complexity for email verification because each type of verification is a simple option which implements the [Commander] interface.
// func NewCreateHumanCommand(username string, opts ...CreateHumanOpt) *CreateUserCommand {
// cmd := &CreateUserCommand{
// user: &User{
// Username: username,
// Traits: &Human{},
// },
// }
for _, opt := range opts {
opt.applyOnCreateHuman(cmd)
}
return cmd
}
// for _, opt := range opts {
// opt.applyOnCreateHuman(cmd)
// }
// return cmd
// }
// String implements [Commander].
func (cmd *CreateUserCommand) String() string {
return "CreateUserCommand"
}
// // String implements [Commander].
// func (cmd *CreateUserCommand) String() string {
// return "CreateUserCommand"
// }
// Events implements [eventer].
func (c *CreateUserCommand) Events() []*eventstore.Event {
return []*eventstore.Event{
{
AggregateType: "user",
AggregateID: c.user.ID,
Type: "user.added",
Payload: c.user,
},
}
}
// // Events implements [eventer].
// func (c *CreateUserCommand) Events() []*eventstore.Event {
// return []*eventstore.Event{
// {
// AggregateType: "user",
// AggregateID: c.user.ID,
// Type: "user.added",
// Payload: c.user,
// },
// }
// }
// 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
}
// // 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 CreateHumanOpt interface {
// applyOnCreateHuman(*CreateUserCommand)
// }
type createHumanIDOpt string
// type createHumanIDOpt string
// applyOnCreateHuman implements [CreateHumanOpt].
func (c createHumanIDOpt) applyOnCreateHuman(cmd *CreateUserCommand) {
cmd.user.ID = string(c)
}
// // applyOnCreateHuman implements [CreateHumanOpt].
// func (c createHumanIDOpt) applyOnCreateHuman(cmd *CreateUserCommand) {
// cmd.user.ID = string(c)
// }
var _ CreateHumanOpt = (*createHumanIDOpt)(nil)
// var _ CreateHumanOpt = (*createHumanIDOpt)(nil)
func CreateHumanWithID(id string) CreateHumanOpt {
return createHumanIDOpt(id)
}
// 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
}
// func (c *CreateUserCommand) ensureUserID() (err error) {
// if c.user.ID != "" {
// return nil
// }
// c.user.ID, err = generateID()
// return err
// }

View File

@@ -1,37 +1,37 @@
package domain
import (
"context"
// import (
// "context"
"github.com/zitadel/zitadel/internal/crypto"
)
// "github.com/zitadel/zitadel/internal/crypto"
// )
type generateCodeCommand struct {
code string
value *crypto.CryptoValue
}
// type generateCodeCommand struct {
// code string
// value *crypto.CryptoValue
// }
// I didn't update this repository to the solution proposed please view one of the following interfaces for correct usage:
// - [UserRepository]
// - [InstanceRepository]
// - [OrgRepository]
type CryptoRepository interface {
GetEncryptionConfig(ctx context.Context) (*crypto.GeneratorConfig, error)
}
// // I didn't update this repository to the solution proposed please view one of the following interfaces for correct usage:
// // - [UserRepository]
// // - [InstanceRepository]
// // - [OrgRepository]
// type CryptoRepository interface {
// GetEncryptionConfig(ctx context.Context) (*crypto.GeneratorConfig, error)
// }
// String implements [Commander].
func (cmd *generateCodeCommand) String() string {
return "generateCodeCommand"
}
// // String implements [Commander].
// func (cmd *generateCodeCommand) String() string {
// return "generateCodeCommand"
// }
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
}
// 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
// }
var _ Commander = (*generateCodeCommand)(nil)
// var _ Commander = (*generateCodeCommand)(nil)

View File

@@ -1,65 +1,66 @@
package domain
import (
"math/rand/v2"
"strconv"
// 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/logging"
"github.com/zitadel/zitadel/backend/v3/telemetry/tracing"
"github.com/zitadel/zitadel/internal/crypto"
)
// "github.com/zitadel/zitadel/backend/v3/storage/cache"
// "github.com/zitadel/zitadel/backend/v3/storage/database"
// The variables could also be moved to a struct.
// I just started with the singleton pattern and kept it like this.
var (
pool database.Pool
userCodeAlgorithm crypto.EncryptionAlgorithm
tracer tracing.Tracer
logger logging.Logger
// // "github.com/zitadel/zitadel/backend/v3/telemetry/logging"
// "github.com/zitadel/zitadel/backend/v3/telemetry/tracing"
// "github.com/zitadel/zitadel/internal/crypto"
// )
userRepo func(database.QueryExecutor) UserRepository
instanceRepo func(database.QueryExecutor) InstanceRepository
cryptoRepo func(database.QueryExecutor) CryptoRepository
orgRepo func(database.QueryExecutor) OrgRepository
// // The variables could also be moved to a struct.
// // I just started with the singleton pattern and kept it like this.
// var (
// pool database.Pool
// userCodeAlgorithm crypto.EncryptionAlgorithm
// tracer tracing.Tracer
// // logger logging.Logger
instanceCache cache.Cache[instanceCacheIndex, string, *Instance]
orgCache cache.Cache[orgCacheIndex, string, *Org]
// userRepo func(database.QueryExecutor) UserRepository
// // instanceRepo func(database.QueryExecutor) InstanceRepository
// cryptoRepo func(database.QueryExecutor) CryptoRepository
// orgRepo func(database.QueryExecutor) OrgRepository
generateID func() (string, error) = func() (string, error) {
return strconv.FormatUint(rand.Uint64(), 10), nil
}
)
// // instanceCache cache.Cache[instanceCacheIndex, string, *Instance]
// orgCache cache.Cache[orgCacheIndex, string, *Org]
func SetPool(p database.Pool) {
pool = p
}
// generateID func() (string, error) = func() (string, error) {
// return strconv.FormatUint(rand.Uint64(), 10), nil
// }
// )
func SetUserCodeAlgorithm(algorithm crypto.EncryptionAlgorithm) {
userCodeAlgorithm = algorithm
}
// func SetPool(p database.Pool) {
// pool = p
// }
func SetTracer(t tracing.Tracer) {
tracer = t
}
// func SetUserCodeAlgorithm(algorithm crypto.EncryptionAlgorithm) {
// userCodeAlgorithm = algorithm
// }
func SetLogger(l logging.Logger) {
logger = l
}
// func SetTracer(t tracing.Tracer) {
// tracer = t
// }
func SetUserRepository(repo func(database.QueryExecutor) UserRepository) {
userRepo = repo
}
// // func SetLogger(l logging.Logger) {
// // logger = l
// // }
func SetOrgRepository(repo func(database.QueryExecutor) OrgRepository) {
orgRepo = repo
}
// func SetUserRepository(repo func(database.QueryExecutor) UserRepository) {
// userRepo = repo
// }
func SetInstanceRepository(repo func(database.QueryExecutor) InstanceRepository) {
instanceRepo = repo
}
// func SetOrgRepository(repo func(database.QueryExecutor) OrgRepository) {
// orgRepo = repo
// }
func SetCryptoRepository(repo func(database.QueryExecutor) CryptoRepository) {
cryptoRepo = repo
}
// // func SetInstanceRepository(repo func(database.QueryExecutor) InstanceRepository) {
// // instanceRepo = repo
// // }
// func SetCryptoRepository(repo func(database.QueryExecutor) CryptoRepository) {
// cryptoRepo = repo
// }

View File

@@ -1,67 +1,67 @@
package domain_test
import (
"context"
"log/slog"
"testing"
// import (
// "context"
// "log/slog"
// "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"
"go.uber.org/mock/gomock"
// "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"
// "go.uber.org/mock/gomock"
. "github.com/zitadel/zitadel/backend/v3/domain"
"github.com/zitadel/zitadel/backend/v3/storage/database/dbmock"
"github.com/zitadel/zitadel/backend/v3/storage/database/repository"
"github.com/zitadel/zitadel/backend/v3/telemetry/logging"
"github.com/zitadel/zitadel/backend/v3/telemetry/tracing"
)
// . "github.com/zitadel/zitadel/backend/v3/domain"
// "github.com/zitadel/zitadel/backend/v3/storage/database/dbmock"
// "github.com/zitadel/zitadel/backend/v3/storage/database/repository"
// "github.com/zitadel/zitadel/backend/v3/telemetry/logging"
// "github.com/zitadel/zitadel/backend/v3/telemetry/tracing"
// )
// These tests give an overview of how to use the domain package.
func TestExample(t *testing.T) {
t.Skip("skip example test because it is not a real test")
ctx := context.Background()
// func TestExample(t *testing.T) {
// t.Skip("skip example test because it is not a real test")
// ctx := context.Background()
ctrl := gomock.NewController(t)
pool := dbmock.NewMockPool(ctrl)
tx := dbmock.NewMockTransaction(ctrl)
// ctrl := gomock.NewController(t)
// pool := dbmock.NewMockPool(ctrl)
// tx := dbmock.NewMockTransaction(ctrl)
pool.EXPECT().Begin(gomock.Any(), gomock.Any()).Return(tx, nil)
tx.EXPECT().End(gomock.Any(), gomock.Any()).Return(nil)
SetPool(pool)
// pool.EXPECT().Begin(gomock.Any(), gomock.Any()).Return(tx, nil)
// tx.EXPECT().End(gomock.Any(), gomock.Any()).Return(nil)
// 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)) }()
// 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)) }()
SetLogger(logging.Logger{Logger: slog.Default()})
// SetLogger(logging.Logger{Logger: slog.Default()})
SetUserRepository(repository.UserRepository)
SetOrgRepository(repository.OrgRepository)
// SetInstanceRepository(repository.Instance)
// SetCryptoRepository(repository.Crypto)
// SetUserRepository(repository.UserRepository)
// SetOrgRepository(repository.OrgRepository)
// // SetInstanceRepository(repository.Instance)
// // SetCryptoRepository(repository.Crypto)
t.Run("create org", func(t *testing.T) {
org := NewAddOrgCommand("testorg", NewAddMemberCommand("testuser", "ORG_OWNER"))
user := NewCreateHumanCommand("testuser")
err := Invoke(ctx, BatchCommands(org, user))
assert.NoError(t, err)
})
// t.Run("create org", func(t *testing.T) {
// org := NewAddOrgCommand("testorg", NewAddMemberCommand("testuser", "ORG_OWNER"))
// user := NewCreateHumanCommand("testuser")
// err := Invoke(ctx, BatchCommands(org, user))
// assert.NoError(t, err)
// })
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("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)
})
}
// t.Run("unverified email", func(t *testing.T) {
// err := Invoke(ctx, NewSetEmailCommand("u2", "test2@example.com", NewEmailVerifiedCommand("u2", false)))
// assert.NoError(t, err)
// })
// }

View File

@@ -1,175 +1,175 @@
package domain
import (
"context"
"time"
)
// import (
// "context"
// "time"
// )
// EmailVerifiedCommand verifies an email address for a user.
type EmailVerifiedCommand struct {
UserID string `json:"userId"`
Email *Email `json:"email"`
}
// // EmailVerifiedCommand verifies an email address for a user.
// type EmailVerifiedCommand struct {
// UserID string `json:"userId"`
// Email *Email `json:"email"`
// }
func NewEmailVerifiedCommand(userID string, isVerified bool) *EmailVerifiedCommand {
return &EmailVerifiedCommand{
UserID: userID,
Email: &Email{
VerifiedAt: time.Time{},
},
}
}
// func NewEmailVerifiedCommand(userID string, isVerified bool) *EmailVerifiedCommand {
// return &EmailVerifiedCommand{
// UserID: userID,
// Email: &Email{
// VerifiedAt: time.Time{},
// },
// }
// }
// String implements [Commander].
func (cmd *EmailVerifiedCommand) String() string {
return "EmailVerifiedCommand"
}
// // String implements [Commander].
// func (cmd *EmailVerifiedCommand) String() string {
// return "EmailVerifiedCommand"
// }
var (
_ Commander = (*EmailVerifiedCommand)(nil)
_ SetEmailOpt = (*EmailVerifiedCommand)(nil)
)
// var (
// _ Commander = (*EmailVerifiedCommand)(nil)
// _ SetEmailOpt = (*EmailVerifiedCommand)(nil)
// )
// Execute implements [Commander]
func (cmd *EmailVerifiedCommand) Execute(ctx context.Context, opts *CommandOpts) error {
repo := userRepo(opts.DB).Human()
return repo.Update(ctx, repo.IDCondition(cmd.UserID), repo.SetEmailVerifiedAt(time.Time{}))
}
// // Execute implements [Commander]
// func (cmd *EmailVerifiedCommand) Execute(ctx context.Context, opts *CommandOpts) error {
// repo := userRepo(opts.DB).Human()
// return repo.Update(ctx, repo.IDCondition(cmd.UserID), repo.SetEmailVerifiedAt(time.Time{}))
// }
// applyOnSetEmail implements [SetEmailOpt]
func (cmd *EmailVerifiedCommand) applyOnSetEmail(setEmailCmd *SetEmailCommand) {
cmd.UserID = setEmailCmd.UserID
cmd.Email.Address = setEmailCmd.Email
setEmailCmd.verification = cmd
}
// // applyOnSetEmail implements [SetEmailOpt]
// func (cmd *EmailVerifiedCommand) applyOnSetEmail(setEmailCmd *SetEmailCommand) {
// cmd.UserID = setEmailCmd.UserID
// cmd.Email.Address = setEmailCmd.Email
// setEmailCmd.verification = cmd
// }
// SendCodeCommand sends a verification code to the user's email address.
// If the URLTemplate is not set it will use the default of the organization / instance.
type SendCodeCommand struct {
UserID string `json:"userId"`
Email string `json:"email"`
URLTemplate *string `json:"urlTemplate"`
generator *generateCodeCommand
}
// // SendCodeCommand sends a verification code to the user's email address.
// // If the URLTemplate is not set it will use the default of the organization / instance.
// type SendCodeCommand struct {
// UserID string `json:"userId"`
// Email string `json:"email"`
// URLTemplate *string `json:"urlTemplate"`
// generator *generateCodeCommand
// }
var (
_ Commander = (*SendCodeCommand)(nil)
_ SetEmailOpt = (*SendCodeCommand)(nil)
)
// var (
// _ Commander = (*SendCodeCommand)(nil)
// _ SetEmailOpt = (*SendCodeCommand)(nil)
// )
func NewSendCodeCommand(userID string, urlTemplate *string) *SendCodeCommand {
return &SendCodeCommand{
UserID: userID,
generator: &generateCodeCommand{},
URLTemplate: urlTemplate,
}
}
// func NewSendCodeCommand(userID string, urlTemplate *string) *SendCodeCommand {
// return &SendCodeCommand{
// UserID: userID,
// generator: &generateCodeCommand{},
// URLTemplate: urlTemplate,
// }
// }
// String implements [Commander].
func (cmd *SendCodeCommand) String() string {
return "SendCodeCommand"
}
// // String implements [Commander].
// func (cmd *SendCodeCommand) String() string {
// return "SendCodeCommand"
// }
// 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
}
// // 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
// if err := opts.Invoker.Invoke(ctx, cmd.generator, opts); err != nil {
// return err
// }
// // TODO: queue notification
return nil
}
// return nil
// }
func (cmd *SendCodeCommand) ensureEmail(ctx context.Context, opts *CommandOpts) error {
if cmd.Email != "" {
return nil
}
repo := userRepo(opts.DB).Human()
email, err := repo.GetEmail(ctx, repo.IDCondition(cmd.UserID))
if err != nil || !email.VerifiedAt.IsZero() {
return err
}
cmd.Email = email.Address
return nil
}
// func (cmd *SendCodeCommand) ensureEmail(ctx context.Context, opts *CommandOpts) error {
// if cmd.Email != "" {
// return nil
// }
// repo := userRepo(opts.DB).Human()
// email, err := repo.GetEmail(ctx, repo.IDCondition(cmd.UserID))
// if err != nil || !email.VerifiedAt.IsZero() {
// 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
}
// 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
}
// // applyOnSetEmail implements [SetEmailOpt]
// func (cmd *SendCodeCommand) applyOnSetEmail(setEmailCmd *SetEmailCommand) {
// cmd.UserID = setEmailCmd.UserID
// cmd.Email = setEmailCmd.Email
// setEmailCmd.verification = cmd
// }
// ReturnCodeCommand creates the code and returns it to the caller.
// The caller gets the code by calling the Code field after the command got executed.
type ReturnCodeCommand struct {
UserID string `json:"userId"`
Email string `json:"email"`
Code string `json:"code"`
generator *generateCodeCommand
}
// // ReturnCodeCommand creates the code and returns it to the caller.
// // The caller gets the code by calling the Code field after the command got executed.
// type ReturnCodeCommand struct {
// UserID string `json:"userId"`
// Email string `json:"email"`
// Code string `json:"code"`
// generator *generateCodeCommand
// }
var (
_ Commander = (*ReturnCodeCommand)(nil)
_ SetEmailOpt = (*ReturnCodeCommand)(nil)
)
// var (
// _ Commander = (*ReturnCodeCommand)(nil)
// _ SetEmailOpt = (*ReturnCodeCommand)(nil)
// )
func NewReturnCodeCommand(userID string) *ReturnCodeCommand {
return &ReturnCodeCommand{
UserID: userID,
generator: &generateCodeCommand{},
}
}
// func NewReturnCodeCommand(userID string) *ReturnCodeCommand {
// return &ReturnCodeCommand{
// UserID: userID,
// generator: &generateCodeCommand{},
// }
// }
// String implements [Commander].
func (cmd *ReturnCodeCommand) String() string {
return "ReturnCodeCommand"
}
// // String implements [Commander].
// func (cmd *ReturnCodeCommand) String() string {
// return "ReturnCodeCommand"
// }
// 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
}
// // 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
}
repo := userRepo(opts.DB).Human()
email, err := repo.GetEmail(ctx, repo.IDCondition(cmd.UserID))
if err != nil || !email.VerifiedAt.IsZero() {
return err
}
cmd.Email = email.Address
return nil
}
// func (cmd *ReturnCodeCommand) ensureEmail(ctx context.Context, opts *CommandOpts) error {
// if cmd.Email != "" {
// return nil
// }
// repo := userRepo(opts.DB).Human()
// email, err := repo.GetEmail(ctx, repo.IDCondition(cmd.UserID))
// if err != nil || !email.VerifiedAt.IsZero() {
// 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
}
// // applyOnSetEmail implements [SetEmailOpt]
// func (cmd *ReturnCodeCommand) applyOnSetEmail(setEmailCmd *SetEmailCommand) {
// cmd.UserID = setEmailCmd.UserID
// cmd.Email = setEmailCmd.Email
// setEmailCmd.verification = cmd
// }

View File

@@ -11,14 +11,14 @@ import (
type Instance struct {
ID string `json:"id,omitempty" db:"id"`
Name string `json:"name,omitempty" db:"name"`
DefaultOrgID string `json:"default_org_id,omitempty" db:"default_org_id"`
IAMProjectID string `json:"iam_project_id,omitempty" db:"iam_project_id"`
ConsoleClientID string `json:"console_client_id,omitempty" db:"console_client_id"`
ConsoleAppID string `json:"console_app_id,omitempty" db:"console_app_id"`
DefaultLanguage string `json:"default_language,omitempty" db:"default_language"`
CreatedAt time.Time `json:"-,omitempty" db:"created_at"`
UpdatedAt time.Time `json:"-,omitempty" db:"updated_at"`
DeletedAt *time.Time `json:"-,omitempty" db:"deleted_at"`
DefaultOrgID string `json:"defaultOrgId,omitempty" db:"default_org_id"`
IAMProjectID string `json:"iamProjectId,omitempty" db:"iam_project_id"`
ConsoleClientID string `json:"consoleClientId,omitempty" db:"console_client_id"`
ConsoleAppID string `json:"consoleAppId,omitempty" db:"console_app_id"`
DefaultLanguage string `json:"defaultLanguage,omitempty" db:"default_language"`
CreatedAt time.Time `json:"createdAt" db:"created_at"`
UpdatedAt time.Time `json:"updatedAt" db:"updated_at"`
DeletedAt *time.Time `json:"deletedAt" db:"deleted_at"`
}
type instanceCacheIndex uint8

View File

@@ -1,158 +1,158 @@
package domain
import (
"context"
"fmt"
// import (
// "context"
// "fmt"
"github.com/zitadel/zitadel/backend/v3/storage/eventstore"
)
// "github.com/zitadel/zitadel/backend/v3/storage/eventstore"
// )
// Invoke provides a way to execute commands within the domain package.
// It uses a chain of responsibility pattern to handle the command execution.
// The default chain includes logging, tracing, and event publishing.
// If you want to invoke multiple commands in a single transaction, you can use the [commandBatch].
func Invoke(ctx context.Context, cmd Commander) error {
invoker := newEventStoreInvoker(newLoggingInvoker(newTraceInvoker(nil)))
opts := &CommandOpts{
Invoker: invoker.collector,
DB: pool,
}
return invoker.Invoke(ctx, cmd, opts)
}
// // Invoke provides a way to execute commands within the domain package.
// // It uses a chain of responsibility pattern to handle the command execution.
// // The default chain includes logging, tracing, and event publishing.
// // If you want to invoke multiple commands in a single transaction, you can use the [commandBatch].
// func Invoke(ctx context.Context, cmd Commander) error {
// invoker := newEventStoreInvoker(newLoggingInvoker(newTraceInvoker(nil)))
// opts := &CommandOpts{
// Invoker: invoker.collector,
// DB: pool,
// }
// return invoker.Invoke(ctx, cmd, opts)
// }
// eventStoreInvoker checks if the command implements the [eventer] interface.
// If it does, it collects the events and publishes them to the event store.
type eventStoreInvoker struct {
collector *eventCollector
}
// // eventStoreInvoker checks if the command implements the [eventer] interface.
// // If it does, it collects the events and publishes them to the event store.
// type eventStoreInvoker struct {
// collector *eventCollector
// }
func newEventStoreInvoker(next Invoker) *eventStoreInvoker {
return &eventStoreInvoker{collector: &eventCollector{next: next}}
}
// 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
}
// 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
// }
// eventCollector collects events from all commands. The [eventStoreInvoker] pushes the collected events after all commands are executed.
type eventCollector struct {
next Invoker
events []*eventstore.Event
}
// // eventCollector collects events from all commands. The [eventStoreInvoker] pushes the collected events after all commands are executed.
// type eventCollector struct {
// next Invoker
// events []*eventstore.Event
// }
type eventer interface {
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) }()
// 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 {
return i.next.Invoke(ctx, command, opts)
}
return command.Execute(ctx, opts)
}
// i.events = append(i.events, e.Events()...)
// }
// if i.next != nil {
// return i.next.Invoke(ctx, command, opts)
// }
// return command.Execute(ctx, opts)
// }
// traceInvoker decorates each command with tracing.
type traceInvoker struct {
next Invoker
}
// // traceInvoker decorates each command with tracing.
// type traceInvoker struct {
// next Invoker
// }
func newTraceInvoker(next Invoker) *traceInvoker {
return &traceInvoker{next: next}
}
// 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 func() {
if err != nil {
span.RecordError(err)
}
span.End()
}()
// func (i *traceInvoker) Invoke(ctx context.Context, command Commander, opts *CommandOpts) (err error) {
// ctx, span := tracer.Start(ctx, fmt.Sprintf("%T", command))
// defer func() {
// if err != nil {
// span.RecordError(err)
// }
// span.End()
// }()
if i.next != nil {
return i.next.Invoke(ctx, command, opts)
}
return command.Execute(ctx, opts)
}
// if i.next != nil {
// return i.next.Invoke(ctx, command, opts)
// }
// return command.Execute(ctx, opts)
// }
// loggingInvoker decorates each command with logging.
// It is an example implementation and logs the command name at the beginning and success or failure after the command got executed.
type loggingInvoker struct {
next Invoker
}
// // loggingInvoker decorates each command with logging.
// // It is an example implementation and logs the command name at the beginning and success or failure after the command got executed.
// type loggingInvoker struct {
// next Invoker
// }
func newLoggingInvoker(next Invoker) *loggingInvoker {
return &loggingInvoker{next: next}
}
// func newLoggingInvoker(next Invoker) *loggingInvoker {
// return &loggingInvoker{next: next}
// }
func (i *loggingInvoker) Invoke(ctx context.Context, command Commander, opts *CommandOpts) (err error) {
logger.InfoContext(ctx, "Invoking command", "command", command.String())
// func (i *loggingInvoker) Invoke(ctx context.Context, command Commander, opts *CommandOpts) (err error) {
// logger.InfoContext(ctx, "Invoking command", "command", command.String())
if i.next != nil {
err = i.next.Invoke(ctx, command, opts)
} else {
err = command.Execute(ctx, opts)
}
// if i.next != nil {
// err = i.next.Invoke(ctx, command, opts)
// } else {
// err = command.Execute(ctx, opts)
// }
if err != nil {
logger.ErrorContext(ctx, "Command invocation failed", "command", command.String(), "error", err)
return err
}
logger.InfoContext(ctx, "Command invocation succeeded", "command", command.String())
return nil
}
// if err != nil {
// logger.ErrorContext(ctx, "Command invocation failed", "command", command.String(), "error", err)
// return err
// }
// logger.InfoContext(ctx, "Command invocation succeeded", "command", command.String())
// return nil
// }
type noopInvoker struct {
next 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)
}
// 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)
// }
// cacheInvoker could be used in the future to do the caching.
// My goal would be to have two interfaces:
// - cacheSetter: which caches an object
// - cacheGetter: which gets an object from the cache, this should also skip the command execution
type cacheInvoker struct {
next Invoker
}
// // cacheInvoker could be used in the future to do the caching.
// // My goal would be to have two interfaces:
// // - cacheSetter: which caches an object
// // - cacheGetter: which gets an object from the cache, this should also skip the command execution
// type cacheInvoker struct {
// next Invoker
// }
type cacher interface {
Cache(opts *CommandOpts)
}
// type cacher interface {
// Cache(opts *CommandOpts)
// }
func (i *cacheInvoker) Invoke(ctx context.Context, command Commander, opts *CommandOpts) (err error) {
if c, ok := command.(cacher); ok {
c.Cache(opts)
}
if i.next != nil {
err = i.next.Invoke(ctx, command, opts)
} else {
err = command.Execute(ctx, opts)
}
return err
}
// func (i *cacheInvoker) Invoke(ctx context.Context, command Commander, opts *CommandOpts) (err error) {
// if c, ok := command.(cacher); ok {
// c.Cache(opts)
// }
// if i.next != nil {
// err = i.next.Invoke(ctx, command, opts)
// } else {
// err = command.Execute(ctx, opts)
// }
// return err
// }

View File

@@ -1,137 +1,137 @@
package domain
import (
"context"
// import (
// "context"
"github.com/zitadel/zitadel/backend/v3/storage/eventstore"
)
// "github.com/zitadel/zitadel/backend/v3/storage/eventstore"
// )
// AddOrgCommand adds a new organization.
// I'm unsure if we should add the Admins here or if this should be a separate command.
type AddOrgCommand struct {
ID string `json:"id"`
Name string `json:"name"`
Admins []*AddMemberCommand `json:"admins"`
}
// // AddOrgCommand adds a new organization.
// // I'm unsure if we should add the Admins here or if this should be a separate command.
// type AddOrgCommand struct {
// ID string `json:"id"`
// Name string `json:"name"`
// Admins []*AddMemberCommand `json:"admins"`
// }
func NewAddOrgCommand(name string, admins ...*AddMemberCommand) *AddOrgCommand {
return &AddOrgCommand{
Name: name,
Admins: admins,
}
}
// func NewAddOrgCommand(name string, admins ...*AddMemberCommand) *AddOrgCommand {
// return &AddOrgCommand{
// Name: name,
// Admins: admins,
// }
// }
// String implements [Commander].
func (cmd *AddOrgCommand) String() string {
return "AddOrgCommand"
}
// // String implements [Commander].
// func (cmd *AddOrgCommand) String() string {
// return "AddOrgCommand"
// }
// 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
}
// // 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
}
// 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
// }
for _, admin := range cmd.Admins {
admin.orgID = cmd.ID
if err = opts.Invoke(ctx, admin); err != nil {
return err
}
}
// for _, admin := range cmd.Admins {
// admin.orgID = cmd.ID
// if err = opts.Invoke(ctx, admin); err != nil {
// return err
// }
// }
orgCache.Set(ctx, &Org{
ID: cmd.ID,
Name: cmd.Name,
})
// orgCache.Set(ctx, &Org{
// ID: cmd.ID,
// Name: cmd.Name,
// })
return nil
}
// return nil
// }
// Events implements [eventer].
func (cmd *AddOrgCommand) Events() []*eventstore.Event {
return []*eventstore.Event{
{
AggregateType: "org",
AggregateID: cmd.ID,
Type: "org.added",
Payload: cmd,
},
}
}
// // Events implements [eventer].
// func (cmd *AddOrgCommand) Events() []*eventstore.Event {
// return []*eventstore.Event{
// {
// AggregateType: "org",
// AggregateID: cmd.ID,
// Type: "org.added",
// Payload: cmd,
// },
// }
// }
var (
_ Commander = (*AddOrgCommand)(nil)
_ eventer = (*AddOrgCommand)(nil)
)
// var (
// _ Commander = (*AddOrgCommand)(nil)
// _ eventer = (*AddOrgCommand)(nil)
// )
func (cmd *AddOrgCommand) ensureID() (err error) {
if cmd.ID != "" {
return nil
}
cmd.ID, err = generateID()
return err
}
// func (cmd *AddOrgCommand) ensureID() (err error) {
// if cmd.ID != "" {
// return nil
// }
// cmd.ID, err = generateID()
// return err
// }
// AddMemberCommand adds a new member to an organization.
// I'm not sure if we should make it more generic to also use it for instances.
type AddMemberCommand struct {
orgID string
UserID string `json:"userId"`
Roles []string `json:"roles"`
}
// // AddMemberCommand adds a new member to an organization.
// // I'm not sure if we should make it more generic to also use it for instances.
// type AddMemberCommand struct {
// orgID string
// UserID string `json:"userId"`
// Roles []string `json:"roles"`
// }
func NewAddMemberCommand(userID string, roles ...string) *AddMemberCommand {
return &AddMemberCommand{
UserID: userID,
Roles: roles,
}
}
// func NewAddMemberCommand(userID string, roles ...string) *AddMemberCommand {
// return &AddMemberCommand{
// UserID: userID,
// Roles: roles,
// }
// }
// String implements [Commander].
func (cmd *AddMemberCommand) String() string {
return "AddMemberCommand"
}
// // String implements [Commander].
// func (cmd *AddMemberCommand) String() string {
// return "AddMemberCommand"
// }
// Execute implements Commander.
func (a *AddMemberCommand) Execute(ctx context.Context, opts *CommandOpts) (err error) {
close, err := opts.EnsureTx(ctx)
if err != nil {
return err
}
defer func() { err = close(ctx, err) }()
// // Execute implements Commander.
// func (a *AddMemberCommand) 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 orgRepo(opts.DB).Member().AddMember(ctx, a.orgID, a.UserID, a.Roles)
}
// return orgRepo(opts.DB).Member().AddMember(ctx, a.orgID, a.UserID, a.Roles)
// }
// Events implements [eventer].
func (a *AddMemberCommand) Events() []*eventstore.Event {
return []*eventstore.Event{
{
AggregateType: "org",
AggregateID: a.UserID,
Type: "member.added",
Payload: a,
},
}
}
// // Events implements [eventer].
// func (a *AddMemberCommand) Events() []*eventstore.Event {
// return []*eventstore.Event{
// {
// AggregateType: "org",
// AggregateID: a.UserID,
// Type: "member.added",
// Payload: a,
// },
// }
// }
var (
_ Commander = (*AddMemberCommand)(nil)
_ eventer = (*AddMemberCommand)(nil)
)
// var (
// _ Commander = (*AddMemberCommand)(nil)
// _ eventer = (*AddMemberCommand)(nil)
// )

View File

@@ -1,74 +1,74 @@
package domain
import (
"context"
// import (
// "context"
"github.com/zitadel/zitadel/backend/v3/storage/eventstore"
)
// "github.com/zitadel/zitadel/backend/v3/storage/eventstore"
// )
// SetEmailCommand sets the email address of a user.
// If allows verification as a sub command.
// The verification command is executed after the email address is set.
// The verification command is executed in the same transaction as the email address update.
type SetEmailCommand struct {
UserID string `json:"userId"`
Email string `json:"email"`
verification Commander
}
// // SetEmailCommand sets the email address of a user.
// // If allows verification as a sub command.
// // The verification command is executed after the email address is set.
// // The verification command is executed in the same transaction as the email address update.
// type SetEmailCommand struct {
// UserID string `json:"userId"`
// Email string `json:"email"`
// verification Commander
// }
var (
_ Commander = (*SetEmailCommand)(nil)
_ eventer = (*SetEmailCommand)(nil)
_ CreateHumanOpt = (*SetEmailCommand)(nil)
)
// var (
// _ Commander = (*SetEmailCommand)(nil)
// _ eventer = (*SetEmailCommand)(nil)
// _ CreateHumanOpt = (*SetEmailCommand)(nil)
// )
type SetEmailOpt interface {
applyOnSetEmail(*SetEmailCommand)
}
// 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 NewSetEmailCommand(userID, email string, verificationType SetEmailOpt) *SetEmailCommand {
// cmd := &SetEmailCommand{
// UserID: userID,
// Email: email,
// }
// verificationType.applyOnSetEmail(cmd)
// return cmd
// }
// String implements [Commander].
func (cmd *SetEmailCommand) String() string {
return "SetEmailCommand"
}
// // String implements [Commander].
// func (cmd *SetEmailCommand) String() string {
// return "SetEmailCommand"
// }
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)
repo := userRepo(opts.DB).Human()
err = repo.Update(ctx, repo.IDCondition(cmd.UserID), repo.SetEmailAddress(cmd.Email))
if err != nil {
return err
}
// 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)
// repo := userRepo(opts.DB).Human()
// err = repo.Update(ctx, repo.IDCondition(cmd.UserID), repo.SetEmailAddress(cmd.Email))
// if err != nil {
// return err
// }
return opts.Invoke(ctx, cmd.verification)
}
// 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,
},
}
}
// // 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) {
createUserCmd.email = cmd
}
// // applyOnCreateHuman implements [CreateHumanOpt].
// func (cmd *SetEmailCommand) applyOnCreateHuman(createUserCmd *CreateUserCommand) {
// createUserCmd.email = cmd
// }

View File

@@ -20,7 +20,7 @@ func (a *and) Write(builder *StatementBuilder) {
if i > 0 {
builder.WriteString(" AND ")
}
condition.(Condition).Write(builder)
condition.Write(builder)
}
}
@@ -45,7 +45,7 @@ func (o *or) Write(builder *StatementBuilder) {
if i > 0 {
builder.WriteString(" OR ")
}
condition.(Condition).Write(builder)
condition.Write(builder)
}
}
@@ -85,7 +85,7 @@ func (i *isNotNull) Write(builder *StatementBuilder) {
// IsNotNull creates a condition that checks if a column is NOT NULL.
func IsNotNull(column Column) *isNotNull {
return &isNotNull{column: column.(Column)}
return &isNotNull{column: column}
}
var _ Condition = (*isNotNull)(nil)

View File

@@ -16,6 +16,7 @@ type Pool interface {
type PoolTest interface {
Pool
// MigrateTest is the same as [Migrator] but executes the migrations multiple times instead of only once.
MigrateTest(ctx context.Context) error
}
@@ -35,6 +36,7 @@ type Querier interface {
}
// Executor is a database client that can execute statements.
// It returns the number of rows affected or an error
type Executor interface {
Exec(ctx context.Context, stmt string, args ...any) (int64, error)
}

View File

@@ -34,7 +34,7 @@ type Config struct {
// // The value will be taken as is. Multiple options are space separated.
// Options string
configuredFields []string
// configuredFields []string
}
// Connect implements [database.Connector].

View File

@@ -10,3 +10,17 @@ CREATE TABLE IF NOT EXISTS zitadel.instances(
updated_at TIMESTAMPTZ DEFAULT NOW(),
deleted_at TIMESTAMPTZ DEFAULT NULL
);
CREATE OR REPLACE FUNCTION zitadel.set_updated_at()
RETURNS TRIGGER AS $$
BEGIN
NEW.updated_at := NOW();
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER trigger_set_updated_at
BEFORE UPDATE ON zitadel.instances
FOR EACH ROW
WHEN (OLD.updated_at IS NOT DISTINCT FROM NEW.updated_at)
EXECUTE FUNCTION zitadel.set_updated_at();

View File

@@ -83,11 +83,6 @@ func (c *pgxPool) Migrate(ctx context.Context) error {
// Migrate implements [database.PoolTest].
func (c *pgxPool) MigrateTest(ctx context.Context) error {
// allow multiple migrations
// if isMigrated {
// return nil
// }
client, err := c.Pool.Acquire(ctx)
if err != nil {
return err

View File

@@ -2,6 +2,7 @@ package postgres
import (
"context"
"errors"
"github.com/jackc/pgx/v5"
@@ -25,7 +26,10 @@ func (tx *pgxTx) Rollback(ctx context.Context) error {
// End implements [database.Transaction].
func (tx *pgxTx) End(ctx context.Context, err error) error {
if err != nil {
tx.Rollback(ctx)
rollbackErr := tx.Rollback(ctx)
if rollbackErr != nil {
err = errors.Join(err, rollbackErr)
}
return err
}
return tx.Commit(ctx)

View File

@@ -105,14 +105,25 @@ func TestServer_TestInstanceDeleteReduces(t *testing.T) {
})
require.NoError(t, err)
instanceRepo := repository.InstanceRepository(pool)
// check instance exists
retryDuration, tick := integration.WaitForAndTickWithMaxDuration(CTX, time.Minute)
assert.EventuallyWithT(t, func(ttt *assert.CollectT) {
instance, err := instanceRepo.Get(CTX,
instanceRepo.NameCondition(database.TextOperationEqual, instanceName),
)
require.NoError(ttt, err)
require.Equal(ttt, instanceName, instance.Name)
}, retryDuration, tick)
_, err = SystemClient.RemoveInstance(CTX, &system.RemoveInstanceRequest{
InstanceId: res.InstanceId,
})
require.NoError(t, err)
instanceRepo := repository.InstanceRepository(pool)
retryDuration, tick := integration.WaitForAndTickWithMaxDuration(CTX, time.Minute)
assert.EventuallyWithT(t, func(t *assert.CollectT) {
retryDuration, tick = integration.WaitForAndTickWithMaxDuration(CTX, time.Minute)
assert.EventuallyWithT(t, func(ttt *assert.CollectT) {
instance, err := instanceRepo.Get(CTX,
instanceRepo.NameCondition(database.TextOperationEqual, instanceName),
)

View File

@@ -46,7 +46,7 @@ func (opts *QueryOpts) WriteOrderBy(builder *StatementBuilder) {
return
}
builder.WriteString(" ORDER BY ")
Columns(opts.OrderBy).Write(builder)
opts.OrderBy.Write(builder)
}
func (opts *QueryOpts) WriteLimit(builder *StatementBuilder) {

View File

@@ -6,6 +6,7 @@ import (
"time"
"github.com/jackc/pgx/v5/pgconn"
"github.com/zitadel/zitadel/backend/v3/domain"
"github.com/zitadel/zitadel/backend/v3/storage/database"
)
@@ -33,28 +34,27 @@ const queryInstanceStmt = `SELECT id, name, default_org_id, iam_project_id, cons
// Get implements [domain.InstanceRepository].
func (i *instance) Get(ctx context.Context, opts ...database.Condition) (*domain.Instance, error) {
builder := database.StatementBuilder{}
var builder database.StatementBuilder
builder.WriteString(queryInstanceStmt)
// return only non deleted isntances
// return only non deleted instances
opts = append(opts, database.IsNull(i.DeletedAtColumn()))
andCondition := database.And(opts...)
i.writeCondition(&builder, andCondition)
i.writeCondition(&builder, database.And(opts...))
return scanInstance(ctx, i.client, &builder)
}
// List implements [domain.InstanceRepository].
func (i *instance) List(ctx context.Context, opts ...database.Condition) ([]*domain.Instance, error) {
builder := database.StatementBuilder{}
var builder database.StatementBuilder
builder.WriteString(queryInstanceStmt)
// return only non deleted isntances
// return only non deleted instances
opts = append(opts, database.IsNull(i.DeletedAtColumn()))
andCondition := database.And(opts...)
i.writeCondition(&builder, andCondition)
notDeletedCondition := database.And(opts...)
i.writeCondition(&builder, notDeletedCondition)
return scanInstances(ctx, i.client, &builder)
}
@@ -65,7 +65,8 @@ const createInstanceStmt = `INSERT INTO zitadel.instances (id, name, default_org
// Create implements [domain.InstanceRepository].
func (i *instance) Create(ctx context.Context, instance *domain.Instance) error {
builder := database.StatementBuilder{}
var builder database.StatementBuilder
builder.AppendArgs(instance.ID, instance.Name, instance.DefaultOrgID, instance.IAMProjectID, instance.ConsoleClientID, instance.ConsoleAppID, instance.DefaultLanguage)
builder.WriteString(createInstanceStmt)
@@ -95,10 +96,14 @@ func (i *instance) Create(ctx context.Context, instance *domain.Instance) error
// Update implements [domain.InstanceRepository].
func (i instance) Update(ctx context.Context, condition database.Condition, changes ...database.Change) (int64, error) {
builder := database.StatementBuilder{}
var builder database.StatementBuilder
builder.WriteString(`UPDATE zitadel.instances SET `)
// don't update deleted instances
conditions := []database.Condition{condition, database.IsNull(i.DeletedAtColumn())}
database.Changes(changes).Write(&builder)
i.writeCondition(&builder, condition)
i.writeCondition(&builder, database.And(conditions...))
stmt := builder.String()
@@ -111,7 +116,8 @@ func (i instance) Delete(ctx context.Context, condition database.Condition) erro
if condition == nil {
return errors.New("Delete must contain a condition") // (otherwise ALL instances will be deleted)
}
builder := database.StatementBuilder{}
var builder database.StatementBuilder
builder.WriteString(`UPDATE zitadel.instances SET deleted_at = $1`)
builder.AppendArgs(time.Now())

View File

@@ -7,7 +7,7 @@ import (
"time"
"github.com/brianvoe/gofakeit/v6"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/zitadel/zitadel/backend/v3/domain"
"github.com/zitadel/zitadel/backend/v3/storage/database"
@@ -17,7 +17,7 @@ import (
func TestCreateInstance(t *testing.T) {
tests := []struct {
name string
testFunc func() *domain.Instance
testFunc func(ctx context.Context, t *testing.T) *domain.Instance
instance domain.Instance
err error
}{
@@ -39,7 +39,7 @@ func TestCreateInstance(t *testing.T) {
}(),
},
{
name: "create instance wihtout name",
name: "create instance without name",
instance: func() domain.Instance {
instanceId := gofakeit.Name()
// instanceName := gofakeit.Name()
@@ -58,12 +58,11 @@ func TestCreateInstance(t *testing.T) {
},
{
name: "adding same instance twice",
testFunc: func() *domain.Instance {
testFunc: func(ctx context.Context, t *testing.T) *domain.Instance {
instanceRepo := repository.InstanceRepository(pool)
instanceId := gofakeit.Name()
instanceName := gofakeit.Name()
ctx := context.Background()
inst := domain.Instance{
ID: instanceId,
Name: instanceName,
@@ -75,7 +74,7 @@ func TestCreateInstance(t *testing.T) {
}
err := instanceRepo.Create(ctx, &inst)
assert.NoError(t, err)
require.NoError(t, err)
return &inst
},
err: errors.New("instance id already exists"),
@@ -105,7 +104,7 @@ func TestCreateInstance(t *testing.T) {
var instance *domain.Instance
if tt.testFunc != nil {
instance = tt.testFunc()
instance = tt.testFunc(ctx, t)
} else {
instance = &tt.instance
}
@@ -114,7 +113,7 @@ func TestCreateInstance(t *testing.T) {
// create instance
beforeCreate := time.Now()
err := instanceRepo.Create(ctx, instance)
assert.Equal(t, tt.err, err)
require.Equal(t, tt.err, err)
if err != nil {
return
}
@@ -124,17 +123,18 @@ func TestCreateInstance(t *testing.T) {
instance, err = instanceRepo.Get(ctx,
instanceRepo.NameCondition(database.TextOperationEqual, instance.Name),
)
assert.Equal(t, tt.instance.ID, instance.ID)
assert.Equal(t, tt.instance.Name, instance.Name)
assert.Equal(t, tt.instance.DefaultOrgID, instance.DefaultOrgID)
assert.Equal(t, tt.instance.IAMProjectID, instance.IAMProjectID)
assert.Equal(t, tt.instance.ConsoleClientID, instance.ConsoleClientID)
assert.Equal(t, tt.instance.ConsoleAppID, instance.ConsoleAppID)
assert.Equal(t, tt.instance.DefaultLanguage, instance.DefaultLanguage)
assert.WithinRange(t, instance.CreatedAt, beforeCreate, afterCreate)
assert.WithinRange(t, instance.UpdatedAt, beforeCreate, afterCreate)
assert.Nil(t, instance.DeletedAt)
assert.NoError(t, err)
require.NoError(t, err)
require.Equal(t, tt.instance.ID, instance.ID)
require.Equal(t, tt.instance.Name, instance.Name)
require.Equal(t, tt.instance.DefaultOrgID, instance.DefaultOrgID)
require.Equal(t, tt.instance.IAMProjectID, instance.IAMProjectID)
require.Equal(t, tt.instance.ConsoleClientID, instance.ConsoleClientID)
require.Equal(t, tt.instance.ConsoleAppID, instance.ConsoleAppID)
require.Equal(t, tt.instance.DefaultLanguage, instance.DefaultLanguage)
require.WithinRange(t, instance.CreatedAt, beforeCreate, afterCreate)
require.WithinRange(t, instance.UpdatedAt, beforeCreate, afterCreate)
require.Nil(t, instance.DeletedAt)
})
}
}
@@ -142,17 +142,16 @@ func TestCreateInstance(t *testing.T) {
func TestUpdateInstance(t *testing.T) {
tests := []struct {
name string
testFunc func() *domain.Instance
testFunc func(ctx context.Context, t *testing.T) *domain.Instance
rowsAffected int64
}{
{
name: "happy path",
testFunc: func() *domain.Instance {
testFunc: func(ctx context.Context, t *testing.T) *domain.Instance {
instanceRepo := repository.InstanceRepository(pool)
instanceId := gofakeit.Name()
instanceName := gofakeit.Name()
ctx := context.Background()
inst := domain.Instance{
ID: instanceId,
Name: instanceName,
@@ -165,14 +164,45 @@ func TestUpdateInstance(t *testing.T) {
// create instance
err := instanceRepo.Create(ctx, &inst)
assert.NoError(t, err)
require.NoError(t, err)
return &inst
},
rowsAffected: 1,
},
{
name: "update deleted instance",
testFunc: func(ctx context.Context, t *testing.T) *domain.Instance {
instanceRepo := repository.InstanceRepository(pool)
instanceId := gofakeit.Name()
instanceName := gofakeit.Name()
inst := domain.Instance{
ID: instanceId,
Name: instanceName,
DefaultOrgID: "defaultOrgId",
IAMProjectID: "iamProject",
ConsoleClientID: "consoleCLient",
ConsoleAppID: "consoleApp",
DefaultLanguage: "defaultLanguage",
}
// create instance
err := instanceRepo.Create(ctx, &inst)
require.NoError(t, err)
// delete instance
err = instanceRepo.Delete(ctx,
instanceRepo.IDCondition(inst.ID),
)
require.NoError(t, err)
return &inst
},
rowsAffected: 0,
},
{
name: "update non existent instance",
testFunc: func() *domain.Instance {
testFunc: func(ctx context.Context, t *testing.T) *domain.Instance {
instanceId := gofakeit.Name()
inst := domain.Instance{
@@ -185,13 +215,12 @@ func TestUpdateInstance(t *testing.T) {
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
beforeUpdate := time.Now()
ctx := context.Background()
instanceRepo := repository.InstanceRepository(pool)
instance := tt.testFunc()
instance := tt.testFunc(ctx, t)
beforeUpdate := time.Now()
// update name
newName := "new_" + instance.Name
rowsAffected, err := instanceRepo.Update(ctx,
@@ -199,9 +228,9 @@ func TestUpdateInstance(t *testing.T) {
instanceRepo.SetName(newName),
)
afterUpdate := time.Now()
assert.NoError(t, err)
require.NoError(t, err)
assert.Equal(t, tt.rowsAffected, rowsAffected)
require.Equal(t, tt.rowsAffected, rowsAffected)
if rowsAffected == 0 {
return
@@ -211,11 +240,11 @@ func TestUpdateInstance(t *testing.T) {
instance, err = instanceRepo.Get(ctx,
instanceRepo.IDCondition(instance.ID),
)
assert.NoError(t, err)
require.NoError(t, err)
assert.Equal(t, newName, instance.Name)
assert.WithinRange(t, instance.UpdatedAt, beforeUpdate, afterUpdate)
assert.Nil(t, instance.DeletedAt)
require.Equal(t, newName, instance.Name)
require.WithinRange(t, instance.UpdatedAt, beforeUpdate, afterUpdate)
require.Nil(t, instance.DeletedAt)
})
}
}
@@ -224,9 +253,8 @@ func TestGetInstance(t *testing.T) {
instanceRepo := repository.InstanceRepository(pool)
type test struct {
name string
testFunc func() *domain.Instance
testFunc func(ctx context.Context, t *testing.T) *domain.Instance
conditionClauses []database.Condition
noInstanceReturned bool
}
tests := []test{
@@ -234,10 +262,9 @@ func TestGetInstance(t *testing.T) {
instanceId := gofakeit.Name()
return test{
name: "happy path get using id",
testFunc: func() *domain.Instance {
testFunc: func(ctx context.Context, t *testing.T) *domain.Instance {
instanceName := gofakeit.Name()
ctx := context.Background()
inst := domain.Instance{
ID: instanceId,
Name: instanceName,
@@ -250,7 +277,7 @@ func TestGetInstance(t *testing.T) {
// create instance
err := instanceRepo.Create(ctx, &inst)
assert.NoError(t, err)
require.NoError(t, err)
return &inst
},
conditionClauses: []database.Condition{instanceRepo.IDCondition(instanceId)},
@@ -260,10 +287,9 @@ func TestGetInstance(t *testing.T) {
instanceName := gofakeit.Name()
return test{
name: "happy path get using name",
testFunc: func() *domain.Instance {
testFunc: func(ctx context.Context, t *testing.T) *domain.Instance {
instanceId := gofakeit.Name()
ctx := context.Background()
inst := domain.Instance{
ID: instanceId,
Name: instanceName,
@@ -276,7 +302,7 @@ func TestGetInstance(t *testing.T) {
// create instance
err := instanceRepo.Create(ctx, &inst)
assert.NoError(t, err)
require.NoError(t, err)
return &inst
},
conditionClauses: []database.Condition{instanceRepo.NameCondition(database.TextOperationEqual, instanceName)},
@@ -284,16 +310,15 @@ func TestGetInstance(t *testing.T) {
}(),
{
name: "get non existent instance",
testFunc: func() *domain.Instance {
testFunc: func(ctx context.Context, t *testing.T) *domain.Instance {
instanceId := gofakeit.Name()
inst := domain.Instance{
_ = domain.Instance{
ID: instanceId,
}
return &inst
return nil
},
conditionClauses: []database.Condition{instanceRepo.NameCondition(database.TextOperationEqual, "non-existent-instance-name")},
noInstanceReturned: true,
},
}
for _, tt := range tests {
@@ -303,58 +328,55 @@ func TestGetInstance(t *testing.T) {
var instance *domain.Instance
if tt.testFunc != nil {
instance = tt.testFunc()
instance = tt.testFunc(ctx, t)
}
// check instance values
returnedInstance, err := instanceRepo.Get(ctx,
tt.conditionClauses...,
)
assert.NoError(t, err)
if tt.noInstanceReturned {
assert.Nil(t, returnedInstance)
require.NoError(t, err)
if instance == nil {
require.Nil(t, instance, returnedInstance)
return
}
require.NoError(t, err)
assert.Equal(t, returnedInstance.ID, instance.ID)
assert.Equal(t, returnedInstance.Name, instance.Name)
assert.Equal(t, returnedInstance.DefaultOrgID, instance.DefaultOrgID)
assert.Equal(t, returnedInstance.IAMProjectID, instance.IAMProjectID)
assert.Equal(t, returnedInstance.ConsoleClientID, instance.ConsoleClientID)
assert.Equal(t, returnedInstance.ConsoleAppID, instance.ConsoleAppID)
assert.Equal(t, returnedInstance.DefaultLanguage, instance.DefaultLanguage)
assert.NoError(t, err)
require.Equal(t, returnedInstance.ID, instance.ID)
require.Equal(t, returnedInstance.Name, instance.Name)
require.Equal(t, returnedInstance.DefaultOrgID, instance.DefaultOrgID)
require.Equal(t, returnedInstance.IAMProjectID, instance.IAMProjectID)
require.Equal(t, returnedInstance.ConsoleClientID, instance.ConsoleClientID)
require.Equal(t, returnedInstance.ConsoleAppID, instance.ConsoleAppID)
require.Equal(t, returnedInstance.DefaultLanguage, instance.DefaultLanguage)
})
}
}
func TestListInstance(t *testing.T) {
ctx := context.Background()
pool, stop, err := newEmbeddedDB(ctx)
require.NoError(t, err)
defer stop()
type test struct {
name string
testFunc func() ([]*domain.Instance, database.PoolTest, func())
testFunc func(ctx context.Context, t *testing.T) []*domain.Instance
conditionClauses []database.Condition
noInstanceReturned bool
}
tests := []test{
{
name: "happy path single instance no filter",
testFunc: func() ([]*domain.Instance, database.PoolTest, func()) {
ctx := context.Background()
// create new db to make sure no instances exist
pool, stop, err := newEmbeededDB()
assert.NoError(t, err)
testFunc: func(ctx context.Context, t *testing.T) []*domain.Instance {
instanceRepo := repository.InstanceRepository(pool)
noOfInstances := 1
instances := make([]*domain.Instance, noOfInstances)
for i := range noOfInstances {
instanceId := gofakeit.Name()
instanceName := gofakeit.Name()
inst := domain.Instance{
ID: instanceId,
Name: instanceName,
ID: gofakeit.Name(),
Name: gofakeit.Name(),
DefaultOrgID: "defaultOrgId",
IAMProjectID: "iamProject",
ConsoleClientID: "consoleCLient",
@@ -364,33 +386,25 @@ func TestListInstance(t *testing.T) {
// create instance
err := instanceRepo.Create(ctx, &inst)
assert.NoError(t, err)
require.NoError(t, err)
instances[i] = &inst
}
return instances, pool, stop
return instances
},
},
{
name: "happy path multiple instance no filter",
testFunc: func() ([]*domain.Instance, database.PoolTest, func()) {
ctx := context.Background()
// create new db to make sure no instances exist
pool, stop, err := newEmbeededDB()
assert.NoError(t, err)
testFunc: func(ctx context.Context, t *testing.T) []*domain.Instance {
instanceRepo := repository.InstanceRepository(pool)
noOfInstances := 5
instances := make([]*domain.Instance, noOfInstances)
for i := range noOfInstances {
instanceId := gofakeit.Name()
instanceName := gofakeit.Name()
inst := domain.Instance{
ID: instanceId,
Name: instanceName,
ID: gofakeit.Name(),
Name: gofakeit.Name(),
DefaultOrgID: "defaultOrgId",
IAMProjectID: "iamProject",
ConsoleClientID: "consoleCLient",
@@ -400,12 +414,12 @@ func TestListInstance(t *testing.T) {
// create instance
err := instanceRepo.Create(ctx, &inst)
assert.NoError(t, err)
require.NoError(t, err)
instances[i] = &inst
}
return instances, pool, stop
return instances
},
},
func() test {
@@ -413,18 +427,14 @@ func TestListInstance(t *testing.T) {
instanceId := gofakeit.Name()
return test{
name: "instance filter on id",
testFunc: func() ([]*domain.Instance, database.PoolTest, func()) {
ctx := context.Background()
testFunc: func(ctx context.Context, t *testing.T) []*domain.Instance {
noOfInstances := 1
instances := make([]*domain.Instance, noOfInstances)
for i := range noOfInstances {
instanceName := gofakeit.Name()
inst := domain.Instance{
ID: instanceId,
Name: instanceName,
Name: gofakeit.Name(),
DefaultOrgID: "defaultOrgId",
IAMProjectID: "iamProject",
ConsoleClientID: "consoleCLient",
@@ -434,12 +444,12 @@ func TestListInstance(t *testing.T) {
// create instance
err := instanceRepo.Create(ctx, &inst)
assert.NoError(t, err)
require.NoError(t, err)
instances[i] = &inst
}
return instances, nil, nil
return instances
},
conditionClauses: []database.Condition{instanceRepo.IDCondition(instanceId)},
}
@@ -449,17 +459,13 @@ func TestListInstance(t *testing.T) {
instanceName := gofakeit.Name()
return test{
name: "multiple instance filter on name",
testFunc: func() ([]*domain.Instance, database.PoolTest, func()) {
ctx := context.Background()
testFunc: func(ctx context.Context, t *testing.T) []*domain.Instance {
noOfInstances := 5
instances := make([]*domain.Instance, noOfInstances)
for i := range noOfInstances {
instanceId := gofakeit.Name()
inst := domain.Instance{
ID: instanceId,
ID: gofakeit.Name(),
Name: instanceName,
DefaultOrgID: "defaultOrgId",
IAMProjectID: "iamProject",
@@ -470,12 +476,12 @@ func TestListInstance(t *testing.T) {
// create instance
err := instanceRepo.Create(ctx, &inst)
assert.NoError(t, err)
require.NoError(t, err)
instances[i] = &inst
}
return instances, nil, nil
return instances
},
conditionClauses: []database.Condition{instanceRepo.NameCondition(database.TextOperationEqual, instanceName)},
}
@@ -483,42 +489,34 @@ func TestListInstance(t *testing.T) {
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
ctx := context.Background()
t.Cleanup(func() {
_, err := pool.Exec(ctx, "DELETE FROM zitadel.instances")
require.NoError(t, err)
})
var instances []*domain.Instance
instances := tt.testFunc(ctx, t)
pool := pool
if tt.testFunc != nil {
var stop func()
var pool_ database.PoolTest
instances, pool_, stop = tt.testFunc()
if pool_ != nil {
pool = pool_
defer stop()
}
}
instanceRepo := repository.InstanceRepository(pool)
// check instance values
returnedInstances, err := instanceRepo.List(ctx,
tt.conditionClauses...,
)
assert.NoError(t, err)
require.NoError(t, err)
if tt.noInstanceReturned {
assert.Nil(t, returnedInstances)
require.Nil(t, returnedInstances)
return
}
assert.Equal(t, len(instances), len(returnedInstances))
require.Equal(t, len(instances), len(returnedInstances))
for i, instance := range instances {
assert.Equal(t, returnedInstances[i].ID, instance.ID)
assert.Equal(t, returnedInstances[i].Name, instance.Name)
assert.Equal(t, returnedInstances[i].DefaultOrgID, instance.DefaultOrgID)
assert.Equal(t, returnedInstances[i].IAMProjectID, instance.IAMProjectID)
assert.Equal(t, returnedInstances[i].ConsoleClientID, instance.ConsoleClientID)
assert.Equal(t, returnedInstances[i].ConsoleAppID, instance.ConsoleAppID)
assert.Equal(t, returnedInstances[i].DefaultLanguage, instance.DefaultLanguage)
assert.NoError(t, err)
require.Equal(t, returnedInstances[i].ID, instance.ID)
require.Equal(t, returnedInstances[i].Name, instance.Name)
require.Equal(t, returnedInstances[i].DefaultOrgID, instance.DefaultOrgID)
require.Equal(t, returnedInstances[i].IAMProjectID, instance.IAMProjectID)
require.Equal(t, returnedInstances[i].ConsoleClientID, instance.ConsoleClientID)
require.Equal(t, returnedInstances[i].ConsoleAppID, instance.ConsoleAppID)
require.Equal(t, returnedInstances[i].DefaultLanguage, instance.DefaultLanguage)
}
})
}
@@ -527,7 +525,7 @@ func TestListInstance(t *testing.T) {
func TestDeleteInstance(t *testing.T) {
type test struct {
name string
testFunc func()
testFunc func(ctx context.Context, t *testing.T)
conditionClauses database.Condition
}
tests := []test{
@@ -536,18 +534,14 @@ func TestDeleteInstance(t *testing.T) {
instanceId := gofakeit.Name()
return test{
name: "happy path delete single instance filter id",
testFunc: func() {
ctx := context.Background()
testFunc: func(ctx context.Context, t *testing.T) {
noOfInstances := 1
instances := make([]*domain.Instance, noOfInstances)
for i := range noOfInstances {
instanceName := gofakeit.Name()
inst := domain.Instance{
ID: instanceId,
Name: instanceName,
Name: gofakeit.Name(),
DefaultOrgID: "defaultOrgId",
IAMProjectID: "iamProject",
ConsoleClientID: "consoleCLient",
@@ -557,7 +551,7 @@ func TestDeleteInstance(t *testing.T) {
// create instance
err := instanceRepo.Create(ctx, &inst)
assert.NoError(t, err)
require.NoError(t, err)
instances[i] = &inst
}
@@ -570,17 +564,13 @@ func TestDeleteInstance(t *testing.T) {
instanceName := gofakeit.Name()
return test{
name: "happy path delete single instance filter name",
testFunc: func() {
ctx := context.Background()
testFunc: func(ctx context.Context, t *testing.T) {
noOfInstances := 1
instances := make([]*domain.Instance, noOfInstances)
for i := range noOfInstances {
instanceId := gofakeit.Name()
inst := domain.Instance{
ID: instanceId,
ID: gofakeit.Name(),
Name: instanceName,
DefaultOrgID: "defaultOrgId",
IAMProjectID: "iamProject",
@@ -591,7 +581,7 @@ func TestDeleteInstance(t *testing.T) {
// create instance
err := instanceRepo.Create(ctx, &inst)
assert.NoError(t, err)
require.NoError(t, err)
instances[i] = &inst
}
@@ -612,17 +602,13 @@ func TestDeleteInstance(t *testing.T) {
instanceName := gofakeit.Name()
return test{
name: "multiple instance filter on name",
testFunc: func() {
ctx := context.Background()
testFunc: func(ctx context.Context, t *testing.T) {
noOfInstances := 5
instances := make([]*domain.Instance, noOfInstances)
for i := range noOfInstances {
instanceId := gofakeit.Name()
inst := domain.Instance{
ID: instanceId,
ID: gofakeit.Name(),
Name: instanceName,
DefaultOrgID: "defaultOrgId",
IAMProjectID: "iamProject",
@@ -633,7 +619,7 @@ func TestDeleteInstance(t *testing.T) {
// create instance
err := instanceRepo.Create(ctx, &inst)
assert.NoError(t, err)
require.NoError(t, err)
instances[i] = &inst
}
@@ -646,17 +632,13 @@ func TestDeleteInstance(t *testing.T) {
instanceName := gofakeit.Name()
return test{
name: "deleted already deleted instance",
testFunc: func() {
ctx := context.Background()
testFunc: func(ctx context.Context, t *testing.T) {
noOfInstances := 1
instances := make([]*domain.Instance, noOfInstances)
for i := range noOfInstances {
instanceId := gofakeit.Name()
inst := domain.Instance{
ID: instanceId,
ID: gofakeit.Name(),
Name: instanceName,
DefaultOrgID: "defaultOrgId",
IAMProjectID: "iamProject",
@@ -667,7 +649,7 @@ func TestDeleteInstance(t *testing.T) {
// create instance
err := instanceRepo.Create(ctx, &inst)
assert.NoError(t, err)
require.NoError(t, err)
instances[i] = &inst
}
@@ -676,7 +658,7 @@ func TestDeleteInstance(t *testing.T) {
err := instanceRepo.Delete(ctx,
instanceRepo.NameCondition(database.TextOperationEqual, instanceName),
)
assert.NoError(t, err)
require.NoError(t, err)
},
conditionClauses: instanceRepo.NameCondition(database.TextOperationEqual, instanceName),
}
@@ -688,21 +670,21 @@ func TestDeleteInstance(t *testing.T) {
instanceRepo := repository.InstanceRepository(pool)
if tt.testFunc != nil {
tt.testFunc()
tt.testFunc(ctx, t)
}
// delete instance
err := instanceRepo.Delete(ctx,
tt.conditionClauses,
)
assert.NoError(t, err)
require.NoError(t, err)
// check instance was deleted
instance, err := instanceRepo.Get(ctx,
tt.conditionClauses,
)
assert.NoError(t, err)
assert.Nil(t, instance)
require.NoError(t, err)
require.Nil(t, instance)
})
}
}

View File

@@ -3,6 +3,5 @@ package repository
import "github.com/zitadel/zitadel/backend/v3/storage/database"
type repository struct {
// builder database.StatementBuilder
client database.QueryExecutor
}

View File

@@ -20,9 +20,10 @@ var pool database.PoolTest
func runTests(m *testing.M) int {
var stop func()
var err error
pool, stop, err = newEmbeededDB()
ctx := context.Background()
pool, stop, err = newEmbeddedDB(ctx)
if err != nil {
log.Print(err)
log.Printf("error with embedded postgres database: %v", err)
return 1
}
defer stop()
@@ -30,24 +31,21 @@ func runTests(m *testing.M) int {
return m.Run()
}
func newEmbeededDB() (pool database.PoolTest, stop func(), err error) {
var connector database.Connector
connector, stop, err = embedded.StartEmbedded()
func newEmbeddedDB(ctx context.Context) (pool database.PoolTest, stop func(), err error) {
connector, stop, err := embedded.StartEmbedded()
if err != nil {
return nil, nil, fmt.Errorf("unable to start embedded postgres: %v", err)
return nil, nil, fmt.Errorf("unable to start embedded postgres: %w", err)
}
ctx := context.Background()
pool_, err := connector.Connect(ctx)
if err != nil {
return nil, nil, fmt.Errorf("unable to connect to embedded postgres: %v", err)
return nil, nil, fmt.Errorf("unable to connect to embedded postgres: %w", err)
}
pool = pool_.(database.PoolTest)
err = pool.MigrateTest(ctx)
if err != nil {
return nil, nil, fmt.Errorf("unable to migrate database: %v", err)
return nil, nil, fmt.Errorf("unable to migrate database: %w", err)
}
return pool, stop, err
}

View File

@@ -191,18 +191,18 @@ func (h userHuman) PhoneVerifiedAtColumn() database.Column {
return database.NewColumn("phone_verified_at")
}
func (h userHuman) columns() database.Columns {
return append(h.user.columns(),
h.FirstNameColumn(),
h.LastNameColumn(),
h.EmailAddressColumn(),
h.EmailVerifiedAtColumn(),
h.PhoneNumberColumn(),
h.PhoneVerifiedAtColumn(),
)
}
// func (h userHuman) columns() database.Columns {
// return append(h.user.columns(),
// h.FirstNameColumn(),
// h.LastNameColumn(),
// h.EmailAddressColumn(),
// h.EmailVerifiedAtColumn(),
// h.PhoneNumberColumn(),
// h.PhoneVerifiedAtColumn(),
// )
// }
func (h userHuman) writeReturning(builder *database.StatementBuilder) {
builder.WriteString(" RETURNING ")
h.columns().Write(builder)
}
// func (h userHuman) writeReturning(builder *database.StatementBuilder) {
// builder.WriteString(" RETURNING ")
// h.columns().Write(builder)
// }

View File

@@ -5,11 +5,10 @@ package repository_test
// "testing"
// "github.com/stretchr/testify/assert"
// "go.uber.org/mock/gomock"
// "github.com/zitadel/zitadel/backend/v3/storage/database"
// "github.com/zitadel/zitadel/backend/v3/storage/database/dbmock"
// "github.com/zitadel/zitadel/backend/v3/storage/database/repository"
// "go.uber.org/mock/gomock"
// )
// func TestQueryUser(t *testing.T) {
@@ -75,3 +74,4 @@ package repository_test
// user.Human().Update(context.Background(), user.IDCondition("test"), user.SetUsername("test"))
// })
// }

File diff suppressed because one or more lines are too long

View File

@@ -4,6 +4,7 @@ import (
"context"
_ "embed"
"io"
"strconv"
"time"
"github.com/jackc/pgx/v5/stdlib"
@@ -41,12 +42,16 @@ func copyAuth(ctx context.Context, config *Migration) {
logging.OnError(err).Fatal("unable to connect to destination database")
defer destClient.Close()
copyAuthRequests(ctx, sourceClient, destClient)
copyAuthRequests(ctx, sourceClient, destClient, config.MaxAuthRequestAge)
}
func copyAuthRequests(ctx context.Context, source, dest *database.DB) {
func copyAuthRequests(ctx context.Context, source, dest *database.DB, maxAuthRequestAge time.Duration) {
start := time.Now()
logging.Info("creating index on auth.auth_requests.change_date to speed up copy in source database")
_, err := source.ExecContext(ctx, "CREATE INDEX CONCURRENTLY IF NOT EXISTS auth_requests_change_date ON auth.auth_requests (change_date)")
logging.OnError(err).Fatal("unable to create index on auth.auth_requests.change_date")
sourceConn, err := source.Conn(ctx)
logging.OnError(err).Fatal("unable to acquire connection")
defer sourceConn.Close()
@@ -55,9 +60,9 @@ func copyAuthRequests(ctx context.Context, source, dest *database.DB) {
errs := make(chan error, 1)
go func() {
err = sourceConn.Raw(func(driverConn interface{}) error {
err = sourceConn.Raw(func(driverConn any) error {
conn := driverConn.(*stdlib.Conn).Conn()
_, err := conn.PgConn().CopyTo(ctx, w, "COPY (SELECT id, regexp_replace(request::TEXT, '\\\\u0000', '', 'g')::JSON request, code, request_type, creation_date, change_date, instance_id FROM auth.auth_requests "+instanceClause()+") TO STDOUT")
_, err := conn.PgConn().CopyTo(ctx, w, "COPY (SELECT id, regexp_replace(request::TEXT, '\\\\u0000', '', 'g')::JSON request, code, request_type, creation_date, change_date, instance_id FROM auth.auth_requests "+instanceClause()+" AND change_date > NOW() - INTERVAL '"+strconv.FormatFloat(maxAuthRequestAge.Seconds(), 'f', -1, 64)+" seconds') TO STDOUT")
w.Close()
return err
})
@@ -69,7 +74,7 @@ func copyAuthRequests(ctx context.Context, source, dest *database.DB) {
defer destConn.Close()
var affected int64
err = destConn.Raw(func(driverConn interface{}) error {
err = destConn.Raw(func(driverConn any) error {
conn := driverConn.(*stdlib.Conn).Conn()
if shouldReplace {

View File

@@ -24,6 +24,7 @@ type Migration struct {
Destination database.Config
EventBulkSize uint32
MaxAuthRequestAge time.Duration
Log *logging.Config
Machine *id.Config

View File

@@ -1,61 +1,64 @@
Source:
cockroach:
Host: localhost # ZITADEL_DATABASE_COCKROACH_HOST
Port: 26257 # ZITADEL_DATABASE_COCKROACH_PORT
Database: zitadel # ZITADEL_DATABASE_COCKROACH_DATABASE
MaxOpenConns: 6 # ZITADEL_DATABASE_COCKROACH_MAXOPENCONNS
MaxIdleConns: 6 # ZITADEL_DATABASE_COCKROACH_MAXIDLECONNS
MaxConnLifetime: 30m # ZITADEL_DATABASE_COCKROACH_MAXCONNLIFETIME
MaxConnIdleTime: 5m # ZITADEL_DATABASE_COCKROACH_MAXCONNIDLETIME
Options: "" # ZITADEL_DATABASE_COCKROACH_OPTIONS
Host: localhost # ZITADEL_SOURCE_COCKROACH_HOST
Port: 26257 # ZITADEL_SOURCE_COCKROACH_PORT
Database: zitadel # ZITADEL_SOURCE_COCKROACH_DATABASE
MaxOpenConns: 6 # ZITADEL_SOURCE_COCKROACH_MAXOPENCONNS
MaxIdleConns: 6 # ZITADEL_SOURCE_COCKROACH_MAXIDLECONNS
MaxConnLifetime: 30m # ZITADEL_SOURCE_COCKROACH_MAXCONNLIFETIME
MaxConnIdleTime: 5m # ZITADEL_SOURCE_COCKROACH_MAXCONNIDLETIME
Options: "" # ZITADEL_SOURCE_COCKROACH_OPTIONS
User:
Username: zitadel # ZITADEL_DATABASE_COCKROACH_USER_USERNAME
Password: "" # ZITADEL_DATABASE_COCKROACH_USER_PASSWORD
Username: zitadel # ZITADEL_SOURCE_COCKROACH_USER_USERNAME
Password: "" # ZITADEL_SOURCE_COCKROACH_USER_PASSWORD
SSL:
Mode: disable # ZITADEL_DATABASE_COCKROACH_USER_SSL_MODE
RootCert: "" # ZITADEL_DATABASE_COCKROACH_USER_SSL_ROOTCERT
Cert: "" # ZITADEL_DATABASE_COCKROACH_USER_SSL_CERT
Key: "" # ZITADEL_DATABASE_COCKROACH_USER_SSL_KEY
Mode: disable # ZITADEL_SOURCE_COCKROACH_USER_SSL_MODE
RootCert: "" # ZITADEL_SOURCE_COCKROACH_USER_SSL_ROOTCERT
Cert: "" # ZITADEL_SOURCE_COCKROACH_USER_SSL_CERT
Key: "" # ZITADEL_SOURCE_COCKROACH_USER_SSL_KEY
# Postgres is used as soon as a value is set
# The values describe the possible fields to set values
postgres:
Host: # ZITADEL_DATABASE_POSTGRES_HOST
Port: # ZITADEL_DATABASE_POSTGRES_PORT
Database: # ZITADEL_DATABASE_POSTGRES_DATABASE
MaxOpenConns: # ZITADEL_DATABASE_POSTGRES_MAXOPENCONNS
MaxIdleConns: # ZITADEL_DATABASE_POSTGRES_MAXIDLECONNS
MaxConnLifetime: # ZITADEL_DATABASE_POSTGRES_MAXCONNLIFETIME
MaxConnIdleTime: # ZITADEL_DATABASE_POSTGRES_MAXCONNIDLETIME
Options: # ZITADEL_DATABASE_POSTGRES_OPTIONS
Host: # ZITADEL_SOURCE_POSTGRES_HOST
Port: # ZITADEL_SOURCE_POSTGRES_PORT
Database: # ZITADEL_SOURCE_POSTGRES_DATABASE
MaxOpenConns: # ZITADEL_SOURCE_POSTGRES_MAXOPENCONNS
MaxIdleConns: # ZITADEL_SOURCE_POSTGRES_MAXIDLECONNS
MaxConnLifetime: # ZITADEL_SOURCE_POSTGRES_MAXCONNLIFETIME
MaxConnIdleTime: # ZITADEL_SOURCE_POSTGRES_MAXCONNIDLETIME
Options: # ZITADEL_SOURCE_POSTGRES_OPTIONS
User:
Username: # ZITADEL_DATABASE_POSTGRES_USER_USERNAME
Password: # ZITADEL_DATABASE_POSTGRES_USER_PASSWORD
Username: # ZITADEL_SOURCE_POSTGRES_USER_USERNAME
Password: # ZITADEL_SOURCE_POSTGRES_USER_PASSWORD
SSL:
Mode: # ZITADEL_DATABASE_POSTGRES_USER_SSL_MODE
RootCert: # ZITADEL_DATABASE_POSTGRES_USER_SSL_ROOTCERT
Cert: # ZITADEL_DATABASE_POSTGRES_USER_SSL_CERT
Key: # ZITADEL_DATABASE_POSTGRES_USER_SSL_KEY
Mode: # ZITADEL_SOURCE_POSTGRES_USER_SSL_MODE
RootCert: # ZITADEL_SOURCE_POSTGRES_USER_SSL_ROOTCERT
Cert: # ZITADEL_SOURCE_POSTGRES_USER_SSL_CERT
Key: # ZITADEL_SOURCE_POSTGRES_USER_SSL_KEY
Destination:
postgres:
Host: localhost # ZITADEL_DATABASE_POSTGRES_HOST
Port: 5432 # ZITADEL_DATABASE_POSTGRES_PORT
Database: zitadel # ZITADEL_DATABASE_POSTGRES_DATABASE
MaxOpenConns: 5 # ZITADEL_DATABASE_POSTGRES_MAXOPENCONNS
MaxIdleConns: 2 # ZITADEL_DATABASE_POSTGRES_MAXIDLECONNS
MaxConnLifetime: 30m # ZITADEL_DATABASE_POSTGRES_MAXCONNLIFETIME
MaxConnIdleTime: 5m # ZITADEL_DATABASE_POSTGRES_MAXCONNIDLETIME
Options: "" # ZITADEL_DATABASE_POSTGRES_OPTIONS
Host: localhost # ZITADEL_DESTINATION_POSTGRES_HOST
Port: 5432 # ZITADEL_DESTINATION_POSTGRES_PORT
Database: zitadel # ZITADEL_DESTINATION_POSTGRES_DATABASE
MaxOpenConns: 5 # ZITADEL_DESTINATION_POSTGRES_MAXOPENCONNS
MaxIdleConns: 2 # ZITADEL_DESTINATION_POSTGRES_MAXIDLECONNS
MaxConnLifetime: 30m # ZITADEL_DESTINATION_POSTGRES_MAXCONNLIFETIME
MaxConnIdleTime: 5m # ZITADEL_DESTINATION_POSTGRES_MAXCONNIDLETIME
Options: "" # ZITADEL_DESTINATION_POSTGRES_OPTIONS
User:
Username: zitadel # ZITADEL_DATABASE_POSTGRES_USER_USERNAME
Password: "" # ZITADEL_DATABASE_POSTGRES_USER_PASSWORD
Username: zitadel # ZITADEL_DESTINATION_POSTGRES_USER_USERNAME
Password: "" # ZITADEL_DESTINATION_POSTGRES_USER_PASSWORD
SSL:
Mode: disable # ZITADEL_DATABASE_POSTGRES_USER_SSL_MODE
RootCert: "" # ZITADEL_DATABASE_POSTGRES_USER_SSL_ROOTCERT
Cert: "" # ZITADEL_DATABASE_POSTGRES_USER_SSL_CERT
Key: "" # ZITADEL_DATABASE_POSTGRES_USER_SSL_KEY
Mode: disable # ZITADEL_DESTINATION_POSTGRES_USER_SSL_MODE
RootCert: "" # ZITADEL_DESTINATION_POSTGRES_USER_SSL_ROOTCERT
Cert: "" # ZITADEL_DESTINATION_POSTGRES_USER_SSL_CERT
Key: "" # ZITADEL_DESTINATION_POSTGRES_USER_SSL_KEY
EventBulkSize: 10000
EventBulkSize: 10000 # ZITADEL_EVENTBULKSIZE
# The maximum duration an auth request was last updated before it gets ignored.
# Default is 30 days
MaxAuthRequestAge: 720h # ZITADEL_MAXAUTHREQUESTAGE
Projections:
# The maximum duration a transaction remains open
@@ -64,14 +67,14 @@ Projections:
TransactionDuration: 0s # ZITADEL_PROJECTIONS_TRANSACTIONDURATION
# turn off scheduler during operation
RequeueEvery: 0s
ConcurrentInstances: 7
EventBulkLimit: 1000
ConcurrentInstances: 7 # ZITADEL_PROJECTIONS_CONCURRENTINSTANCES
EventBulkLimit: 1000 # ZITADEL_PROJECTIONS_EVENTBULKLIMIT
Customizations:
notifications:
MaxFailureCount: 1
Eventstore:
MaxRetries: 3
MaxRetries: 3 # ZITADEL_EVENTSTORE_MAXRETRIES
Auth:
Spooler:

View File

@@ -3,6 +3,8 @@ package mirror
import (
"context"
"github.com/shopspring/decimal"
"github.com/zitadel/zitadel/internal/v2/eventstore"
"github.com/zitadel/zitadel/internal/v2/readmodel"
"github.com/zitadel/zitadel/internal/v2/system"
@@ -29,7 +31,7 @@ func queryLastSuccessfulMigration(ctx context.Context, destinationES *eventstore
return lastSuccess, nil
}
func writeMigrationSucceeded(ctx context.Context, destinationES *eventstore.EventStore, id, source string, position float64) error {
func writeMigrationSucceeded(ctx context.Context, destinationES *eventstore.EventStore, id, source string, position decimal.Decimal) error {
return destinationES.Push(
ctx,
eventstore.NewPushIntent(

View File

@@ -8,7 +8,9 @@ import (
"io"
"time"
"github.com/jackc/pgx/v5/pgconn"
"github.com/jackc/pgx/v5/stdlib"
"github.com/shopspring/decimal"
"github.com/spf13/cobra"
"github.com/spf13/viper"
"github.com/zitadel/logging"
@@ -69,6 +71,7 @@ func positionQuery(db *db.DB) string {
}
func copyEvents(ctx context.Context, source, dest *db.DB, bulkSize uint32) {
logging.Info("starting to copy events")
start := time.Now()
reader, writer := io.Pipe()
@@ -88,7 +91,7 @@ func copyEvents(ctx context.Context, source, dest *db.DB, bulkSize uint32) {
previousMigration, err := queryLastSuccessfulMigration(ctx, destinationES, source.DatabaseName())
logging.OnError(err).Fatal("unable to query latest successful migration")
var maxPosition float64
var maxPosition decimal.Decimal
err = source.QueryRowContext(ctx,
func(row *sql.Row) error {
return row.Scan(&maxPosition)
@@ -100,7 +103,7 @@ func copyEvents(ctx context.Context, source, dest *db.DB, bulkSize uint32) {
logging.WithFields("from", previousMigration.Position, "to", maxPosition).Info("start event migration")
nextPos := make(chan bool, 1)
pos := make(chan float64, 1)
pos := make(chan decimal.Decimal, 1)
errs := make(chan error, 3)
go func() {
@@ -130,7 +133,10 @@ func copyEvents(ctx context.Context, source, dest *db.DB, bulkSize uint32) {
if err != nil {
return zerrors.ThrowUnknownf(err, "MIGRA-KTuSq", "unable to copy events from source during iteration %d", i)
}
logging.WithFields("batch_count", i).Info("batch of events copied")
if tag.RowsAffected() < int64(bulkSize) {
logging.WithFields("batch_count", i).Info("last batch of events copied")
return nil
}
@@ -148,7 +154,7 @@ func copyEvents(ctx context.Context, source, dest *db.DB, bulkSize uint32) {
go func() {
defer close(pos)
for range nextPos {
var position float64
var position decimal.Decimal
err := dest.QueryRowContext(
ctx,
func(row *sql.Row) error {
@@ -171,6 +177,10 @@ func copyEvents(ctx context.Context, source, dest *db.DB, bulkSize uint32) {
tag, err := conn.PgConn().CopyFrom(ctx, reader, "COPY eventstore.events2 FROM STDIN")
eventCount = tag.RowsAffected()
if err != nil {
pgErr := new(pgconn.PgError)
errors.As(err, &pgErr)
logging.WithError(err).WithField("pg_err_details", pgErr.Detail).Error("unable to copy events into destination")
return zerrors.ThrowUnknown(err, "MIGRA-DTHi7", "unable to copy events into destination")
}
@@ -183,7 +193,7 @@ func copyEvents(ctx context.Context, source, dest *db.DB, bulkSize uint32) {
logging.WithFields("took", time.Since(start), "count", eventCount).Info("events migrated")
}
func writeCopyEventsDone(ctx context.Context, es *eventstore.EventStore, id, source string, position float64, errs <-chan error) {
func writeCopyEventsDone(ctx context.Context, es *eventstore.EventStore, id, source string, position decimal.Decimal, errs <-chan error) {
joinedErrs := make([]error, 0, len(errs))
for err := range errs {
joinedErrs = append(joinedErrs, err)
@@ -202,6 +212,7 @@ func writeCopyEventsDone(ctx context.Context, es *eventstore.EventStore, id, sou
}
func copyUniqueConstraints(ctx context.Context, source, dest *db.DB) {
logging.Info("starting to copy unique constraints")
start := time.Now()
reader, writer := io.Pipe()
errs := make(chan error, 1)

View File

@@ -3,6 +3,7 @@ package mirror
import (
"context"
"database/sql"
"fmt"
"net/http"
"sync"
"time"
@@ -104,6 +105,7 @@ func projections(
config *ProjectionsConfig,
masterKey string,
) {
logging.Info("starting to fill projections")
start := time.Now()
client, err := database.Connect(config.Destination, false)
@@ -255,8 +257,10 @@ func projections(
go execProjections(ctx, instances, failedInstances, &wg)
}
for _, instance := range queryInstanceIDs(ctx, client) {
existingInstances := queryInstanceIDs(ctx, client)
for i, instance := range existingInstances {
instances <- instance
logging.WithFields("id", instance, "index", fmt.Sprintf("%d/%d", i, len(existingInstances))).Info("instance queued for projection")
}
close(instances)
wg.Wait()
@@ -268,7 +272,7 @@ func projections(
func execProjections(ctx context.Context, instances <-chan string, failedInstances chan<- string, wg *sync.WaitGroup) {
for instance := range instances {
logging.WithFields("instance", instance).Info("start projections")
logging.WithFields("instance", instance).Info("starting projections")
ctx = internal_authz.WithInstanceID(ctx, instance)
err := projection.ProjectInstance(ctx)
@@ -292,6 +296,13 @@ func execProjections(ctx context.Context, instances <-chan string, failedInstanc
continue
}
err = projection.ProjectInstanceFields(ctx)
if err != nil {
logging.WithFields("instance", instance).WithError(err).Info("trigger fields failed")
failedInstances <- instance
continue
}
err = auth_handler.ProjectInstance(ctx)
if err != nil {
logging.WithFields("instance", instance).WithError(err).Info("trigger auth handler failed")
@@ -311,7 +322,7 @@ func execProjections(ctx context.Context, instances <-chan string, failedInstanc
wg.Done()
}
// returns the instance configured by flag
// queryInstanceIDs returns the instance configured by flag
// or all instances which are not removed
func queryInstanceIDs(ctx context.Context, source *database.DB) []string {
if len(instanceIDs) > 0 {

View File

@@ -46,6 +46,7 @@ func copySystem(ctx context.Context, config *Migration) {
}
func copyAssets(ctx context.Context, source, dest *database.DB) {
logging.Info("starting to copy assets")
start := time.Now()
sourceConn, err := source.Conn(ctx)
@@ -70,7 +71,7 @@ func copyAssets(ctx context.Context, source, dest *database.DB) {
logging.OnError(err).Fatal("unable to acquire dest connection")
defer destConn.Close()
var eventCount int64
var assetCount int64
err = destConn.Raw(func(driverConn interface{}) error {
conn := driverConn.(*stdlib.Conn).Conn()
@@ -82,16 +83,17 @@ func copyAssets(ctx context.Context, source, dest *database.DB) {
}
tag, err := conn.PgConn().CopyFrom(ctx, r, "COPY system.assets (instance_id, asset_type, resource_owner, name, content_type, data, updated_at) FROM stdin")
eventCount = tag.RowsAffected()
assetCount = tag.RowsAffected()
return err
})
logging.OnError(err).Fatal("unable to copy assets to destination")
logging.OnError(<-errs).Fatal("unable to copy assets from source")
logging.WithFields("took", time.Since(start), "count", eventCount).Info("assets migrated")
logging.WithFields("took", time.Since(start), "count", assetCount).Info("assets migrated")
}
func copyEncryptionKeys(ctx context.Context, source, dest *database.DB) {
logging.Info("starting to copy encryption keys")
start := time.Now()
sourceConn, err := source.Conn(ctx)
@@ -116,7 +118,7 @@ func copyEncryptionKeys(ctx context.Context, source, dest *database.DB) {
logging.OnError(err).Fatal("unable to acquire dest connection")
defer destConn.Close()
var eventCount int64
var keyCount int64
err = destConn.Raw(func(driverConn interface{}) error {
conn := driverConn.(*stdlib.Conn).Conn()
@@ -128,11 +130,11 @@ func copyEncryptionKeys(ctx context.Context, source, dest *database.DB) {
}
tag, err := conn.PgConn().CopyFrom(ctx, r, "COPY system.encryption_keys FROM stdin")
eventCount = tag.RowsAffected()
keyCount = tag.RowsAffected()
return err
})
logging.OnError(err).Fatal("unable to copy encryption keys to destination")
logging.OnError(<-errs).Fatal("unable to copy encryption keys from source")
logging.WithFields("took", time.Since(start), "count", eventCount).Info("encryption keys migrated")
logging.WithFields("took", time.Since(start), "count", keyCount).Info("encryption keys migrated")
}

View File

@@ -15,7 +15,6 @@ var (
type BackChannelLogoutNotificationStart struct {
dbClient *database.DB
esClient *eventstore.Eventstore
}
func (mig *BackChannelLogoutNotificationStart) Execute(ctx context.Context, e eventstore.Event) error {

View File

@@ -4,29 +4,24 @@ import (
"context"
_ "embed"
"github.com/zitadel/zitadel/backend/v3/storage/database/dialect/postgres"
"github.com/zitadel/zitadel/internal/database"
"github.com/zitadel/zitadel/internal/eventstore"
)
type TransactionalTables struct {
var (
//go:embed 54.sql
instancePositionIndex string
)
type InstancePositionIndex struct {
dbClient *database.DB
}
func (mig *TransactionalTables) Execute(ctx context.Context, _ eventstore.Event) error {
config := &postgres.Config{Pool: mig.dbClient.Pool}
pool, err := config.Connect(ctx)
if err != nil {
func (mig *InstancePositionIndex) Execute(ctx context.Context, _ eventstore.Event) error {
_, err := mig.dbClient.ExecContext(ctx, instancePositionIndex)
return err
}
return pool.Migrate(ctx)
}
func (mig *TransactionalTables) String() string {
return "54_repeatable_transactional_tables"
}
func (mig *TransactionalTables) Check(lastRun map[string]interface{}) bool {
return true
func (mig *InstancePositionIndex) String() string {
return "54_instance_position_index_again"
}

1
cmd/setup/54.sql Normal file
View File

@@ -0,0 +1 @@
CREATE INDEX CONCURRENTLY IF NOT EXISTS es_instance_position ON eventstore.events2 (instance_id, position);

27
cmd/setup/55.go Normal file
View File

@@ -0,0 +1,27 @@
package setup
import (
"context"
_ "embed"
"github.com/zitadel/zitadel/internal/database"
"github.com/zitadel/zitadel/internal/eventstore"
)
var (
//go:embed 55.sql
executionHandlerCurrentState string
)
type ExecutionHandlerStart struct {
dbClient *database.DB
}
func (mig *ExecutionHandlerStart) Execute(ctx context.Context, e eventstore.Event) error {
_, err := mig.dbClient.ExecContext(ctx, executionHandlerCurrentState, e.Sequence(), e.CreatedAt(), e.Position())
return err
}
func (mig *ExecutionHandlerStart) String() string {
return "55_execution_handler_start"
}

22
cmd/setup/55.sql Normal file
View File

@@ -0,0 +1,22 @@
INSERT INTO projections.current_states AS cs ( instance_id
, projection_name
, last_updated
, sequence
, event_date
, position
, filter_offset)
SELECT instance_id
, 'projections.execution_handler'
, now()
, $1
, $2
, $3
, 0
FROM eventstore.events2 AS e
WHERE aggregate_type = 'instance'
AND event_type = 'instance.added'
ON CONFLICT (instance_id, projection_name) DO UPDATE SET last_updated = EXCLUDED.last_updated,
sequence = EXCLUDED.sequence,
event_date = EXCLUDED.event_date,
position = EXCLUDED.position,
filter_offset = EXCLUDED.filter_offset;

27
cmd/setup/56.go Normal file
View File

@@ -0,0 +1,27 @@
package setup
import (
"context"
_ "embed"
"github.com/zitadel/zitadel/internal/database"
"github.com/zitadel/zitadel/internal/eventstore"
)
var (
//go:embed 56.sql
addSAMLFederatedLogout string
)
type IDPTemplate6SAMLFederatedLogout struct {
dbClient *database.DB
}
func (mig *IDPTemplate6SAMLFederatedLogout) Execute(ctx context.Context, _ eventstore.Event) error {
_, err := mig.dbClient.ExecContext(ctx, addSAMLFederatedLogout)
return err
}
func (mig *IDPTemplate6SAMLFederatedLogout) String() string {
return "56_idp_templates6_add_saml_federated_logout"
}

1
cmd/setup/56.sql Normal file
View File

@@ -0,0 +1 @@
ALTER TABLE IF EXISTS projections.idp_templates6_saml ADD COLUMN IF NOT EXISTS federated_logout_enabled BOOLEAN DEFAULT FALSE;

27
cmd/setup/57.go Normal file
View File

@@ -0,0 +1,27 @@
package setup
import (
"context"
_ "embed"
"github.com/zitadel/zitadel/internal/database"
"github.com/zitadel/zitadel/internal/eventstore"
)
var (
//go:embed 57.sql
createResourceCounts string
)
type CreateResourceCounts struct {
dbClient *database.DB
}
func (mig *CreateResourceCounts) Execute(ctx context.Context, _ eventstore.Event) error {
_, err := mig.dbClient.ExecContext(ctx, createResourceCounts)
return err
}
func (mig *CreateResourceCounts) String() string {
return "57_create_resource_counts"
}

106
cmd/setup/57.sql Normal file
View File

@@ -0,0 +1,106 @@
CREATE TABLE IF NOT EXISTS projections.resource_counts
(
id SERIAL PRIMARY KEY, -- allows for easy pagination
instance_id TEXT NOT NULL,
table_name TEXT NOT NULL, -- needed for trigger matching, not in reports
parent_type TEXT NOT NULL,
parent_id TEXT NOT NULL,
resource_name TEXT NOT NULL, -- friendly name for reporting
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
amount INTEGER NOT NULL DEFAULT 1 CHECK (amount >= 0),
UNIQUE (instance_id, parent_type, parent_id, table_name)
);
-- count_resource is a trigger function which increases or decreases the count of a resource.
-- When creating the trigger the following required arguments (TG_ARGV) can be passed:
-- 1. The type of the parent
-- 2. The column name of the instance id
-- 3. The column name of the owner id
-- 4. The name of the resource
CREATE OR REPLACE FUNCTION projections.count_resource()
RETURNS trigger
LANGUAGE 'plpgsql' VOLATILE
AS $$
DECLARE
-- trigger variables
tg_table_name TEXT := TG_TABLE_SCHEMA || '.' || TG_TABLE_NAME;
tg_parent_type TEXT := TG_ARGV[0];
tg_instance_id_column TEXT := TG_ARGV[1];
tg_parent_id_column TEXT := TG_ARGV[2];
tg_resource_name TEXT := TG_ARGV[3];
tg_instance_id TEXT;
tg_parent_id TEXT;
select_ids TEXT := format('SELECT ($1).%I, ($1).%I', tg_instance_id_column, tg_parent_id_column);
BEGIN
IF (TG_OP = 'INSERT') THEN
EXECUTE select_ids INTO tg_instance_id, tg_parent_id USING NEW;
INSERT INTO projections.resource_counts(instance_id, table_name, parent_type, parent_id, resource_name)
VALUES (tg_instance_id, tg_table_name, tg_parent_type, tg_parent_id, tg_resource_name)
ON CONFLICT (instance_id, table_name, parent_type, parent_id) DO
UPDATE SET updated_at = now(), amount = projections.resource_counts.amount + 1;
RETURN NEW;
ELSEIF (TG_OP = 'DELETE') THEN
EXECUTE select_ids INTO tg_instance_id, tg_parent_id USING OLD;
UPDATE projections.resource_counts
SET updated_at = now(), amount = amount - 1
WHERE instance_id = tg_instance_id
AND table_name = tg_table_name
AND parent_type = tg_parent_type
AND parent_id = tg_parent_id
AND resource_name = tg_resource_name
AND amount > 0; -- prevent check failure on negative amount.
RETURN OLD;
END IF;
END
$$;
-- delete_table_counts removes all resource counts for a TRUNCATED table.
CREATE OR REPLACE FUNCTION projections.delete_table_counts()
RETURNS trigger
LANGUAGE 'plpgsql'
AS $$
DECLARE
-- trigger variables
tg_table_name TEXT := TG_TABLE_SCHEMA || '.' || TG_TABLE_NAME;
BEGIN
DELETE FROM projections.resource_counts
WHERE table_name = tg_table_name;
END
$$;
-- delete_parent_counts removes all resource counts for a deleted parent.
-- 1. The type of the parent
-- 2. The column name of the instance id
-- 3. The column name of the owner id
CREATE OR REPLACE FUNCTION projections.delete_parent_counts()
RETURNS trigger
LANGUAGE 'plpgsql'
AS $$
DECLARE
-- trigger variables
tg_parent_type TEXT := TG_ARGV[0];
tg_instance_id_column TEXT := TG_ARGV[1];
tg_parent_id_column TEXT := TG_ARGV[2];
tg_instance_id TEXT;
tg_parent_id TEXT;
select_ids TEXT := format('SELECT ($1).%I, ($1).%I', tg_instance_id_column, tg_parent_id_column);
BEGIN
EXECUTE select_ids INTO tg_instance_id, tg_parent_id USING OLD;
DELETE FROM projections.resource_counts
WHERE instance_id = tg_instance_id
AND parent_type = tg_parent_type
AND parent_id = tg_parent_id;
RETURN OLD;
END
$$;

49
cmd/setup/58.go Normal file
View File

@@ -0,0 +1,49 @@
package setup
import (
"context"
"database/sql"
"embed"
"fmt"
"github.com/zitadel/logging"
"github.com/zitadel/zitadel/internal/database"
"github.com/zitadel/zitadel/internal/eventstore"
)
var (
//go:embed 58/*.sql
replaceLoginNames3View embed.FS
)
type ReplaceLoginNames3View struct {
dbClient *database.DB
}
func (mig *ReplaceLoginNames3View) Execute(ctx context.Context, _ eventstore.Event) error {
var exists bool
err := mig.dbClient.QueryRowContext(ctx, func(r *sql.Row) error {
return r.Scan(&exists)
}, "SELECT exists(SELECT 1 from information_schema.views WHERE table_schema = 'projections' AND table_name = 'login_names3')")
if err != nil || !exists {
return err
}
statements, err := readStatements(replaceLoginNames3View, "58")
if err != nil {
return err
}
for _, stmt := range statements {
logging.WithFields("file", stmt.file, "migration", mig.String()).Info("execute statement")
if _, err := mig.dbClient.ExecContext(ctx, stmt.query); err != nil {
return fmt.Errorf("%s %s: %w", mig.String(), stmt.file, err)
}
}
return nil
}
func (mig *ReplaceLoginNames3View) String() string {
return "58_replace_login_names3_view"
}

View File

@@ -0,0 +1,36 @@
CREATE OR REPLACE VIEW projections.login_names3 AS
SELECT
u.id AS user_id
, CASE
WHEN p.must_be_domain THEN CONCAT(u.user_name, '@', d.name)
ELSE u.user_name
END AS login_name
, COALESCE(d.is_primary, TRUE) AS is_primary
, u.instance_id
FROM
projections.login_names3_users AS u
LEFT JOIN LATERAL (
SELECT
must_be_domain
, is_default
FROM
projections.login_names3_policies AS p
WHERE
(
p.instance_id = u.instance_id
AND NOT p.is_default
AND p.resource_owner = u.resource_owner
) OR (
p.instance_id = u.instance_id
AND p.is_default
)
ORDER BY
p.is_default -- custom first
LIMIT 1
) AS p ON TRUE
LEFT JOIN
projections.login_names3_domains d
ON
p.must_be_domain
AND u.resource_owner = d.resource_owner
AND u.instance_id = d.instance_id

View File

@@ -0,0 +1 @@
CREATE INDEX CONCURRENTLY IF NOT EXISTS login_names3_policies_is_default_owner_idx ON projections.login_names3_policies (instance_id, is_default, resource_owner) INCLUDE (must_be_domain)

32
cmd/setup/59.go Normal file
View File

@@ -0,0 +1,32 @@
package setup
import (
"context"
_ "embed"
"github.com/zitadel/zitadel/backend/v3/storage/database/dialect/postgres"
"github.com/zitadel/zitadel/internal/database"
"github.com/zitadel/zitadel/internal/eventstore"
)
type TransactionalTables struct {
dbClient *database.DB
}
func (mig *TransactionalTables) Execute(ctx context.Context, _ eventstore.Event) error {
config := &postgres.Config{Pool: mig.dbClient.Pool}
pool, err := config.Connect(ctx)
if err != nil {
return err
}
return pool.Migrate(ctx)
}
func (mig *TransactionalTables) String() string {
return "59_repeatable_transactional_tables"
}
func (mig *TransactionalTables) Check(lastRun map[string]interface{}) bool {
return true
}

View File

@@ -150,6 +150,11 @@ type Steps struct {
s51IDPTemplate6RootCA *IDPTemplate6RootCA
s52IDPTemplate6LDAP2 *IDPTemplate6LDAP2
s53InitPermittedOrgsFunction *InitPermittedOrgsFunction53
s54InstancePositionIndex *InstancePositionIndex
s55ExecutionHandlerStart *ExecutionHandlerStart
s56IDPTemplate6SAMLFederatedLogout *IDPTemplate6SAMLFederatedLogout
s57CreateResourceCounts *CreateResourceCounts
s58ReplaceLoginNames3View *ReplaceLoginNames3View
}
func MustNewSteps(v *viper.Viper) *Steps {

View File

@@ -198,7 +198,7 @@ func Setup(ctx context.Context, config *Config, steps *Steps, masterKey string)
steps.s35AddPositionToIndexEsWm = &AddPositionToIndexEsWm{dbClient: dbClient}
steps.s36FillV2Milestones = &FillV3Milestones{dbClient: dbClient, eventstore: eventstoreClient}
steps.s37Apps7OIDConfigsBackChannelLogoutURI = &Apps7OIDConfigsBackChannelLogoutURI{dbClient: dbClient}
steps.s38BackChannelLogoutNotificationStart = &BackChannelLogoutNotificationStart{dbClient: dbClient, esClient: eventstoreClient}
steps.s38BackChannelLogoutNotificationStart = &BackChannelLogoutNotificationStart{dbClient: dbClient}
steps.s40InitPushFunc = &InitPushFunc{dbClient: dbClient}
steps.s42Apps7OIDCConfigsLoginVersion = &Apps7OIDCConfigsLoginVersion{dbClient: dbClient}
steps.s43CreateFieldsDomainIndex = &CreateFieldsDomainIndex{dbClient: dbClient}
@@ -212,6 +212,11 @@ func Setup(ctx context.Context, config *Config, steps *Steps, masterKey string)
steps.s51IDPTemplate6RootCA = &IDPTemplate6RootCA{dbClient: dbClient}
steps.s52IDPTemplate6LDAP2 = &IDPTemplate6LDAP2{dbClient: dbClient}
steps.s53InitPermittedOrgsFunction = &InitPermittedOrgsFunction53{dbClient: dbClient}
steps.s54InstancePositionIndex = &InstancePositionIndex{dbClient: dbClient}
steps.s55ExecutionHandlerStart = &ExecutionHandlerStart{dbClient: dbClient}
steps.s56IDPTemplate6SAMLFederatedLogout = &IDPTemplate6SAMLFederatedLogout{dbClient: dbClient}
steps.s57CreateResourceCounts = &CreateResourceCounts{dbClient: dbClient}
steps.s58ReplaceLoginNames3View = &ReplaceLoginNames3View{dbClient: dbClient}
err = projection.Create(ctx, dbClient, eventstoreClient, config.Projections, nil, nil, nil)
logging.OnError(err).Fatal("unable to start projections")
@@ -254,6 +259,11 @@ func Setup(ctx context.Context, config *Config, steps *Steps, masterKey string)
steps.s51IDPTemplate6RootCA,
steps.s52IDPTemplate6LDAP2,
steps.s53InitPermittedOrgsFunction,
steps.s54InstancePositionIndex,
steps.s55ExecutionHandlerStart,
steps.s56IDPTemplate6SAMLFederatedLogout,
steps.s57CreateResourceCounts,
steps.s58ReplaceLoginNames3View,
} {
setupErr = executeMigration(ctx, eventstoreClient, step, "migration failed")
if setupErr != nil {
@@ -293,6 +303,7 @@ func Setup(ctx context.Context, config *Config, steps *Steps, masterKey string)
client: dbClient,
},
}
repeatableSteps = append(repeatableSteps, triggerSteps(dbClient)...)
for _, repeatableStep := range repeatableSteps {
setupErr = executeMigration(ctx, eventstoreClient, repeatableStep, "unable to migrate repeatable step")

125
cmd/setup/trigger_steps.go Normal file
View File

@@ -0,0 +1,125 @@
package setup
import (
"fmt"
"github.com/zitadel/zitadel/internal/database"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/migration"
"github.com/zitadel/zitadel/internal/query/projection"
)
// triggerSteps defines the repeatable migrations that set up triggers
// for counting resources in the database.
func triggerSteps(db *database.DB) []migration.RepeatableMigration {
return []migration.RepeatableMigration{
// Delete parent count triggers for instances and organizations
migration.DeleteParentCountsTrigger(db,
projection.InstanceProjectionTable,
domain.CountParentTypeInstance,
projection.InstanceColumnID,
projection.InstanceColumnID,
"instance",
),
migration.DeleteParentCountsTrigger(db,
projection.OrgProjectionTable,
domain.CountParentTypeOrganization,
projection.OrgColumnInstanceID,
projection.OrgColumnID,
"organization",
),
// Count triggers for all the resources
migration.CountTrigger(db,
projection.OrgProjectionTable,
domain.CountParentTypeInstance,
projection.OrgColumnInstanceID,
projection.OrgColumnInstanceID,
"organization",
),
migration.CountTrigger(db,
projection.ProjectProjectionTable,
domain.CountParentTypeOrganization,
projection.ProjectColumnInstanceID,
projection.ProjectColumnResourceOwner,
"project",
),
migration.CountTrigger(db,
projection.UserTable,
domain.CountParentTypeOrganization,
projection.UserInstanceIDCol,
projection.UserResourceOwnerCol,
"user",
),
migration.CountTrigger(db,
projection.InstanceMemberProjectionTable,
domain.CountParentTypeInstance,
projection.MemberInstanceID,
projection.MemberResourceOwner,
"iam_admin",
),
migration.CountTrigger(db,
projection.IDPTable,
domain.CountParentTypeInstance,
projection.IDPInstanceIDCol,
projection.IDPInstanceIDCol,
"identity_provider",
),
migration.CountTrigger(db,
projection.IDPTemplateLDAPTable,
domain.CountParentTypeInstance,
projection.LDAPInstanceIDCol,
projection.LDAPInstanceIDCol,
"identity_provider_ldap",
),
migration.CountTrigger(db,
projection.ActionTable,
domain.CountParentTypeInstance,
projection.ActionInstanceIDCol,
projection.ActionInstanceIDCol,
"action_v1",
),
migration.CountTrigger(db,
projection.ExecutionTable,
domain.CountParentTypeInstance,
projection.ExecutionInstanceIDCol,
projection.ExecutionInstanceIDCol,
"execution",
),
migration.CountTrigger(db,
fmt.Sprintf("%s_%s", projection.ExecutionTable, projection.ExecutionTargetSuffix),
domain.CountParentTypeInstance,
projection.ExecutionTargetInstanceIDCol,
projection.ExecutionTargetInstanceIDCol,
"execution_target",
),
migration.CountTrigger(db,
projection.LoginPolicyTable,
domain.CountParentTypeInstance,
projection.LoginPolicyInstanceIDCol,
projection.LoginPolicyInstanceIDCol,
"login_policy",
),
migration.CountTrigger(db,
projection.PasswordComplexityTable,
domain.CountParentTypeInstance,
projection.ComplexityPolicyInstanceIDCol,
projection.ComplexityPolicyInstanceIDCol,
"password_complexity_policy",
),
migration.CountTrigger(db,
projection.PasswordAgeTable,
domain.CountParentTypeInstance,
projection.AgePolicyInstanceIDCol,
projection.AgePolicyInstanceIDCol,
"password_expiry_policy",
),
migration.CountTrigger(db,
projection.LockoutPolicyTable,
domain.CountParentTypeInstance,
projection.LockoutPolicyInstanceIDCol,
projection.LockoutPolicyInstanceIDCol,
"lockout_policy",
),
}
}

View File

@@ -40,11 +40,13 @@ import (
feature_v2 "github.com/zitadel/zitadel/internal/api/grpc/feature/v2"
feature_v2beta "github.com/zitadel/zitadel/internal/api/grpc/feature/v2beta"
idp_v2 "github.com/zitadel/zitadel/internal/api/grpc/idp/v2"
instance "github.com/zitadel/zitadel/internal/api/grpc/instance/v2beta"
"github.com/zitadel/zitadel/internal/api/grpc/management"
oidc_v2 "github.com/zitadel/zitadel/internal/api/grpc/oidc/v2"
oidc_v2beta "github.com/zitadel/zitadel/internal/api/grpc/oidc/v2beta"
org_v2 "github.com/zitadel/zitadel/internal/api/grpc/org/v2"
org_v2beta "github.com/zitadel/zitadel/internal/api/grpc/org/v2beta"
project_v2beta "github.com/zitadel/zitadel/internal/api/grpc/project/v2beta"
"github.com/zitadel/zitadel/internal/api/grpc/resources/debug_events/debug_events"
user_v3_alpha "github.com/zitadel/zitadel/internal/api/grpc/resources/user/v3alpha"
userschema_v3_alpha "github.com/zitadel/zitadel/internal/api/grpc/resources/userschema/v3alpha"
@@ -72,12 +74,14 @@ import (
"github.com/zitadel/zitadel/internal/authz"
authz_repo "github.com/zitadel/zitadel/internal/authz/repository"
authz_es "github.com/zitadel/zitadel/internal/authz/repository/eventsourcing/eventstore"
"github.com/zitadel/zitadel/internal/cache"
"github.com/zitadel/zitadel/internal/cache/connector"
"github.com/zitadel/zitadel/internal/command"
"github.com/zitadel/zitadel/internal/crypto"
cryptoDB "github.com/zitadel/zitadel/internal/crypto/database"
"github.com/zitadel/zitadel/internal/database"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/domain/federatedlogout"
"github.com/zitadel/zitadel/internal/eventstore"
old_es "github.com/zitadel/zitadel/internal/eventstore/repository/sql"
new_es "github.com/zitadel/zitadel/internal/eventstore/v3"
@@ -304,7 +308,7 @@ func startZitadel(ctx context.Context, config *Config, masterKey string, server
execution.Register(
ctx,
config.Projections.Customizations["executions"],
config.Projections.Customizations["execution_handler"],
config.Executions,
queries,
eventstoreClient.EventTypes(),
@@ -442,6 +446,9 @@ func startAPIs(
if err := apis.RegisterServer(ctx, system.CreateServer(commands, queries, config.Database.DatabaseName(), config.DefaultInstance, config.ExternalDomain), tlsConfig); err != nil {
return nil, err
}
if err := apis.RegisterService(ctx, instance.CreateServer(commands, queries, config.Database.DatabaseName(), config.DefaultInstance, config.ExternalDomain)); err != nil {
return nil, err
}
if err := apis.RegisterServer(ctx, admin.CreateServer(config.Database.DatabaseName(), commands, queries, keys.User, config.AuditLogRetention), tlsConfig); err != nil {
return nil, err
}
@@ -454,7 +461,7 @@ func startAPIs(
if err := apis.RegisterService(ctx, user_v2beta.CreateServer(commands, queries, keys.User, keys.IDPConfig, idp.CallbackURL(), idp.SAMLRootURL(), assets.AssetAPI(), permissionCheck)); err != nil {
return nil, err
}
if err := apis.RegisterService(ctx, user_v2.CreateServer(commands, queries, keys.User, keys.IDPConfig, idp.CallbackURL(), idp.SAMLRootURL(), assets.AssetAPI(), permissionCheck)); err != nil {
if err := apis.RegisterService(ctx, user_v2.CreateServer(config.SystemDefaults, commands, queries, keys.User, keys.IDPConfig, idp.CallbackURL(), idp.SAMLRootURL(), assets.AssetAPI(), permissionCheck)); err != nil {
return nil, err
}
if err := apis.RegisterService(ctx, session_v2beta.CreateServer(commands, queries, permissionCheck)); err != nil {
@@ -463,7 +470,7 @@ func startAPIs(
if err := apis.RegisterService(ctx, settings_v2beta.CreateServer(commands, queries)); err != nil {
return nil, err
}
if err := apis.RegisterService(ctx, org_v2beta.CreateServer(commands, queries, permissionCheck)); err != nil {
if err := apis.RegisterService(ctx, org_v2beta.CreateServer(config.SystemDefaults, commands, queries, permissionCheck)); err != nil {
return nil, err
}
if err := apis.RegisterService(ctx, feature_v2beta.CreateServer(commands, queries)); err != nil {
@@ -487,6 +494,9 @@ func startAPIs(
if err := apis.RegisterService(ctx, action_v2_beta.CreateServer(config.SystemDefaults, commands, queries, domain.AllActionFunctions, apis.ListGrpcMethods, apis.ListGrpcServices)); err != nil {
return nil, err
}
if err := apis.RegisterService(ctx, project_v2beta.CreateServer(config.SystemDefaults, commands, queries, permissionCheck)); err != nil {
return nil, err
}
if err := apis.RegisterService(ctx, userschema_v3_alpha.CreateServer(config.SystemDefaults, commands, queries)); err != nil {
return nil, err
}
@@ -503,7 +513,12 @@ func startAPIs(
assetsCache := middleware.AssetsCacheInterceptor(config.AssetStorage.Cache.MaxAge, config.AssetStorage.Cache.SharedMaxAge)
apis.RegisterHandlerOnPrefix(assets.HandlerPrefix, assets.NewHandler(commands, verifier, config.SystemAuthZ, config.InternalAuthZ, id.SonyFlakeGenerator(), store, queries, middleware.CallDurationHandler, instanceInterceptor.Handler, assetsCache.Handler, limitingAccessInterceptor.Handle))
apis.RegisterHandlerOnPrefix(idp.HandlerPrefix, idp.NewHandler(commands, queries, keys.IDPConfig, instanceInterceptor.Handler))
federatedLogoutsCache, err := connector.StartCache[federatedlogout.Index, string, *federatedlogout.FederatedLogout](ctx, []federatedlogout.Index{federatedlogout.IndexRequestID}, cache.PurposeFederatedLogout, cacheConnectors.Config.FederatedLogouts, cacheConnectors)
if err != nil {
return nil, err
}
apis.RegisterHandlerOnPrefix(idp.HandlerPrefix, idp.NewHandler(commands, queries, keys.IDPConfig, instanceInterceptor.Handler, federatedLogoutsCache))
userAgentInterceptor, err := middleware.NewUserAgentHandler(config.UserAgentCookie, keys.UserAgentCookieKey, id.SonyFlakeGenerator(), config.ExternalSecure, login.EndpointResources, login.EndpointExternalLoginCallbackFormPost, login.EndpointSAMLACS)
if err != nil {
@@ -524,7 +539,25 @@ func startAPIs(
}
apis.RegisterHandlerOnPrefix(openapi.HandlerPrefix, openAPIHandler)
oidcServer, err := oidc.NewServer(ctx, config.OIDC, login.DefaultLoggedOutPath, config.ExternalSecure, commands, queries, authRepo, keys.OIDC, keys.OIDCKey, eventstore, dbClient, userAgentInterceptor, instanceInterceptor.Handler, limitingAccessInterceptor, config.Log.Slog(), config.SystemDefaults.SecretHasher)
oidcServer, err := oidc.NewServer(
ctx,
config.OIDC,
login.DefaultLoggedOutPath,
config.ExternalSecure,
commands,
queries,
authRepo,
keys.OIDC,
keys.OIDCKey,
eventstore,
dbClient,
userAgentInterceptor,
instanceInterceptor.Handler,
limitingAccessInterceptor,
config.Log.Slog(),
config.SystemDefaults.SecretHasher,
federatedLogoutsCache,
)
if err != nil {
return nil, fmt.Errorf("unable to start oidc provider: %w", err)
}
@@ -573,6 +606,7 @@ func startAPIs(
keys.IDPConfig,
keys.CSRFCookieKey,
cacheConnectors,
federatedLogoutsCache,
)
if err != nil {
return nil, fmt.Errorf("unable to start login: %w", err)

View File

@@ -1,6 +1,8 @@
package start
import (
"context"
"github.com/spf13/cobra"
"github.com/spf13/viper"
"github.com/zitadel/logging"
@@ -29,14 +31,19 @@ Requirements:
masterKey, err := key.MasterKey(cmd)
logging.OnError(err).Panic("No master key provided")
initialise.InitAll(cmd.Context(), initialise.MustNewConfig(viper.GetViper()))
initCtx, cancel := context.WithCancel(cmd.Context())
initialise.InitAll(initCtx, initialise.MustNewConfig(viper.GetViper()))
cancel()
err = setup.BindInitProjections(cmd)
logging.OnError(err).Fatal("unable to bind \"init-projections\" flag")
setupConfig := setup.MustNewConfig(viper.GetViper())
setupSteps := setup.MustNewSteps(viper.New())
setup.Setup(cmd.Context(), setupConfig, setupSteps, masterKey)
setupCtx, cancel := context.WithCancel(cmd.Context())
setup.Setup(setupCtx, setupConfig, setupSteps, masterKey)
cancel()
startConfig := MustNewConfig(viper.GetViper())

View File

@@ -1,6 +1,8 @@
package start
import (
"context"
"github.com/spf13/cobra"
"github.com/spf13/viper"
"github.com/zitadel/logging"
@@ -34,7 +36,10 @@ Requirements:
setupConfig := setup.MustNewConfig(viper.GetViper())
setupSteps := setup.MustNewSteps(viper.New())
setup.Setup(cmd.Context(), setupConfig, setupSteps, masterKey)
setupCtx, cancel := context.WithCancel(cmd.Context())
setup.Setup(setupCtx, setupConfig, setupSteps, masterKey)
cancel()
startConfig := MustNewConfig(viper.GetViper())

View File

@@ -31,8 +31,8 @@
"@fortawesome/fontawesome-svg-core": "^6.7.2",
"@fortawesome/free-brands-svg-icons": "^6.7.2",
"@ngx-translate/core": "^15.0.0",
"@zitadel/client": "^1.0.7",
"@zitadel/proto": "1.0.5-sha-4118a9d",
"@zitadel/client": "1.2.0",
"@zitadel/proto": "1.2.0",
"angular-oauth2-oidc": "^15.0.1",
"angularx-qrcode": "^16.0.2",
"buffer": "^6.0.3",
@@ -82,6 +82,7 @@
"jasmine-spec-reporter": "~7.0.0",
"karma": "^6.4.4",
"karma-chrome-launcher": "^3.2.0",
"karma-coverage": "^2.2.1",
"karma-coverage-istanbul-reporter": "^3.0.3",
"karma-jasmine": "^5.1.0",
"karma-jasmine-html-reporter": "^2.1.0",

View File

@@ -1,16 +1,16 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { QuickstartComponent } from './quickstart.component';
import { OIDCConfigurationComponent } from './oidc-configuration.component';
describe('QuickstartComponent', () => {
let component: QuickstartComponent;
let fixture: ComponentFixture<QuickstartComponent>;
let component: OIDCConfigurationComponent;
let fixture: ComponentFixture<OIDCConfigurationComponent>;
beforeEach(() => {
TestBed.configureTestingModule({
declarations: [QuickstartComponent],
declarations: [OIDCConfigurationComponent],
});
fixture = TestBed.createComponent(QuickstartComponent);
fixture = TestBed.createComponent(OIDCConfigurationComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});

View File

@@ -24,8 +24,8 @@
<th mat-header-cell *matHeaderCellDef>{{ 'ACTIONSTWO.EXECUTION.TABLE.TARGET' | translate }}</th>
<td mat-cell *cnslCellDef="let row; dataSource: dataSource">
<div class="target-key">
<cnsl-project-role-chip *ngFor="let target of row.mappedTargets; trackBy: trackTarget" [roleName]="target.name"
>{{ target.name }}
<cnsl-project-role-chip *ngFor="let target of row.mappedTargets; trackBy: trackTarget" [roleName]="target.name">
{{ target.name }}
</cnsl-project-role-chip>
</div>
</td>

View File

@@ -55,13 +55,9 @@ export class ActionsTwoActionsTableComponent {
}
return executions.map((execution) => {
const mappedTargets = execution.targets.map((target) => {
const targetType = targetsMap.get(target.type.value);
if (!targetType) {
throw new Error(`Target with id ${target.type.value} not found`);
}
return targetType;
});
const mappedTargets = execution.targets
.map((target) => targetsMap.get(target))
.filter((target): target is NonNullable<typeof target> => !!target);
return { execution, mappedTargets };
});
});

View File

@@ -1,4 +1,7 @@
<h2>{{ 'ACTIONSTWO.EXECUTION.TITLE' | translate }}</h2>
<cnsl-info-section [type]="InfoSectionType.ALERT">
{{ 'ACTIONSTWO.BETA_NOTE' | translate }}
</cnsl-info-section>
<p class="cnsl-secondary-text">{{ 'ACTIONSTWO.EXECUTION.DESCRIPTION' | translate }}</p>
<cnsl-actions-two-actions-table

View File

@@ -15,6 +15,8 @@ import { MatDialog } from '@angular/material/dialog';
import { MessageInitShape } from '@bufbuild/protobuf';
import { SetExecutionRequestSchema } from '@zitadel/proto/zitadel/action/v2beta/action_service_pb';
import { Target } from '@zitadel/proto/zitadel/action/v2beta/target_pb';
import { InfoSectionType } from '../../info-section/info-section.component';
import { ExecutionFieldName } from '@zitadel/proto/zitadel/action/v2beta/query_pb';
@Component({
selector: 'cnsl-actions-two-actions',
@@ -41,7 +43,7 @@ export class ActionsTwoActionsComponent {
return this.refresh$.pipe(
startWith(true),
switchMap(() => {
return this.actionService.listExecutions({});
return this.actionService.listExecutions({ sortingColumn: ExecutionFieldName.ID, pagination: { asc: true } });
}),
map(({ result }) => result.map(correctlyTypeExecution)),
catchError((err) => {
@@ -110,4 +112,6 @@ export class ActionsTwoActionsComponent {
this.toast.showError(error);
}
}
protected readonly InfoSectionType = InfoSectionType;
}

View File

@@ -84,7 +84,7 @@
<div class="execution-condition-text">
<span>{{ 'ACTIONSTWO.EXECUTION.DIALOG.CONDITION.ALL.TITLE' | translate }}</span>
<span class="description cnsl-secondary-text">{{
'ACTIONSTWO.EXECUTION.DIALOG.CONDITION.ALL.DESCRIPTION' | translate
'ACTIONSTWO.EXECUTION.DIALOG.CONDITION.ALL_EVENTS' | translate
}}</span>
</div>
</mat-checkbox>

View File

@@ -10,12 +10,7 @@ import {
} from './actions-two-add-action-condition/actions-two-add-action-condition.component';
import { ActionsTwoAddActionTargetComponent } from './actions-two-add-action-target/actions-two-add-action-target.component';
import { CommonModule } from '@angular/common';
import {
Condition,
Execution,
ExecutionTargetType,
ExecutionTargetTypeSchema,
} from '@zitadel/proto/zitadel/action/v2beta/execution_pb';
import { Condition, Execution } from '@zitadel/proto/zitadel/action/v2beta/execution_pb';
import { Subject } from 'rxjs';
import { SetExecutionRequestSchema } from '@zitadel/proto/zitadel/action/v2beta/action_service_pb';
@@ -27,11 +22,8 @@ enum Page {
export type CorrectlyTypedCondition = Condition & { conditionType: Extract<Condition['conditionType'], { case: string }> };
type CorrectlyTypedTargets = { type: Extract<ExecutionTargetType['type'], { case: 'target' }> };
export type CorrectlyTypedExecution = Omit<Execution, 'targets' | 'condition'> & {
export type CorrectlyTypedExecution = Omit<Execution, 'condition'> & {
condition: CorrectlyTypedCondition;
targets: CorrectlyTypedTargets[];
};
export const correctlyTypeExecution = (execution: Execution): CorrectlyTypedExecution => {
@@ -48,9 +40,6 @@ export const correctlyTypeExecution = (execution: Execution): CorrectlyTypedExec
return {
...execution,
condition,
targets: execution.targets
.map(({ type }) => ({ type }))
.filter((target): target is CorrectlyTypedTargets => target.type.case === 'target'),
};
};
@@ -81,7 +70,7 @@ export class ActionTwoAddActionDialogComponent {
protected readonly typeSignal = signal<ConditionType>('request');
protected readonly conditionSignal = signal<MessageInitShape<typeof SetExecutionRequestSchema>['condition']>(undefined);
protected readonly targetsSignal = signal<MessageInitShape<typeof ExecutionTargetTypeSchema>[]>([]);
protected readonly targetsSignal = signal<string[]>([]);
protected readonly continueSubject = new Subject<void>();
@@ -112,7 +101,7 @@ export class ActionTwoAddActionDialogComponent {
this.targetsSignal.set(data.execution.targets);
this.typeSignal.set(data.execution.condition.conditionType.case);
this.conditionSignal.set(data.execution.condition);
this.preselectedTargetIds = data.execution.targets.map((target) => target.type.value);
this.preselectedTargetIds = data.execution.targets;
this.page.set(Page.Target); // Set the initial page based on the provided execution data
}

View File

@@ -1,4 +1,4 @@
<form class="form-grid" [formGroup]="form()" (ngSubmit)="submit()">
<form *ngIf="form$ | async as form" class="form-grid" [formGroup]="form" (ngSubmit)="submit()">
<p class="target-description">{{ 'ACTIONSTWO.EXECUTION.DIALOG.TARGET.DESCRIPTION' | translate }}</p>
<cnsl-form-field class="full-width">
@@ -8,9 +8,9 @@
#trigger="matAutocompleteTrigger"
#input
type="text"
[formControl]="form().controls.autocomplete"
[formControl]="form.controls.autocomplete"
[matAutocomplete]="autoservice"
(keydown.enter)="handleEnter($event); input.blur(); trigger.closePanel()"
(keydown.enter)="handleEnter($event, form); input.blur(); trigger.closePanel()"
/>
<mat-autocomplete #autoservice="matAutocomplete">
<mat-option *ngIf="targets().state === 'loading'" class="is-loading">
@@ -19,7 +19,7 @@
<mat-option
*ngFor="let target of selectableTargets(); trackBy: trackTarget"
#option
(click)="addTarget(target); option.deselect()"
(click)="addTarget(target, form); option.deselect()"
[value]="target.name"
>
{{ target.name }}
@@ -27,7 +27,7 @@
</mat-autocomplete>
</cnsl-form-field>
<table mat-table cdkDropList (cdkDropListDropped)="drop($event)" [dataSource]="dataSource" [trackBy]="trackTarget">
<table mat-table cdkDropList (cdkDropListDropped)="drop($event, form)" [dataSource]="dataSource" [trackBy]="trackTarget">
<ng-container matColumnDef="order">
<th mat-header-cell *matHeaderCellDef>Reorder</th>
<td mat-cell *cnslCellDef="let row; let i = index; dataSource: dataSource">
@@ -48,7 +48,7 @@
actions
matTooltip="{{ 'ACTIONS.REMOVE' | translate }}"
color="warn"
(click)="removeTarget(i)"
(click)="removeTarget(i, form)"
mat-icon-button
>
<i class="las la-trash"></i>
@@ -65,7 +65,7 @@
{{ 'ACTIONS.BACK' | translate }}
</button>
<span class="fill-space"></span>
<button color="primary" [disabled]="form().invalid" mat-raised-button type="submit">
<button color="primary" [disabled]="form.invalid" mat-raised-button type="submit">
{{ 'ACTIONS.CONTINUE' | translate }}
</button>
</div>

View File

@@ -14,7 +14,7 @@ import { RouterModule } from '@angular/router';
import { TranslateModule } from '@ngx-translate/core';
import { MatButtonModule } from '@angular/material/button';
import { FormBuilder, FormControl, FormsModule, ReactiveFormsModule } from '@angular/forms';
import { ReplaySubject, switchMap } from 'rxjs';
import { ObservedValueOf, ReplaySubject, shareReplay, switchMap } from 'rxjs';
import { MatRadioModule } from '@angular/material/radio';
import { ActionService } from 'src/app/services/action.service';
import { ToastService } from 'src/app/services/toast.service';
@@ -23,14 +23,13 @@ import { InputModule } from 'src/app/modules/input/input.module';
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
import { MessageInitShape } from '@bufbuild/protobuf';
import { Target } from '@zitadel/proto/zitadel/action/v2beta/target_pb';
import { ExecutionTargetTypeSchema } from '@zitadel/proto/zitadel/action/v2beta/execution_pb';
import { MatSelectModule } from '@angular/material/select';
import { ActionConditionPipeModule } from 'src/app/pipes/action-condition-pipe/action-condition-pipe.module';
import { MatTableDataSource, MatTableModule } from '@angular/material/table';
import { startWith } from 'rxjs/operators';
import { map, startWith } from 'rxjs/operators';
import { TypeSafeCellDefModule } from 'src/app/directives/type-safe-cell-def/type-safe-cell-def.module';
import { CdkDrag, CdkDragDrop, CdkDropList, moveItemInArray } from '@angular/cdk/drag-drop';
import { toObservable, toSignal } from '@angular/core/rxjs-interop';
import { toSignal } from '@angular/core/rxjs-interop';
import { minArrayLengthValidator } from '../../../form-field/validators/validators';
import { ProjectRoleChipModule } from '../../../project-role-chip/project-role-chip.module';
import { MatTooltipModule } from '@angular/material/tooltip';
@@ -72,11 +71,12 @@ export class ActionsTwoAddActionTargetComponent {
}
@Output() public readonly back = new EventEmitter<void>();
@Output() public readonly continue = new EventEmitter<MessageInitShape<typeof ExecutionTargetTypeSchema>[]>();
@Output() public readonly continue = new EventEmitter<string[]>();
private readonly preselectedTargetIds$ = new ReplaySubject<string[]>(1);
protected readonly form: ReturnType<typeof this.buildForm>;
protected readonly form$: ReturnType<typeof this.buildForm>;
protected readonly targets: ReturnType<typeof this.listTargets>;
private readonly selectedTargetIds: Signal<string[]>;
protected readonly selectableTargets: Signal<Target[]>;
@@ -87,26 +87,27 @@ export class ActionsTwoAddActionTargetComponent {
private readonly actionService: ActionService,
private readonly toast: ToastService,
) {
this.form = this.buildForm();
this.form$ = this.buildForm().pipe(shareReplay({ refCount: true, bufferSize: 1 }));
this.targets = this.listTargets();
this.selectedTargetIds = this.getSelectedTargetIds(this.form);
this.selectableTargets = this.getSelectableTargets(this.targets, this.selectedTargetIds);
this.selectedTargetIds = this.getSelectedTargetIds(this.form$);
this.selectableTargets = this.getSelectableTargets(this.targets, this.selectedTargetIds, this.form$);
this.dataSource = this.getDataSource(this.targets, this.selectedTargetIds);
}
private buildForm() {
const preselectedTargetIds = toSignal(this.preselectedTargetIds$, { initialValue: [] as string[] });
return computed(() => {
return this.preselectedTargetIds$.pipe(
startWith([] as string[]),
map((preselectedTargetIds) => {
return this.fb.group({
autocomplete: new FormControl('', { nonNullable: true }),
selectedTargetIds: new FormControl(preselectedTargetIds(), {
selectedTargetIds: new FormControl(preselectedTargetIds, {
nonNullable: true,
validators: [minArrayLengthValidator(1)],
}),
});
});
}),
);
}
private listTargets() {
@@ -129,25 +130,35 @@ export class ActionsTwoAddActionTargetComponent {
return computed(targetsSignal);
}
private getSelectedTargetIds(form: typeof this.form) {
const selectedTargetIds$ = toObservable(form).pipe(
startWith(form()),
switchMap((form) => {
const { selectedTargetIds } = form.controls;
private getSelectedTargetIds(form$: typeof this.form$) {
const selectedTargetIds$ = form$.pipe(
switchMap(({ controls: { selectedTargetIds } }) => {
return selectedTargetIds.valueChanges.pipe(startWith(selectedTargetIds.value));
}),
);
return toSignal(selectedTargetIds$, { requireSync: true });
}
private getSelectableTargets(targets: typeof this.targets, selectedTargetIds: Signal<string[]>) {
return computed(() => {
private getSelectableTargets(targets: typeof this.targets, selectedTargetIds: Signal<string[]>, form$: typeof this.form$) {
const autocomplete$ = form$.pipe(
switchMap(({ controls: { autocomplete } }) => {
return autocomplete.valueChanges.pipe(startWith(autocomplete.value));
}),
);
const autocompleteSignal = toSignal(autocomplete$, { requireSync: true });
const unselectedTargets = computed(() => {
const targetsCopy = new Map(targets().targets);
for (const selectedTargetId of selectedTargetIds()) {
targetsCopy.delete(selectedTargetId);
}
return Array.from(targetsCopy.values());
});
return computed(() => {
const autocomplete = autocompleteSignal().toLowerCase();
return unselectedTargets().filter(({ name }) => name.toLowerCase().includes(autocomplete));
});
}
private getDataSource(targetsSignal: typeof this.targets, selectedTargetIdsSignal: Signal<string[]>) {
@@ -178,46 +189,39 @@ export class ActionsTwoAddActionTargetComponent {
return dataSource;
}
protected addTarget(target: Target) {
const { selectedTargetIds } = this.form().controls;
protected addTarget(target: Target, form: ObservedValueOf<typeof this.form$>) {
const { selectedTargetIds } = form.controls;
selectedTargetIds.setValue([target.id, ...selectedTargetIds.value]);
this.form().controls.autocomplete.setValue('');
form.controls.autocomplete.setValue('');
}
protected removeTarget(index: number) {
const { selectedTargetIds } = this.form().controls;
protected removeTarget(index: number, form: ObservedValueOf<typeof this.form$>) {
const { selectedTargetIds } = form.controls;
const data = [...selectedTargetIds.value];
data.splice(index, 1);
selectedTargetIds.setValue(data);
}
protected drop(event: CdkDragDrop<undefined>) {
const { selectedTargetIds } = this.form().controls;
protected drop(event: CdkDragDrop<undefined>, form: ObservedValueOf<typeof this.form$>) {
const { selectedTargetIds } = form.controls;
const data = [...selectedTargetIds.value];
moveItemInArray(data, event.previousIndex, event.currentIndex);
selectedTargetIds.setValue(data);
}
protected handleEnter(event: Event) {
protected handleEnter(event: Event, form: ObservedValueOf<typeof this.form$>) {
const selectableTargets = this.selectableTargets();
if (selectableTargets.length !== 1) {
return;
}
event.preventDefault();
this.addTarget(selectableTargets[0]);
this.addTarget(selectableTargets[0], form);
}
protected submit() {
const selectedTargets = this.selectedTargetIds().map((value) => ({
type: {
case: 'target' as const,
value,
},
}));
this.continue.emit(selectedTargets);
this.continue.emit(this.selectedTargetIds());
}
protected trackTarget(_: number, target: Target) {

View File

@@ -26,6 +26,9 @@
{{ 'ACTIONSTWO.TARGET.CREATE.TYPES.' + type | translate }}
</mat-option>
</mat-select>
<span class="name-hint cnsl-secondary-text types-description">
{{ 'ACTIONSTWO.TARGET.CREATE.TYPES_DESCRIPTION' | translate }}
</span>
</cnsl-form-field>
<cnsl-form-field class="full-width">

View File

@@ -23,3 +23,7 @@
.name-hint {
font-size: 12px;
}
.types-description {
white-space: pre-line;
}

View File

@@ -1,4 +1,7 @@
<h2>{{ 'ACTIONSTWO.TARGET.TITLE' | translate }}</h2>
<cnsl-info-section [type]="InfoSectionType.ALERT">
{{ 'ACTIONSTWO.BETA_NOTE' | translate }}
</cnsl-info-section>
<p class="cnsl-secondary-text">{{ 'ACTIONSTWO.TARGET.DESCRIPTION' | translate }}</p>
<cnsl-actions-two-targets-table

View File

@@ -12,6 +12,7 @@ import {
CreateTargetRequestSchema,
UpdateTargetRequestSchema,
} from '@zitadel/proto/zitadel/action/v2beta/action_service_pb';
import { InfoSectionType } from '../../info-section/info-section.component';
@Component({
selector: 'cnsl-actions-two-targets',
@@ -76,7 +77,8 @@ export class ActionsTwoTargetsComponent {
if ('id' in request) {
await this.actionService.updateTarget(request);
} else {
await this.actionService.createTarget(request);
const resp = await this.actionService.createTarget(request);
console.log(`Your singing key: ${resp.signingKey}`);
}
await new Promise((res) => setTimeout(res, 1000));
@@ -86,4 +88,6 @@ export class ActionsTwoTargetsComponent {
this.toast.showError(error);
}
}
protected readonly InfoSectionType = InfoSectionType;
}

View File

@@ -20,6 +20,7 @@ import { ProjectRoleChipModule } from '../project-role-chip/project-role-chip.mo
import { ActionConditionPipeModule } from 'src/app/pipes/action-condition-pipe/action-condition-pipe.module';
import { MatSelectModule } from '@angular/material/select';
import { MatIconModule } from '@angular/material/icon';
import { InfoSectionModule } from '../info-section/info-section.module';
@NgModule({
declarations: [
@@ -47,6 +48,7 @@ import { MatIconModule } from '@angular/material/icon';
TypeSafeCellDefModule,
ProjectRoleChipModule,
ActionConditionPipeModule,
InfoSectionModule,
],
exports: [ActionsTwoActionsComponent, ActionsTwoTargetsComponent, ActionsTwoTargetsTableComponent],
})

View File

@@ -1,19 +1,19 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { OrgDomainsComponent } from './org-domains.component';
import { DomainsComponent } from './domains.component';
describe('OrgDomainsComponent', () => {
let component: OrgDomainsComponent;
let fixture: ComponentFixture<OrgDomainsComponent>;
let component: DomainsComponent;
let fixture: ComponentFixture<DomainsComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [OrgDomainsComponent],
declarations: [DomainsComponent],
}).compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(OrgDomainsComponent);
fixture = TestBed.createComponent(DomainsComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});

View File

@@ -69,4 +69,19 @@
</cnsl-form-field>
</div>
</div>
<div class="id-query">
<mat-checkbox id="id" class="cb" [checked]="getSubFilter(SubQuery.ID)" (change)="changeCheckbox(SubQuery.ID, $event)"
>{{ 'FILTER.ORGID' | translate }}
</mat-checkbox>
<div class="subquery" *ngIf="getSubFilter(SubQuery.ID) as idq">
<span class="nomethod cnsl-secondary-text">
{{ 'FILTER.METHODS.1' | translate }}
</span>
<cnsl-form-field class="filter-input-value">
<input cnslInput name="value" [value]="idq.getId()" (change)="setValue(SubQuery.ID, idq, $event)" />
</cnsl-form-field>
</div>
</div>
</cnsl-filter>

View File

@@ -3,7 +3,14 @@ import { MatCheckboxChange } from '@angular/material/checkbox';
import { ActivatedRoute, Router } from '@angular/router';
import { take } from 'rxjs';
import { TextQueryMethod } from 'src/app/proto/generated/zitadel/object_pb';
import { OrgDomainQuery, OrgNameQuery, OrgQuery, OrgState, OrgStateQuery } from 'src/app/proto/generated/zitadel/org_pb';
import {
OrgDomainQuery,
OrgNameQuery,
OrgQuery,
OrgState,
OrgStateQuery,
OrgIDQuery,
} from 'src/app/proto/generated/zitadel/org_pb';
import { UserNameQuery } from 'src/app/proto/generated/zitadel/user_pb';
import { FilterComponent } from '../filter/filter.component';
@@ -12,6 +19,7 @@ enum SubQuery {
NAME,
STATE,
DOMAIN,
ID,
}
@Component({
@@ -61,6 +69,12 @@ export class FilterOrgComponent extends FilterComponent implements OnInit {
orgDomainQuery.setMethod(filter.domainQuery.method);
orgQuery.setDomainQuery(orgDomainQuery);
return orgQuery;
} else if (filter.idQuery) {
const orgQuery = new OrgQuery();
const orgIdQuery = new OrgIDQuery();
orgIdQuery.setId(filter.idQuery.id);
orgQuery.setIdQuery(orgIdQuery);
return orgQuery;
} else {
return undefined;
}
@@ -100,6 +114,13 @@ export class FilterOrgComponent extends FilterComponent implements OnInit {
odq.setDomainQuery(dq);
this.searchQueries.push(odq);
break;
case SubQuery.ID:
const idq = new OrgIDQuery();
idq.setId('');
const oidq = new OrgQuery();
oidq.setIdQuery(idq);
this.searchQueries.push(oidq);
break;
}
} else {
switch (subquery) {
@@ -121,6 +142,12 @@ export class FilterOrgComponent extends FilterComponent implements OnInit {
this.searchQueries.splice(index_pdn, 1);
}
break;
case SubQuery.ID:
const index_id = this.searchQueries.findIndex((q) => (q as OrgQuery).toObject().idQuery !== undefined);
if (index_id > -1) {
this.searchQueries.splice(index_id, 1);
}
break;
}
}
}
@@ -140,6 +167,10 @@ export class FilterOrgComponent extends FilterComponent implements OnInit {
(query as OrgDomainQuery).setDomain(value);
this.filterChanged.emit(this.searchQueries ? this.searchQueries : []);
break;
case SubQuery.ID:
(query as OrgIDQuery).setId(value);
this.filterChanged.emit(this.searchQueries ? this.searchQueries : []);
break;
}
}
@@ -166,6 +197,13 @@ export class FilterOrgComponent extends FilterComponent implements OnInit {
} else {
return undefined;
}
case SubQuery.ID:
const id = this.searchQueries.find((q) => (q as OrgQuery).toObject().idQuery !== undefined);
if (id) {
return (id as OrgQuery).getIdQuery();
} else {
return undefined;
}
}
}

View File

@@ -1,19 +1,19 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { FilterUserComponent } from './filter-user.component';
import { FilterProjectComponent } from './filter-project.component';
describe('FilterUserComponent', () => {
let component: FilterUserComponent;
let fixture: ComponentFixture<FilterUserComponent>;
let component: FilterProjectComponent;
let fixture: ComponentFixture<FilterProjectComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [FilterUserComponent],
declarations: [FilterProjectComponent],
}).compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(FilterUserComponent);
fixture = TestBed.createComponent(FilterProjectComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});

View File

@@ -2,7 +2,7 @@
<div class="info-wrapper">
<p class="info-row-title">{{ 'USER.PAGES.STATE' | translate }}</p>
<p
*ngIf="user && user.state !== undefined"
*ngIf="user?.state"
class="state"
[ngClass]="{
active: user.state === UserState.USER_STATE_ACTIVE,
@@ -53,7 +53,7 @@
<div class="info-wrapper">
<p class="info-row-title">{{ 'IAM.PAGES.STATE' | translate }}</p>
<p
*ngIf="instance && instance.state !== undefined"
*ngIf="instance?.state"
class="state"
[ngClass]="{
active: instance.state === State.INSTANCE_STATE_RUNNING,
@@ -66,17 +66,17 @@
<div class="info-wrapper">
<p class="info-row-title">{{ 'RESOURCEID' | translate }}</p>
<p *ngIf="instance && instance.id" class="info-row-desc">{{ instance.id }}</p>
<p *ngIf="instance?.id" class="info-row-desc">{{ instance.id }}</p>
</div>
<div class="info-wrapper">
<p class="info-row-title">{{ 'NAME' | translate }}</p>
<p *ngIf="instance && instance.name" class="info-row-desc">{{ instance.name }}</p>
<p *ngIf="instance?.name" class="info-row-desc">{{ instance.name }}</p>
</div>
<div class="info-wrapper">
<p class="info-row-title">{{ 'VERSION' | translate }}</p>
<p *ngIf="instance && instance.version" class="info-row-desc">{{ instance.version }}</p>
<p *ngIf="instance?.version" class="info-row-desc">{{ instance.version }}</p>
</div>
<div class="info-wrapper width">
@@ -96,15 +96,15 @@
<div class="info-wrapper">
<p class="info-row-title">{{ 'ORG.PAGES.CREATIONDATE' | translate }}</p>
<p *ngIf="instance && instance.details && instance.details.creationDate" class="info-row-desc">
{{ instance.details.creationDate | timestampToDate | localizedDate: 'dd. MMMM YYYY, HH:mm' }}
<p *ngIf="instance?.details?.creationDate as creationDate" class="info-row-desc">
{{ creationDate | timestampToDate | localizedDate: 'dd. MMMM YYYY, HH:mm' }}
</p>
</div>
<div class="info-wrapper">
<p class="info-row-title">{{ 'ORG.PAGES.DATECHANGED' | translate }}</p>
<p *ngIf="instance && instance.details && instance.details.changeDate" class="info-row-desc">
{{ instance.details.changeDate | timestampToDate | localizedDate: 'dd. MMMM YYYY, HH:mm' }}
<p *ngIf="instance?.details?.changeDate as changeDate" class="info-row-desc">
{{ changeDate | timestampToDate | localizedDate: 'dd. MMMM YYYY, HH:mm' }}
</p>
</div>
</div>
@@ -113,7 +113,7 @@
<div class="info-wrapper">
<p class="info-row-title">{{ 'ORG.PAGES.STATE' | translate }}</p>
<p
*ngIf="org && org.state !== undefined"
*ngIf="org?.state"
class="state"
[ngClass]="{ active: org.state === OrgState.ORG_STATE_ACTIVE, inactive: org.state === OrgState.ORG_STATE_INACTIVE }"
>
@@ -123,7 +123,7 @@
<div class="info-wrapper">
<p class="info-row-title">{{ 'RESOURCEID' | translate }}</p>
<p *ngIf="org && org.id" class="info-row-desc">{{ org.id }}</p>
<p *ngIf="org?.id" class="info-row-desc">{{ org.id }}</p>
</div>
<div class="info-wrapper width">
@@ -143,15 +143,15 @@
<div class="info-wrapper">
<p class="info-row-title">{{ 'ORG.PAGES.CREATIONDATE' | translate }}</p>
<p *ngIf="org && org.details && org.details.creationDate" class="info-row-desc">
{{ org.details.creationDate | timestampToDate | localizedDate: 'dd. MMMM YYYY, HH:mm' }}
<p *ngIf="org?.details?.creationDate as creationDate" class="info-row-desc">
{{ creationDate | timestampToDate | localizedDate: 'dd. MMMM YYYY, HH:mm' }}
</p>
</div>
<div class="info-wrapper">
<p class="info-row-title">{{ 'ORG.PAGES.DATECHANGED' | translate }}</p>
<p *ngIf="org && org.details && org.details.changeDate" class="info-row-desc">
{{ org.details.changeDate | timestampToDate | localizedDate: 'dd. MMMM YYYY, HH:mm' }}
<p *ngIf="org?.details?.changeDate as changeDate" class="info-row-desc">
{{ changeDate | timestampToDate | localizedDate: 'dd. MMMM YYYY, HH:mm' }}
</p>
</div>
</div>
@@ -160,7 +160,7 @@
<div class="info-wrapper">
<p class="info-row-title">{{ 'PROJECT.STATE.TITLE' | translate }}</p>
<p
*ngIf="project && project.state !== undefined"
*ngIf="project?.state"
class="state"
[ngClass]="{
active: project.state === ProjectState.PROJECT_STATE_ACTIVE,
@@ -173,20 +173,20 @@
<div class="info-wrapper">
<p class="info-row-title">{{ 'RESOURCEID' | translate }}</p>
<p *ngIf="project && project.id" class="info-row-desc">{{ project.id }}</p>
<p *ngIf="project?.id" class="info-row-desc">{{ project.id }}</p>
</div>
<div class="info-wrapper">
<p class="info-row-title">{{ 'PROJECT.PAGES.CREATEDON' | translate }}</p>
<p *ngIf="project && project.details && project.details.creationDate" class="info-row-desc">
{{ project.details.creationDate | timestampToDate | localizedDate: 'dd. MMMM YYYY, HH:mm' }}
<p *ngIf="project?.details?.creationDate as creationDate" class="info-row-desc">
{{ creationDate | timestampToDate | localizedDate: 'dd. MMMM YYYY, HH:mm' }}
</p>
</div>
<div class="info-wrapper">
<p class="info-row-title">{{ 'PROJECT.PAGES.LASTMODIFIED' | translate }}</p>
<p *ngIf="project && project.details && project.details.changeDate" class="info-row-desc">
{{ project.details.changeDate | timestampToDate | localizedDate: 'dd. MMMM YYYY, HH:mm' }}
<p *ngIf="project?.details?.changeDate as changeDate" class="info-row-desc">
{{ changeDate | timestampToDate | localizedDate: 'dd. MMMM YYYY, HH:mm' }}
</p>
</div>
</div>
@@ -195,7 +195,7 @@
<div class="info-wrapper">
<p class="info-row-title">{{ 'PROJECT.STATE.TITLE' | translate }}</p>
<p
*ngIf="grantedProject && grantedProject.state !== undefined"
*ngIf="grantedProject?.state"
class="state"
[ngClass]="{
active: grantedProject.state === ProjectGrantState.PROJECT_GRANT_STATE_ACTIVE,
@@ -208,25 +208,25 @@
<div class="info-wrapper">
<p class="info-row-title">{{ 'RESOURCEID' | translate }}</p>
<p *ngIf="grantedProject && grantedProject.projectId" class="info-row-desc">{{ grantedProject.projectId }}</p>
<p *ngIf="grantedProject?.projectId" class="info-row-desc">{{ grantedProject.projectId }}</p>
</div>
<div class="info-wrapper">
<p class="info-row-title">{{ 'PROJECT.GRANT.GRANTID' | translate }}</p>
<p *ngIf="grantedProject && grantedProject.grantId" class="info-row-desc">{{ grantedProject.grantId }}</p>
<p *ngIf="grantedProject?.grantId" class="info-row-desc">{{ grantedProject.grantId }}</p>
</div>
<div class="info-wrapper">
<p class="info-row-title">{{ 'PROJECT.PAGES.CREATEDON' | translate }}</p>
<p *ngIf="grantedProject && grantedProject.details && grantedProject.details.creationDate" class="info-row-desc">
{{ grantedProject.details.creationDate | timestampToDate | localizedDate: 'dd. MMMM YYYY, HH:mm' }}
<p *ngIf="grantedProject?.details?.creationDate as creationDate" class="info-row-desc">
{{ creationDate | timestampToDate | localizedDate: 'dd. MMMM YYYY, HH:mm' }}
</p>
</div>
<div class="info-wrapper">
<p class="info-row-title">{{ 'PROJECT.PAGES.LASTMODIFIED' | translate }}</p>
<p *ngIf="grantedProject && grantedProject.details && grantedProject.details.changeDate" class="info-row-desc">
{{ grantedProject.details.changeDate | timestampToDate | localizedDate: 'dd. MMMM YYYY, HH:mm' }}
<p *ngIf="grantedProject?.details?.changeDate as changeDate" class="info-row-desc">
{{ changeDate | timestampToDate | localizedDate: 'dd. MMMM YYYY, HH:mm' }}
</p>
</div>
</div>
@@ -236,30 +236,43 @@
<div class="info-wrapper">
<p class="info-row-title">{{ 'APP.PAGES.STATE' | translate }}</p>
<p
*ngIf="app && app.state !== undefined"
*ngIf="app?.state"
class="state"
[ngClass]="{ active: app.state === AppState.APP_STATE_ACTIVE, inactive: app.state === AppState.APP_STATE_INACTIVE }"
>
{{ 'APP.PAGES.DETAIL.STATE.' + app.state | translate }}
</p>
</div>
<div class="info-wrapper" *ngIf="app?.apiConfig?.authMethodType as authMethodType">
<p class="info-row-title">{{ 'APP.AUTHMETHOD' | translate }}</p>
<p class="info-row-desc">
{{ 'APP.API.AUTHMETHOD.' + authMethodType | translate }}
</p>
</div>
<div class="info-wrapper" *ngIf="app?.oidcConfig?.authMethodType as authMethodType">
<p class="info-row-title">{{ 'APP.AUTHMETHOD' | translate }}</p>
<p class="info-row-desc">
{{ 'APP.OIDC.AUTHMETHOD.' + authMethodType | translate }}
</p>
</div>
<div class="info-wrapper">
<p class="info-row-title">{{ 'APP.PAGES.ID' | translate }}</p>
<p *ngIf="app && app.id" class="info-row-desc">{{ app.id }}</p>
<p *ngIf="app?.id" class="info-row-desc">{{ app.id }}</p>
</div>
<div class="info-wrapper">
<p class="info-row-title">{{ 'APP.PAGES.DATECREATED' | translate }}</p>
<p *ngIf="app && app.details && app.details.creationDate" class="info-row-desc">
{{ app.details.creationDate | timestampToDate | localizedDate: 'dd. MMMM YYYY, HH:mm' }}
<p *ngIf="app?.details?.creationDate as creationDate" class="info-row-desc">
{{ creationDate | timestampToDate | localizedDate: 'dd. MMMM YYYY, HH:mm' }}
</p>
</div>
<div class="info-wrapper">
<p class="info-row-title">{{ 'APP.PAGES.DATECHANGED' | translate }}</p>
<p *ngIf="app && app.details && app.details.changeDate" class="info-row-desc">
{{ app.details.changeDate | timestampToDate | localizedDate: 'dd. MMMM YYYY, HH:mm' }}
<p *ngIf="app?.details?.changeDate as changeDate" class="info-row-desc">
{{ changeDate | timestampToDate | localizedDate: 'dd. MMMM YYYY, HH:mm' }}
</p>
</div>
@@ -267,27 +280,27 @@
<p class="info-row-title">{{ 'APP.OIDC.INFO.CLIENTID' | translate }}</p>
<div class="copy-row" *ngIf="app.oidcConfig?.clientId">
<button
*ngIf="app.oidcConfig && app.oidcConfig?.clientId"
[disabled]="copied === app.oidcConfig.clientId"
[matTooltip]="(copied !== app.oidcConfig.clientId ? 'ACTIONS.COPY' : 'ACTIONS.COPIED') | translate"
*ngIf="app.oidcConfig?.clientId as clientId"
[disabled]="copied === clientId"
[matTooltip]="(copied !== clientId ? 'ACTIONS.COPY' : 'ACTIONS.COPIED') | translate"
cnslCopyToClipboard
[valueToCopy]="app.oidcConfig.clientId"
[valueToCopy]="clientId"
(copiedValue)="copied = $event"
>
{{ app.oidcConfig.clientId }}
{{ clientId }}
</button>
</div>
<div class="copy-row" *ngIf="app.apiConfig?.clientId">
<button
*ngIf="app && app.apiConfig && app.apiConfig.clientId"
[disabled]="copied === app.apiConfig.clientId"
[matTooltip]="(copied !== app.apiConfig.clientId ? 'ACTIONS.COPY' : 'ACTIONS.COPIED') | translate"
*ngIf="app.apiConfig?.clientId as clientId"
[disabled]="copied === clientId"
[matTooltip]="(copied !== clientId ? 'ACTIONS.COPY' : 'ACTIONS.COPIED') | translate"
cnslCopyToClipboard
[valueToCopy]="app.apiConfig.clientId"
[valueToCopy]="clientId"
(copiedValue)="copied = $event"
>
{{ app.apiConfig.clientId }}
{{ clientId }}
</button>
</div>
</div>
@@ -304,22 +317,22 @@
<div class="info-wrapper">
<p class="info-row-title">{{ 'IDP.DETAIL.DATECREATED' | translate }}</p>
<p class="info-row-desc" *ngIf="idp && idp.details && idp.details.creationDate">
{{ idp.details.creationDate | timestampToDate | localizedDate: 'dd. MMMM YYYY, HH:mm' }}
<p class="info-row-desc" *ngIf="idp?.details?.creationDate as creationDate">
{{ creationDate | timestampToDate | localizedDate: 'dd. MMMM YYYY, HH:mm' }}
</p>
</div>
<div class="info-wrapper">
<p class="info-row-title">{{ 'IDP.DETAIL.DATECHANGED' | translate }}</p>
<p class="info-row-desc" *ngIf="idp && idp.details && idp.details.changeDate">
{{ idp.details.changeDate | timestampToDate | localizedDate: 'dd. MMMM YYYY, HH:mm' }}
<p class="info-row-desc" *ngIf="idp?.details?.changeDate as changeDate">
{{ changeDate | timestampToDate | localizedDate: 'dd. MMMM YYYY, HH:mm' }}
</p>
</div>
<div class="info-wrapper">
<p class="info-row-title">{{ 'IDP.STATE' | translate }}</p>
<p
*ngIf="idp && idp.state !== undefined"
*ngIf="idp?.state"
class="state"
[ngClass]="{ active: idp.state === IDPState.IDP_STATE_ACTIVE, inactive: idp.state === IDPState.IDP_STATE_INACTIVE }"
>

View File

@@ -1,8 +1,49 @@
import { Component, ElementRef, NgZone } from '@angular/core';
import { TestBed, ComponentFixture } from '@angular/core/testing';
import { InputDirective } from './input.directive';
import { Platform } from '@angular/cdk/platform';
import { NgControl, NgForm, FormGroupDirective } from '@angular/forms';
import { ErrorStateMatcher } from '@angular/material/core';
import { AutofillMonitor } from '@angular/cdk/text-field';
import { MatFormField } from '@angular/material/form-field';
import { MAT_INPUT_VALUE_ACCESSOR } from '@angular/material/input';
import { of } from 'rxjs';
import { By } from '@angular/platform-browser';
@Component({
template: `<input appInputDirective />`,
})
class TestHostComponent {}
describe('InputDirective', () => {
let fixture: ComponentFixture<TestHostComponent>;
beforeEach(() => {
TestBed.configureTestingModule({
declarations: [InputDirective, TestHostComponent],
providers: [
{ provide: ElementRef, useValue: new ElementRef(document.createElement('input')) },
Platform,
{ provide: NgControl, useValue: null },
{ provide: NgForm, useValue: null },
{ provide: FormGroupDirective, useValue: null },
ErrorStateMatcher,
{ provide: MAT_INPUT_VALUE_ACCESSOR, useValue: null },
{
provide: AutofillMonitor,
useValue: { monitor: () => of(), stopMonitoring: () => {} },
},
NgZone,
{ provide: MatFormField, useValue: null },
],
}).compileComponents();
fixture = TestBed.createComponent(TestHostComponent);
fixture.detectChanges();
});
it('should create an instance', () => {
const directive = new InputDirective();
expect(directive).toBeTruthy();
const directiveEl = fixture.debugElement.query(By.directive(InputDirective));
expect(directiveEl).toBeTruthy();
});
});

View File

@@ -1,19 +1,19 @@
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { AvatarComponent } from './avatar.component';
import { LabelComponent } from './label.component';
describe('AvatarComponent', () => {
let component: AvatarComponent;
let fixture: ComponentFixture<AvatarComponent>;
let component: LabelComponent;
let fixture: ComponentFixture<LabelComponent>;
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
declarations: [AvatarComponent],
declarations: [LabelComponent],
}).compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(AvatarComponent);
fixture = TestBed.createComponent(LabelComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});

View File

@@ -4,7 +4,6 @@ import { Timestamp } from 'google-protobuf/google/protobuf/timestamp_pb';
import { ToastService } from 'src/app/services/toast.service';
import { Metadata as MetadataV2 } from '@zitadel/proto/zitadel/metadata_pb';
import { Metadata } from 'src/app/proto/generated/zitadel/metadata_pb';
import { Buffer } from 'buffer';
export type MetadataDialogData = {
metadata: (Metadata.AsObject | MetadataV2)[];
@@ -26,9 +25,10 @@ export class MetadataDialogComponent {
public dialogRef: MatDialogRef<MetadataDialogComponent>,
@Inject(MAT_DIALOG_DATA) public data: MetadataDialogData,
) {
const decoder = new TextDecoder();
this.metadata = data.metadata.map(({ key, value }) => ({
key,
value: typeof value === 'string' ? value : Buffer.from(value as unknown as string, 'base64').toString('utf8'),
value: typeof value === 'string' ? value : decoder.decode(value),
}));
}

View File

@@ -5,7 +5,6 @@ import { Observable, ReplaySubject } from 'rxjs';
import { Metadata as MetadataV2 } from '@zitadel/proto/zitadel/metadata_pb';
import { map, startWith } from 'rxjs/operators';
import { Metadata } from 'src/app/proto/generated/zitadel/metadata_pb';
import { Buffer } from 'buffer';
type StringMetadata = {
key: string;
@@ -37,12 +36,13 @@ export class MetadataComponent implements OnInit {
ngOnInit() {
this.dataSource$ = this.metadata$.pipe(
map((metadata) =>
metadata.map(({ key, value }) => ({
map((metadata) => {
const decoder = new TextDecoder();
return metadata.map(({ key, value }) => ({
key,
value: Buffer.from(value as any as string, 'base64').toString('utf-8'),
})),
),
value: typeof value === 'string' ? value : decoder.decode(value),
}));
}),
startWith([] as StringMetadata[]),
map((metadata) => new MatTableDataSource(metadata)),
);

View File

@@ -2,14 +2,12 @@ import { Component, Injector, Input, OnDestroy, OnInit, Type } from '@angular/co
import { AbstractControl, UntypedFormBuilder, UntypedFormGroup } from '@angular/forms';
import { MatDialog } from '@angular/material/dialog';
import { Duration } from 'google-protobuf/google/protobuf/duration_pb';
import { firstValueFrom, forkJoin, from, Observable, of, Subject, take } from 'rxjs';
import { forkJoin, from, of, Subject, take } from 'rxjs';
import {
GetLoginPolicyResponse as AdminGetLoginPolicyResponse,
UpdateLoginPolicyRequest,
UpdateLoginPolicyResponse,
} from 'src/app/proto/generated/zitadel/admin_pb';
import {
AddCustomLoginPolicyRequest,
GetLoginPolicyResponse as MgmtGetLoginPolicyResponse,
UpdateCustomLoginPolicyRequest,
} from 'src/app/proto/generated/zitadel/management_pb';
@@ -24,8 +22,7 @@ import { InfoSectionType } from '../../info-section/info-section.component';
import { WarnDialogComponent } from '../../warn-dialog/warn-dialog.component';
import { PolicyComponentServiceType } from '../policy-component-types.enum';
import { LoginMethodComponentType } from './factor-table/factor-table.component';
import { catchError, map, takeUntil } from 'rxjs/operators';
import { error } from 'console';
import { map, takeUntil } from 'rxjs/operators';
import { LoginPolicyService } from '../../../services/login-policy.service';
const minValueValidator = (minValue: number) => (control: AbstractControl) => {

View File

@@ -1,19 +1,19 @@
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { LoginPolicyComponent } from './login-policy.component';
import { MessageTextsComponent } from './message-texts.component';
describe('LoginPolicyComponent', () => {
let component: LoginPolicyComponent;
let fixture: ComponentFixture<LoginPolicyComponent>;
let component: MessageTextsComponent;
let fixture: ComponentFixture<MessageTextsComponent>;
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
declarations: [LoginPolicyComponent],
declarations: [MessageTextsComponent],
}).compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(LoginPolicyComponent);
fixture = TestBed.createComponent(MessageTextsComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});

View File

@@ -1,19 +1,19 @@
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { PasswordComplexityPolicyComponent } from './password-complexity-policy.component';
import { NotificationPolicyComponent } from './notification-policy.component';
describe('PasswordComplexityPolicyComponent', () => {
let component: PasswordComplexityPolicyComponent;
let fixture: ComponentFixture<PasswordComplexityPolicyComponent>;
let component: NotificationPolicyComponent;
let fixture: ComponentFixture<NotificationPolicyComponent>;
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
declarations: [PasswordComplexityPolicyComponent],
declarations: [NotificationPolicyComponent],
}).compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(PasswordComplexityPolicyComponent);
fixture = TestBed.createComponent(NotificationPolicyComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});

View File

@@ -1,19 +1,19 @@
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { PasswordDialogComponent } from './password-dialog-sms-provider.component';
import { PasswordDialogSMSProviderComponent } from './password-dialog-sms-provider.component';
describe('PasswordDialogComponent', () => {
let component: PasswordDialogComponent;
let fixture: ComponentFixture<PasswordDialogComponent>;
let component: PasswordDialogSMSProviderComponent;
let fixture: ComponentFixture<PasswordDialogSMSProviderComponent>;
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
declarations: [PasswordDialogComponent],
declarations: [PasswordDialogSMSProviderComponent],
}).compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(PasswordDialogComponent);
fixture = TestBed.createComponent(PasswordDialogSMSProviderComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});

View File

@@ -1,19 +1,19 @@
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { ProviderOAuthComponent } from './provider-oauth.component';
import { ProviderGithubESComponent } from './provider-github-es.component';
describe('ProviderOAuthComponent', () => {
let component: ProviderOAuthComponent;
let fixture: ComponentFixture<ProviderOAuthComponent>;
let component: ProviderGithubESComponent;
let fixture: ComponentFixture<ProviderGithubESComponent>;
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
declarations: [ProviderOAuthComponent],
declarations: [ProviderGithubESComponent],
}).compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(ProviderOAuthComponent);
fixture = TestBed.createComponent(ProviderGithubESComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});

View File

@@ -1,19 +1,19 @@
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { ProviderGoogleComponent } from './provider-google.component';
import { ProviderGitlabSelfHostedComponent } from './provider-gitlab-self-hosted.component';
describe('ProviderGoogleComponent', () => {
let component: ProviderGoogleComponent;
let fixture: ComponentFixture<ProviderGoogleComponent>;
let component: ProviderGitlabSelfHostedComponent;
let fixture: ComponentFixture<ProviderGitlabSelfHostedComponent>;
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
declarations: [ProviderGoogleComponent],
declarations: [ProviderGitlabSelfHostedComponent],
}).compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(ProviderGoogleComponent);
fixture = TestBed.createComponent(ProviderGitlabSelfHostedComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});

View File

@@ -1,19 +1,19 @@
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { ProviderGoogleComponent } from './provider-google.component';
import { ProviderGitlabComponent } from './provider-gitlab.component';
describe('ProviderGoogleComponent', () => {
let component: ProviderGoogleComponent;
let fixture: ComponentFixture<ProviderGoogleComponent>;
let component: ProviderGitlabComponent;
let fixture: ComponentFixture<ProviderGitlabComponent>;
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
declarations: [ProviderGoogleComponent],
declarations: [ProviderGitlabComponent],
}).compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(ProviderGoogleComponent);
fixture = TestBed.createComponent(ProviderGitlabComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});

View File

@@ -98,14 +98,13 @@
<p class="checkbox-desc">{{ 'IDP.ISIDTOKENMAPPING_DESC' | translate }}</p>
<mat-checkbox formControlName="isIdTokenMapping">{{ 'IDP.ISIDTOKENMAPPING' | translate }}</mat-checkbox>
</div>
</cnsl-info-section>
<cnsl-info-section>
<div>
<p class="checkbox-desc">{{ 'IDP.USEPKCE_DESC' | translate }}</p>
<mat-checkbox formControlName="usePkce">{{ 'IDP.USEPKCE' | translate }}</mat-checkbox>
</div>
</cnsl-info-section>
</cnsl-info-section>
</div>
<cnsl-provider-options

View File

@@ -82,7 +82,7 @@
<cnsl-info-section>
<div>
<p class="transient-info-desc">{{ 'IDP.SAML.TRANSIENTMAPPINGATTRIBUTENAME_DESC' | translate }}</p>
<p class="option-desc">{{ 'IDP.SAML.TRANSIENTMAPPINGATTRIBUTENAME_DESC' | translate }}</p>
</div>
<cnsl-form-field class="formfield">
@@ -90,6 +90,15 @@
<input cnslInput formControlName="transientMappingAttributeName" />
</cnsl-form-field>
</cnsl-info-section>
<cnsl-info-section>
<div>
<p class="option-desc">{{ 'IDP.FEDERATEDLOGOUTENABLED_DESC' | translate }}</p>
<mat-checkbox formControlName="federatedLogoutEnabled">{{
'IDP.FEDERATEDLOGOUTENABLED' | translate
}}</mat-checkbox>
</div>
</cnsl-info-section>
</div>
<cnsl-provider-options

View File

@@ -4,9 +4,9 @@
.transient-info {
max-width: 500px;
}
.transient-info-desc {
.option-desc {
margin-top: 0;
margin-bottom: 0.5rem;
}
}

View File

@@ -127,6 +127,7 @@ export class ProviderSamlSpComponent {
withSignedRequest: new UntypedFormControl(true, [requiredValidator]),
nameIdFormat: new UntypedFormControl(SAMLNameIDFormat.SAML_NAME_ID_FORMAT_PERSISTENT, []),
transientMappingAttributeName: new UntypedFormControl('', []),
federatedLogoutEnabled: new UntypedFormControl(false, []),
},
atLeastOneIsFilled('metadataXml', 'metadataUrl'),
);
@@ -210,6 +211,7 @@ export class ProviderSamlSpComponent {
// @ts-ignore
req.setNameIdFormat(SAMLNameIDFormat[this.nameIDFormat?.value]);
req.setTransientMappingAttributeName(this.transientMapping?.value);
req.setFederatedLogoutEnabled(this.federatedLogoutEnabled?.value);
req.setProviderOptions(this.options);
this.loading = true;
@@ -250,6 +252,7 @@ export class ProviderSamlSpComponent {
req.setNameIdFormat(SAMLNameIDFormat[this.nameIDFormat.value]);
}
req.setTransientMappingAttributeName(this.transientMapping?.value);
req.setFederatedLogoutEnabled(this.federatedLogoutEnabled?.value);
this.loading = true;
this.service
.addSAMLProvider(req)
@@ -335,4 +338,8 @@ export class ProviderSamlSpComponent {
private get transientMapping(): AbstractControl | null {
return this.form.get('transientMappingAttributeName');
}
private get federatedLogoutEnabled(): AbstractControl | null {
return this.form.get('federatedLogoutEnabled');
}
}

View File

@@ -228,9 +228,9 @@ export const ACTIONS: SidenavSetting = {
i18nKey: 'SETTINGS.LIST.ACTIONS',
groupI18nKey: 'SETTINGS.GROUPS.ACTIONS',
requiredRoles: {
// todo: figure out roles
[PolicyComponentServiceType.ADMIN]: ['iam.policy.read'],
[PolicyComponentServiceType.ADMIN]: ['action.execution.write', 'action.target.write'],
},
beta: true,
};
export const ACTIONS_TARGETS: SidenavSetting = {
@@ -238,7 +238,7 @@ export const ACTIONS_TARGETS: SidenavSetting = {
i18nKey: 'SETTINGS.LIST.TARGETS',
groupI18nKey: 'SETTINGS.GROUPS.ACTIONS',
requiredRoles: {
// todo: figure out roles
[PolicyComponentServiceType.ADMIN]: ['iam.policy.read'],
[PolicyComponentServiceType.ADMIN]: ['action.execution.write', 'action.target.write'],
},
beta: true,
};

View File

@@ -1,19 +1,19 @@
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { ShowKeyDialogComponent } from './show-key-dialog.component';
import { ShowTokenDialogComponent } from './show-token-dialog.component';
describe('ShowKeyDialogComponent', () => {
let component: ShowKeyDialogComponent;
let fixture: ComponentFixture<ShowKeyDialogComponent>;
let component: ShowTokenDialogComponent;
let fixture: ComponentFixture<ShowTokenDialogComponent>;
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
declarations: [ShowKeyDialogComponent],
declarations: [ShowTokenDialogComponent],
}).compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(ShowKeyDialogComponent);
fixture = TestBed.createComponent(ShowTokenDialogComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});

View File

@@ -28,6 +28,7 @@
[attr.data-e2e]="'sidenav-element-' + setting.id"
>
<span>{{ setting.i18nKey | translate }}</span>
<span class="state" *ngIf="setting?.beta">{{ 'SETTINGS.BETA' | translate }}</span>
<mat-icon *ngIf="setting.showWarn" class="warn-icon" svgIcon="mdi_shield_alert"></mat-icon>
</button>
</ng-container>

View File

@@ -90,6 +90,10 @@
flex-shrink: 0;
}
.state {
margin-left: 0.5rem;
}
&:hover {
span {
opacity: 1;

View File

@@ -11,6 +11,7 @@ export interface SidenavSetting {
[PolicyComponentServiceType.ADMIN]?: string[];
};
showWarn?: boolean;
beta?: boolean;
}
@Component({

View File

@@ -1,19 +1,19 @@
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { IdpTableComponent } from './smtp-table.component';
import { SMTPTableComponent } from './smtp-table.component';
describe('UserTableComponent', () => {
let component: IdpTableComponent;
let fixture: ComponentFixture<IdpTableComponent>;
let component: SMTPTableComponent;
let fixture: ComponentFixture<SMTPTableComponent>;
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
declarations: [IdpTableComponent],
declarations: [SMTPTableComponent],
}).compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(IdpTableComponent);
fixture = TestBed.createComponent(SMTPTableComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});

Some files were not shown because too many files have changed in this diff Show More