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: with:
node_version: "18" node_version: "18"
buf_version: "latest" 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_key: ${{ needs.core.outputs.cache_key }}
core_cache_path: ${{ needs.core.outputs.cache_path }} 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/ 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 ### 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. 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 #### 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. 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`. 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. 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. 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. 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. 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. 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 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. 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. 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. 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 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 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. 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. 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). 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). 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. 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. 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. ### Pagination
```protobuf
// ListQuery is a general query object for lists to allow pagination and sorting. Most listing methods SHOULD use the `PaginationRequest` message to allow the client to specify the limit, offset, and sorting options.
message ListQuery { ```protobuf
uint64 offset = 1; message ListTargetsRequest {
// limit is the maximum amount of objects returned. The default is set to 100 // List limitations and ordering.
// with a maximum of 1000 in the runtime configuration. optional zitadel.filter.v2beta.PaginationRequest pagination = 1;
// If the limit exceeds the maximum configured ZITADEL will throw an error. // 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.
// If no limit is present the default is taken. optional TargetFieldName sorting_column = 2 [
uint32 limit = 2; (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = {
// Asc is the sorting order. If true the list is sorted ascending, if false default: "\"TARGET_FIELD_NAME_CREATION_DATE\""
// the list is sorted descending. The default is descending. }
bool asc = 3; ];
// 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. 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 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. 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 ## Error Handling
The API returns machine-readable errors in the response body. This includes a status code, an error code and possibly 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 ### 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) 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 ## Security

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,90 +1,90 @@
package domain package domain
import ( // import (
"context" // "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. // // 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: // // In the future it might make sense to separate the command into two commands:
// - CreateHumanCommand: creates a new human user // // - CreateHumanCommand: creates a new human user
// - CreateMachineCommand: creates a new machine user // // - CreateMachineCommand: creates a new machine user
type CreateUserCommand struct { // type CreateUserCommand struct {
user *User // user *User
email *SetEmailCommand // email *SetEmailCommand
} // }
var ( // var (
_ Commander = (*CreateUserCommand)(nil) // _ Commander = (*CreateUserCommand)(nil)
_ eventer = (*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. // // 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 { // func NewCreateHumanCommand(username string, opts ...CreateHumanOpt) *CreateUserCommand {
cmd := &CreateUserCommand{ // cmd := &CreateUserCommand{
user: &User{ // user: &User{
Username: username, // Username: username,
Traits: &Human{}, // Traits: &Human{},
}, // },
} // }
for _, opt := range opts { // for _, opt := range opts {
opt.applyOnCreateHuman(cmd) // opt.applyOnCreateHuman(cmd)
} // }
return cmd // return cmd
} // }
// String implements [Commander]. // // String implements [Commander].
func (cmd *CreateUserCommand) String() string { // func (cmd *CreateUserCommand) String() string {
return "CreateUserCommand" // return "CreateUserCommand"
} // }
// Events implements [eventer]. // // Events implements [eventer].
func (c *CreateUserCommand) Events() []*eventstore.Event { // func (c *CreateUserCommand) Events() []*eventstore.Event {
return []*eventstore.Event{ // return []*eventstore.Event{
{ // {
AggregateType: "user", // AggregateType: "user",
AggregateID: c.user.ID, // AggregateID: c.user.ID,
Type: "user.added", // Type: "user.added",
Payload: c.user, // Payload: c.user,
}, // },
} // }
} // }
// Execute implements [Commander]. // // Execute implements [Commander].
func (c *CreateUserCommand) Execute(ctx context.Context, opts *CommandOpts) error { // func (c *CreateUserCommand) Execute(ctx context.Context, opts *CommandOpts) error {
if err := c.ensureUserID(); err != nil { // if err := c.ensureUserID(); err != nil {
return err // return err
} // }
c.email.UserID = c.user.ID // c.email.UserID = c.user.ID
if err := opts.Invoke(ctx, c.email); err != nil { // if err := opts.Invoke(ctx, c.email); err != nil {
return err // return err
} // }
return nil // return nil
} // }
type CreateHumanOpt interface { // type CreateHumanOpt interface {
applyOnCreateHuman(*CreateUserCommand) // applyOnCreateHuman(*CreateUserCommand)
} // }
type createHumanIDOpt string // type createHumanIDOpt string
// applyOnCreateHuman implements [CreateHumanOpt]. // // applyOnCreateHuman implements [CreateHumanOpt].
func (c createHumanIDOpt) applyOnCreateHuman(cmd *CreateUserCommand) { // func (c createHumanIDOpt) applyOnCreateHuman(cmd *CreateUserCommand) {
cmd.user.ID = string(c) // cmd.user.ID = string(c)
} // }
var _ CreateHumanOpt = (*createHumanIDOpt)(nil) // var _ CreateHumanOpt = (*createHumanIDOpt)(nil)
func CreateHumanWithID(id string) CreateHumanOpt { // func CreateHumanWithID(id string) CreateHumanOpt {
return createHumanIDOpt(id) // return createHumanIDOpt(id)
} // }
func (c *CreateUserCommand) ensureUserID() (err error) { // func (c *CreateUserCommand) ensureUserID() (err error) {
if c.user.ID != "" { // if c.user.ID != "" {
return nil // return nil
} // }
c.user.ID, err = generateID() // c.user.ID, err = generateID()
return err // return err
} // }

View File

@@ -1,37 +1,37 @@
package domain package domain
import ( // import (
"context" // "context"
"github.com/zitadel/zitadel/internal/crypto" // "github.com/zitadel/zitadel/internal/crypto"
) // )
type generateCodeCommand struct { // type generateCodeCommand struct {
code string // code string
value *crypto.CryptoValue // value *crypto.CryptoValue
} // }
// I didn't update this repository to the solution proposed please view one of the following interfaces for correct usage: // // I didn't update this repository to the solution proposed please view one of the following interfaces for correct usage:
// - [UserRepository] // // - [UserRepository]
// - [InstanceRepository] // // - [InstanceRepository]
// - [OrgRepository] // // - [OrgRepository]
type CryptoRepository interface { // type CryptoRepository interface {
GetEncryptionConfig(ctx context.Context) (*crypto.GeneratorConfig, error) // GetEncryptionConfig(ctx context.Context) (*crypto.GeneratorConfig, error)
} // }
// String implements [Commander]. // // String implements [Commander].
func (cmd *generateCodeCommand) String() string { // func (cmd *generateCodeCommand) String() string {
return "generateCodeCommand" // return "generateCodeCommand"
} // }
func (cmd *generateCodeCommand) Execute(ctx context.Context, opts *CommandOpts) error { // func (cmd *generateCodeCommand) Execute(ctx context.Context, opts *CommandOpts) error {
config, err := cryptoRepo(opts.DB).GetEncryptionConfig(ctx) // config, err := cryptoRepo(opts.DB).GetEncryptionConfig(ctx)
if err != nil { // if err != nil {
return err // return err
} // }
generator := crypto.NewEncryptionGenerator(*config, userCodeAlgorithm) // generator := crypto.NewEncryptionGenerator(*config, userCodeAlgorithm)
cmd.value, cmd.code, err = crypto.NewCode(generator) // cmd.value, cmd.code, err = crypto.NewCode(generator)
return err // return err
} // }
var _ Commander = (*generateCodeCommand)(nil) // var _ Commander = (*generateCodeCommand)(nil)

View File

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

View File

@@ -1,67 +1,67 @@
package domain_test package domain_test
import ( // import (
"context" // "context"
"log/slog" // "log/slog"
"testing" // "testing"
"github.com/stretchr/testify/assert" // "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" // "github.com/stretchr/testify/require"
"go.opentelemetry.io/otel" // "go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/exporters/stdout/stdouttrace" // "go.opentelemetry.io/otel/exporters/stdout/stdouttrace"
sdktrace "go.opentelemetry.io/otel/sdk/trace" // sdktrace "go.opentelemetry.io/otel/sdk/trace"
"go.uber.org/mock/gomock" // "go.uber.org/mock/gomock"
. "github.com/zitadel/zitadel/backend/v3/domain" // . "github.com/zitadel/zitadel/backend/v3/domain"
"github.com/zitadel/zitadel/backend/v3/storage/database/dbmock" // "github.com/zitadel/zitadel/backend/v3/storage/database/dbmock"
"github.com/zitadel/zitadel/backend/v3/storage/database/repository" // "github.com/zitadel/zitadel/backend/v3/storage/database/repository"
"github.com/zitadel/zitadel/backend/v3/telemetry/logging" // "github.com/zitadel/zitadel/backend/v3/telemetry/logging"
"github.com/zitadel/zitadel/backend/v3/telemetry/tracing" // "github.com/zitadel/zitadel/backend/v3/telemetry/tracing"
) // )
// These tests give an overview of how to use the domain package. // These tests give an overview of how to use the domain package.
func TestExample(t *testing.T) { // func TestExample(t *testing.T) {
t.Skip("skip example test because it is not a real test") // t.Skip("skip example test because it is not a real test")
ctx := context.Background() // ctx := context.Background()
ctrl := gomock.NewController(t) // ctrl := gomock.NewController(t)
pool := dbmock.NewMockPool(ctrl) // pool := dbmock.NewMockPool(ctrl)
tx := dbmock.NewMockTransaction(ctrl) // tx := dbmock.NewMockTransaction(ctrl)
pool.EXPECT().Begin(gomock.Any(), gomock.Any()).Return(tx, nil) // pool.EXPECT().Begin(gomock.Any(), gomock.Any()).Return(tx, nil)
tx.EXPECT().End(gomock.Any(), gomock.Any()).Return(nil) // tx.EXPECT().End(gomock.Any(), gomock.Any()).Return(nil)
SetPool(pool) // SetPool(pool)
exporter, err := stdouttrace.New(stdouttrace.WithPrettyPrint()) // exporter, err := stdouttrace.New(stdouttrace.WithPrettyPrint())
require.NoError(t, err) // require.NoError(t, err)
tracerProvider := sdktrace.NewTracerProvider( // tracerProvider := sdktrace.NewTracerProvider(
sdktrace.WithSyncer(exporter), // sdktrace.WithSyncer(exporter),
) // )
otel.SetTracerProvider(tracerProvider) // otel.SetTracerProvider(tracerProvider)
SetTracer(tracing.Tracer{Tracer: tracerProvider.Tracer("test")}) // SetTracer(tracing.Tracer{Tracer: tracerProvider.Tracer("test")})
defer func() { assert.NoError(t, tracerProvider.Shutdown(ctx)) }() // defer func() { assert.NoError(t, tracerProvider.Shutdown(ctx)) }()
SetLogger(logging.Logger{Logger: slog.Default()}) // SetLogger(logging.Logger{Logger: slog.Default()})
SetUserRepository(repository.UserRepository) // SetUserRepository(repository.UserRepository)
SetOrgRepository(repository.OrgRepository) // SetOrgRepository(repository.OrgRepository)
// SetInstanceRepository(repository.Instance) // // SetInstanceRepository(repository.Instance)
// SetCryptoRepository(repository.Crypto) // // SetCryptoRepository(repository.Crypto)
t.Run("create org", func(t *testing.T) { // t.Run("create org", func(t *testing.T) {
org := NewAddOrgCommand("testorg", NewAddMemberCommand("testuser", "ORG_OWNER")) // org := NewAddOrgCommand("testorg", NewAddMemberCommand("testuser", "ORG_OWNER"))
user := NewCreateHumanCommand("testuser") // user := NewCreateHumanCommand("testuser")
err := Invoke(ctx, BatchCommands(org, user)) // err := Invoke(ctx, BatchCommands(org, user))
assert.NoError(t, err) // assert.NoError(t, err)
}) // })
t.Run("verified email", func(t *testing.T) { // t.Run("verified email", func(t *testing.T) {
err := Invoke(ctx, NewSetEmailCommand("u1", "test@example.com", NewEmailVerifiedCommand("u1", true))) // err := Invoke(ctx, NewSetEmailCommand("u1", "test@example.com", NewEmailVerifiedCommand("u1", true)))
assert.NoError(t, err) // assert.NoError(t, err)
}) // })
t.Run("unverified email", func(t *testing.T) { // t.Run("unverified email", func(t *testing.T) {
err := Invoke(ctx, NewSetEmailCommand("u2", "test2@example.com", NewEmailVerifiedCommand("u2", false))) // err := Invoke(ctx, NewSetEmailCommand("u2", "test2@example.com", NewEmailVerifiedCommand("u2", false)))
assert.NoError(t, err) // assert.NoError(t, err)
}) // })
} // }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -16,6 +16,7 @@ type Pool interface {
type PoolTest interface { type PoolTest interface {
Pool Pool
// MigrateTest is the same as [Migrator] but executes the migrations multiple times instead of only once.
MigrateTest(ctx context.Context) error MigrateTest(ctx context.Context) error
} }
@@ -35,6 +36,7 @@ type Querier interface {
} }
// Executor is a database client that can execute statements. // Executor is a database client that can execute statements.
// It returns the number of rows affected or an error
type Executor interface { type Executor interface {
Exec(ctx context.Context, stmt string, args ...any) (int64, error) 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. // // The value will be taken as is. Multiple options are space separated.
// Options string // Options string
configuredFields []string // configuredFields []string
} }
// Connect implements [database.Connector]. // Connect implements [database.Connector].

View File

@@ -10,3 +10,17 @@ CREATE TABLE IF NOT EXISTS zitadel.instances(
updated_at TIMESTAMPTZ DEFAULT NOW(), updated_at TIMESTAMPTZ DEFAULT NOW(),
deleted_at TIMESTAMPTZ DEFAULT NULL 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]. // Migrate implements [database.PoolTest].
func (c *pgxPool) MigrateTest(ctx context.Context) error { func (c *pgxPool) MigrateTest(ctx context.Context) error {
// allow multiple migrations
// if isMigrated {
// return nil
// }
client, err := c.Pool.Acquire(ctx) client, err := c.Pool.Acquire(ctx)
if err != nil { if err != nil {
return err return err

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -5,11 +5,10 @@ package repository_test
// "testing" // "testing"
// "github.com/stretchr/testify/assert" // "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"
// "github.com/zitadel/zitadel/backend/v3/storage/database/dbmock" // "github.com/zitadel/zitadel/backend/v3/storage/database/dbmock"
// "github.com/zitadel/zitadel/backend/v3/storage/database/repository" // "github.com/zitadel/zitadel/backend/v3/storage/database/repository"
// "go.uber.org/mock/gomock"
// ) // )
// func TestQueryUser(t *testing.T) { // func TestQueryUser(t *testing.T) {
@@ -75,3 +74,4 @@ package repository_test
// user.Human().Update(context.Background(), user.IDCondition("test"), user.SetUsername("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" "context"
_ "embed" _ "embed"
"io" "io"
"strconv"
"time" "time"
"github.com/jackc/pgx/v5/stdlib" "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") logging.OnError(err).Fatal("unable to connect to destination database")
defer destClient.Close() 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() 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) sourceConn, err := source.Conn(ctx)
logging.OnError(err).Fatal("unable to acquire connection") logging.OnError(err).Fatal("unable to acquire connection")
defer sourceConn.Close() defer sourceConn.Close()
@@ -55,9 +60,9 @@ func copyAuthRequests(ctx context.Context, source, dest *database.DB) {
errs := make(chan error, 1) errs := make(chan error, 1)
go func() { go func() {
err = sourceConn.Raw(func(driverConn interface{}) error { err = sourceConn.Raw(func(driverConn any) error {
conn := driverConn.(*stdlib.Conn).Conn() 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() w.Close()
return err return err
}) })
@@ -69,7 +74,7 @@ func copyAuthRequests(ctx context.Context, source, dest *database.DB) {
defer destConn.Close() defer destConn.Close()
var affected int64 var affected int64
err = destConn.Raw(func(driverConn interface{}) error { err = destConn.Raw(func(driverConn any) error {
conn := driverConn.(*stdlib.Conn).Conn() conn := driverConn.(*stdlib.Conn).Conn()
if shouldReplace { if shouldReplace {

View File

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

View File

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

View File

@@ -3,6 +3,8 @@ package mirror
import ( import (
"context" "context"
"github.com/shopspring/decimal"
"github.com/zitadel/zitadel/internal/v2/eventstore" "github.com/zitadel/zitadel/internal/v2/eventstore"
"github.com/zitadel/zitadel/internal/v2/readmodel" "github.com/zitadel/zitadel/internal/v2/readmodel"
"github.com/zitadel/zitadel/internal/v2/system" "github.com/zitadel/zitadel/internal/v2/system"
@@ -29,7 +31,7 @@ func queryLastSuccessfulMigration(ctx context.Context, destinationES *eventstore
return lastSuccess, nil 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( return destinationES.Push(
ctx, ctx,
eventstore.NewPushIntent( eventstore.NewPushIntent(

View File

@@ -8,7 +8,9 @@ import (
"io" "io"
"time" "time"
"github.com/jackc/pgx/v5/pgconn"
"github.com/jackc/pgx/v5/stdlib" "github.com/jackc/pgx/v5/stdlib"
"github.com/shopspring/decimal"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/spf13/viper" "github.com/spf13/viper"
"github.com/zitadel/logging" "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) { func copyEvents(ctx context.Context, source, dest *db.DB, bulkSize uint32) {
logging.Info("starting to copy events")
start := time.Now() start := time.Now()
reader, writer := io.Pipe() 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()) previousMigration, err := queryLastSuccessfulMigration(ctx, destinationES, source.DatabaseName())
logging.OnError(err).Fatal("unable to query latest successful migration") logging.OnError(err).Fatal("unable to query latest successful migration")
var maxPosition float64 var maxPosition decimal.Decimal
err = source.QueryRowContext(ctx, err = source.QueryRowContext(ctx,
func(row *sql.Row) error { func(row *sql.Row) error {
return row.Scan(&maxPosition) 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") logging.WithFields("from", previousMigration.Position, "to", maxPosition).Info("start event migration")
nextPos := make(chan bool, 1) nextPos := make(chan bool, 1)
pos := make(chan float64, 1) pos := make(chan decimal.Decimal, 1)
errs := make(chan error, 3) errs := make(chan error, 3)
go func() { go func() {
@@ -130,7 +133,10 @@ func copyEvents(ctx context.Context, source, dest *db.DB, bulkSize uint32) {
if err != nil { if err != nil {
return zerrors.ThrowUnknownf(err, "MIGRA-KTuSq", "unable to copy events from source during iteration %d", i) 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) { if tag.RowsAffected() < int64(bulkSize) {
logging.WithFields("batch_count", i).Info("last batch of events copied")
return nil return nil
} }
@@ -148,7 +154,7 @@ func copyEvents(ctx context.Context, source, dest *db.DB, bulkSize uint32) {
go func() { go func() {
defer close(pos) defer close(pos)
for range nextPos { for range nextPos {
var position float64 var position decimal.Decimal
err := dest.QueryRowContext( err := dest.QueryRowContext(
ctx, ctx,
func(row *sql.Row) error { 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") tag, err := conn.PgConn().CopyFrom(ctx, reader, "COPY eventstore.events2 FROM STDIN")
eventCount = tag.RowsAffected() eventCount = tag.RowsAffected()
if err != nil { 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") 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") 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)) joinedErrs := make([]error, 0, len(errs))
for err := range errs { for err := range errs {
joinedErrs = append(joinedErrs, err) 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) { func copyUniqueConstraints(ctx context.Context, source, dest *db.DB) {
logging.Info("starting to copy unique constraints")
start := time.Now() start := time.Now()
reader, writer := io.Pipe() reader, writer := io.Pipe()
errs := make(chan error, 1) errs := make(chan error, 1)

View File

@@ -3,6 +3,7 @@ package mirror
import ( import (
"context" "context"
"database/sql" "database/sql"
"fmt"
"net/http" "net/http"
"sync" "sync"
"time" "time"
@@ -104,6 +105,7 @@ func projections(
config *ProjectionsConfig, config *ProjectionsConfig,
masterKey string, masterKey string,
) { ) {
logging.Info("starting to fill projections")
start := time.Now() start := time.Now()
client, err := database.Connect(config.Destination, false) client, err := database.Connect(config.Destination, false)
@@ -255,8 +257,10 @@ func projections(
go execProjections(ctx, instances, failedInstances, &wg) go execProjections(ctx, instances, failedInstances, &wg)
} }
for _, instance := range queryInstanceIDs(ctx, client) { existingInstances := queryInstanceIDs(ctx, client)
for i, instance := range existingInstances {
instances <- instance instances <- instance
logging.WithFields("id", instance, "index", fmt.Sprintf("%d/%d", i, len(existingInstances))).Info("instance queued for projection")
} }
close(instances) close(instances)
wg.Wait() wg.Wait()
@@ -268,7 +272,7 @@ func projections(
func execProjections(ctx context.Context, instances <-chan string, failedInstances chan<- string, wg *sync.WaitGroup) { func execProjections(ctx context.Context, instances <-chan string, failedInstances chan<- string, wg *sync.WaitGroup) {
for instance := range instances { for instance := range instances {
logging.WithFields("instance", instance).Info("start projections") logging.WithFields("instance", instance).Info("starting projections")
ctx = internal_authz.WithInstanceID(ctx, instance) ctx = internal_authz.WithInstanceID(ctx, instance)
err := projection.ProjectInstance(ctx) err := projection.ProjectInstance(ctx)
@@ -292,6 +296,13 @@ func execProjections(ctx context.Context, instances <-chan string, failedInstanc
continue 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) err = auth_handler.ProjectInstance(ctx)
if err != nil { if err != nil {
logging.WithFields("instance", instance).WithError(err).Info("trigger auth handler failed") 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() wg.Done()
} }
// returns the instance configured by flag // queryInstanceIDs returns the instance configured by flag
// or all instances which are not removed // or all instances which are not removed
func queryInstanceIDs(ctx context.Context, source *database.DB) []string { func queryInstanceIDs(ctx context.Context, source *database.DB) []string {
if len(instanceIDs) > 0 { 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) { func copyAssets(ctx context.Context, source, dest *database.DB) {
logging.Info("starting to copy assets")
start := time.Now() start := time.Now()
sourceConn, err := source.Conn(ctx) 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") logging.OnError(err).Fatal("unable to acquire dest connection")
defer destConn.Close() defer destConn.Close()
var eventCount int64 var assetCount int64
err = destConn.Raw(func(driverConn interface{}) error { err = destConn.Raw(func(driverConn interface{}) error {
conn := driverConn.(*stdlib.Conn).Conn() 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") 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 return err
}) })
logging.OnError(err).Fatal("unable to copy assets to destination") logging.OnError(err).Fatal("unable to copy assets to destination")
logging.OnError(<-errs).Fatal("unable to copy assets from source") 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) { func copyEncryptionKeys(ctx context.Context, source, dest *database.DB) {
logging.Info("starting to copy encryption keys")
start := time.Now() start := time.Now()
sourceConn, err := source.Conn(ctx) 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") logging.OnError(err).Fatal("unable to acquire dest connection")
defer destConn.Close() defer destConn.Close()
var eventCount int64 var keyCount int64
err = destConn.Raw(func(driverConn interface{}) error { err = destConn.Raw(func(driverConn interface{}) error {
conn := driverConn.(*stdlib.Conn).Conn() 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") tag, err := conn.PgConn().CopyFrom(ctx, r, "COPY system.encryption_keys FROM stdin")
eventCount = tag.RowsAffected() keyCount = tag.RowsAffected()
return err return err
}) })
logging.OnError(err).Fatal("unable to copy encryption keys to destination") logging.OnError(err).Fatal("unable to copy encryption keys to destination")
logging.OnError(<-errs).Fatal("unable to copy encryption keys from source") 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 { type BackChannelLogoutNotificationStart struct {
dbClient *database.DB dbClient *database.DB
esClient *eventstore.Eventstore
} }
func (mig *BackChannelLogoutNotificationStart) Execute(ctx context.Context, e eventstore.Event) error { func (mig *BackChannelLogoutNotificationStart) Execute(ctx context.Context, e eventstore.Event) error {

View File

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

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 s51IDPTemplate6RootCA *IDPTemplate6RootCA
s52IDPTemplate6LDAP2 *IDPTemplate6LDAP2 s52IDPTemplate6LDAP2 *IDPTemplate6LDAP2
s53InitPermittedOrgsFunction *InitPermittedOrgsFunction53 s53InitPermittedOrgsFunction *InitPermittedOrgsFunction53
s54InstancePositionIndex *InstancePositionIndex
s55ExecutionHandlerStart *ExecutionHandlerStart
s56IDPTemplate6SAMLFederatedLogout *IDPTemplate6SAMLFederatedLogout
s57CreateResourceCounts *CreateResourceCounts
s58ReplaceLoginNames3View *ReplaceLoginNames3View
} }
func MustNewSteps(v *viper.Viper) *Steps { 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.s35AddPositionToIndexEsWm = &AddPositionToIndexEsWm{dbClient: dbClient}
steps.s36FillV2Milestones = &FillV3Milestones{dbClient: dbClient, eventstore: eventstoreClient} steps.s36FillV2Milestones = &FillV3Milestones{dbClient: dbClient, eventstore: eventstoreClient}
steps.s37Apps7OIDConfigsBackChannelLogoutURI = &Apps7OIDConfigsBackChannelLogoutURI{dbClient: dbClient} steps.s37Apps7OIDConfigsBackChannelLogoutURI = &Apps7OIDConfigsBackChannelLogoutURI{dbClient: dbClient}
steps.s38BackChannelLogoutNotificationStart = &BackChannelLogoutNotificationStart{dbClient: dbClient, esClient: eventstoreClient} steps.s38BackChannelLogoutNotificationStart = &BackChannelLogoutNotificationStart{dbClient: dbClient}
steps.s40InitPushFunc = &InitPushFunc{dbClient: dbClient} steps.s40InitPushFunc = &InitPushFunc{dbClient: dbClient}
steps.s42Apps7OIDCConfigsLoginVersion = &Apps7OIDCConfigsLoginVersion{dbClient: dbClient} steps.s42Apps7OIDCConfigsLoginVersion = &Apps7OIDCConfigsLoginVersion{dbClient: dbClient}
steps.s43CreateFieldsDomainIndex = &CreateFieldsDomainIndex{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.s51IDPTemplate6RootCA = &IDPTemplate6RootCA{dbClient: dbClient}
steps.s52IDPTemplate6LDAP2 = &IDPTemplate6LDAP2{dbClient: dbClient} steps.s52IDPTemplate6LDAP2 = &IDPTemplate6LDAP2{dbClient: dbClient}
steps.s53InitPermittedOrgsFunction = &InitPermittedOrgsFunction53{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) err = projection.Create(ctx, dbClient, eventstoreClient, config.Projections, nil, nil, nil)
logging.OnError(err).Fatal("unable to start projections") 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.s51IDPTemplate6RootCA,
steps.s52IDPTemplate6LDAP2, steps.s52IDPTemplate6LDAP2,
steps.s53InitPermittedOrgsFunction, steps.s53InitPermittedOrgsFunction,
steps.s54InstancePositionIndex,
steps.s55ExecutionHandlerStart,
steps.s56IDPTemplate6SAMLFederatedLogout,
steps.s57CreateResourceCounts,
steps.s58ReplaceLoginNames3View,
} { } {
setupErr = executeMigration(ctx, eventstoreClient, step, "migration failed") setupErr = executeMigration(ctx, eventstoreClient, step, "migration failed")
if setupErr != nil { if setupErr != nil {
@@ -293,6 +303,7 @@ func Setup(ctx context.Context, config *Config, steps *Steps, masterKey string)
client: dbClient, client: dbClient,
}, },
} }
repeatableSteps = append(repeatableSteps, triggerSteps(dbClient)...)
for _, repeatableStep := range repeatableSteps { for _, repeatableStep := range repeatableSteps {
setupErr = executeMigration(ctx, eventstoreClient, repeatableStep, "unable to migrate repeatable step") 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_v2 "github.com/zitadel/zitadel/internal/api/grpc/feature/v2"
feature_v2beta "github.com/zitadel/zitadel/internal/api/grpc/feature/v2beta" feature_v2beta "github.com/zitadel/zitadel/internal/api/grpc/feature/v2beta"
idp_v2 "github.com/zitadel/zitadel/internal/api/grpc/idp/v2" 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" "github.com/zitadel/zitadel/internal/api/grpc/management"
oidc_v2 "github.com/zitadel/zitadel/internal/api/grpc/oidc/v2" oidc_v2 "github.com/zitadel/zitadel/internal/api/grpc/oidc/v2"
oidc_v2beta "github.com/zitadel/zitadel/internal/api/grpc/oidc/v2beta" oidc_v2beta "github.com/zitadel/zitadel/internal/api/grpc/oidc/v2beta"
org_v2 "github.com/zitadel/zitadel/internal/api/grpc/org/v2" org_v2 "github.com/zitadel/zitadel/internal/api/grpc/org/v2"
org_v2beta "github.com/zitadel/zitadel/internal/api/grpc/org/v2beta" 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" "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" 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" userschema_v3_alpha "github.com/zitadel/zitadel/internal/api/grpc/resources/userschema/v3alpha"
@@ -72,12 +74,14 @@ import (
"github.com/zitadel/zitadel/internal/authz" "github.com/zitadel/zitadel/internal/authz"
authz_repo "github.com/zitadel/zitadel/internal/authz/repository" authz_repo "github.com/zitadel/zitadel/internal/authz/repository"
authz_es "github.com/zitadel/zitadel/internal/authz/repository/eventsourcing/eventstore" 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/cache/connector"
"github.com/zitadel/zitadel/internal/command" "github.com/zitadel/zitadel/internal/command"
"github.com/zitadel/zitadel/internal/crypto" "github.com/zitadel/zitadel/internal/crypto"
cryptoDB "github.com/zitadel/zitadel/internal/crypto/database" cryptoDB "github.com/zitadel/zitadel/internal/crypto/database"
"github.com/zitadel/zitadel/internal/database" "github.com/zitadel/zitadel/internal/database"
"github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/domain/federatedlogout"
"github.com/zitadel/zitadel/internal/eventstore" "github.com/zitadel/zitadel/internal/eventstore"
old_es "github.com/zitadel/zitadel/internal/eventstore/repository/sql" old_es "github.com/zitadel/zitadel/internal/eventstore/repository/sql"
new_es "github.com/zitadel/zitadel/internal/eventstore/v3" 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( execution.Register(
ctx, ctx,
config.Projections.Customizations["executions"], config.Projections.Customizations["execution_handler"],
config.Executions, config.Executions,
queries, queries,
eventstoreClient.EventTypes(), 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 { if err := apis.RegisterServer(ctx, system.CreateServer(commands, queries, config.Database.DatabaseName(), config.DefaultInstance, config.ExternalDomain), tlsConfig); err != nil {
return nil, err 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 { if err := apis.RegisterServer(ctx, admin.CreateServer(config.Database.DatabaseName(), commands, queries, keys.User, config.AuditLogRetention), tlsConfig); err != nil {
return nil, err 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 { 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 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 return nil, err
} }
if err := apis.RegisterService(ctx, session_v2beta.CreateServer(commands, queries, permissionCheck)); err != nil { 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 { if err := apis.RegisterService(ctx, settings_v2beta.CreateServer(commands, queries)); err != nil {
return nil, err 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 return nil, err
} }
if err := apis.RegisterService(ctx, feature_v2beta.CreateServer(commands, queries)); err != nil { 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 { if err := apis.RegisterService(ctx, action_v2_beta.CreateServer(config.SystemDefaults, commands, queries, domain.AllActionFunctions, apis.ListGrpcMethods, apis.ListGrpcServices)); err != nil {
return nil, err 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 { if err := apis.RegisterService(ctx, userschema_v3_alpha.CreateServer(config.SystemDefaults, commands, queries)); err != nil {
return nil, err return nil, err
} }
@@ -503,7 +513,12 @@ func startAPIs(
assetsCache := middleware.AssetsCacheInterceptor(config.AssetStorage.Cache.MaxAge, config.AssetStorage.Cache.SharedMaxAge) 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(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) userAgentInterceptor, err := middleware.NewUserAgentHandler(config.UserAgentCookie, keys.UserAgentCookieKey, id.SonyFlakeGenerator(), config.ExternalSecure, login.EndpointResources, login.EndpointExternalLoginCallbackFormPost, login.EndpointSAMLACS)
if err != nil { if err != nil {
@@ -524,7 +539,25 @@ func startAPIs(
} }
apis.RegisterHandlerOnPrefix(openapi.HandlerPrefix, openAPIHandler) 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 { if err != nil {
return nil, fmt.Errorf("unable to start oidc provider: %w", err) return nil, fmt.Errorf("unable to start oidc provider: %w", err)
} }
@@ -573,6 +606,7 @@ func startAPIs(
keys.IDPConfig, keys.IDPConfig,
keys.CSRFCookieKey, keys.CSRFCookieKey,
cacheConnectors, cacheConnectors,
federatedLogoutsCache,
) )
if err != nil { if err != nil {
return nil, fmt.Errorf("unable to start login: %w", err) return nil, fmt.Errorf("unable to start login: %w", err)

View File

@@ -1,6 +1,8 @@
package start package start
import ( import (
"context"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/spf13/viper" "github.com/spf13/viper"
"github.com/zitadel/logging" "github.com/zitadel/logging"
@@ -29,14 +31,19 @@ Requirements:
masterKey, err := key.MasterKey(cmd) masterKey, err := key.MasterKey(cmd)
logging.OnError(err).Panic("No master key provided") 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) err = setup.BindInitProjections(cmd)
logging.OnError(err).Fatal("unable to bind \"init-projections\" flag") logging.OnError(err).Fatal("unable to bind \"init-projections\" flag")
setupConfig := setup.MustNewConfig(viper.GetViper()) setupConfig := setup.MustNewConfig(viper.GetViper())
setupSteps := setup.MustNewSteps(viper.New()) 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()) startConfig := MustNewConfig(viper.GetViper())

View File

@@ -1,6 +1,8 @@
package start package start
import ( import (
"context"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/spf13/viper" "github.com/spf13/viper"
"github.com/zitadel/logging" "github.com/zitadel/logging"
@@ -34,7 +36,10 @@ Requirements:
setupConfig := setup.MustNewConfig(viper.GetViper()) setupConfig := setup.MustNewConfig(viper.GetViper())
setupSteps := setup.MustNewSteps(viper.New()) 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()) startConfig := MustNewConfig(viper.GetViper())

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,4 +1,7 @@
<h2>{{ 'ACTIONSTWO.EXECUTION.TITLE' | translate }}</h2> <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> <p class="cnsl-secondary-text">{{ 'ACTIONSTWO.EXECUTION.DESCRIPTION' | translate }}</p>
<cnsl-actions-two-actions-table <cnsl-actions-two-actions-table

View File

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

View File

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

View File

@@ -10,12 +10,7 @@ import {
} from './actions-two-add-action-condition/actions-two-add-action-condition.component'; } 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 { ActionsTwoAddActionTargetComponent } from './actions-two-add-action-target/actions-two-add-action-target.component';
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { import { Condition, Execution } from '@zitadel/proto/zitadel/action/v2beta/execution_pb';
Condition,
Execution,
ExecutionTargetType,
ExecutionTargetTypeSchema,
} from '@zitadel/proto/zitadel/action/v2beta/execution_pb';
import { Subject } from 'rxjs'; import { Subject } from 'rxjs';
import { SetExecutionRequestSchema } from '@zitadel/proto/zitadel/action/v2beta/action_service_pb'; 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 }> }; export type CorrectlyTypedCondition = Condition & { conditionType: Extract<Condition['conditionType'], { case: string }> };
type CorrectlyTypedTargets = { type: Extract<ExecutionTargetType['type'], { case: 'target' }> }; export type CorrectlyTypedExecution = Omit<Execution, 'condition'> & {
export type CorrectlyTypedExecution = Omit<Execution, 'targets' | 'condition'> & {
condition: CorrectlyTypedCondition; condition: CorrectlyTypedCondition;
targets: CorrectlyTypedTargets[];
}; };
export const correctlyTypeExecution = (execution: Execution): CorrectlyTypedExecution => { export const correctlyTypeExecution = (execution: Execution): CorrectlyTypedExecution => {
@@ -48,9 +40,6 @@ export const correctlyTypeExecution = (execution: Execution): CorrectlyTypedExec
return { return {
...execution, ...execution,
condition, 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 typeSignal = signal<ConditionType>('request');
protected readonly conditionSignal = signal<MessageInitShape<typeof SetExecutionRequestSchema>['condition']>(undefined); 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>(); protected readonly continueSubject = new Subject<void>();
@@ -112,7 +101,7 @@ export class ActionTwoAddActionDialogComponent {
this.targetsSignal.set(data.execution.targets); this.targetsSignal.set(data.execution.targets);
this.typeSignal.set(data.execution.condition.conditionType.case); this.typeSignal.set(data.execution.condition.conditionType.case);
this.conditionSignal.set(data.execution.condition); 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 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> <p class="target-description">{{ 'ACTIONSTWO.EXECUTION.DIALOG.TARGET.DESCRIPTION' | translate }}</p>
<cnsl-form-field class="full-width"> <cnsl-form-field class="full-width">
@@ -8,9 +8,9 @@
#trigger="matAutocompleteTrigger" #trigger="matAutocompleteTrigger"
#input #input
type="text" type="text"
[formControl]="form().controls.autocomplete" [formControl]="form.controls.autocomplete"
[matAutocomplete]="autoservice" [matAutocomplete]="autoservice"
(keydown.enter)="handleEnter($event); input.blur(); trigger.closePanel()" (keydown.enter)="handleEnter($event, form); input.blur(); trigger.closePanel()"
/> />
<mat-autocomplete #autoservice="matAutocomplete"> <mat-autocomplete #autoservice="matAutocomplete">
<mat-option *ngIf="targets().state === 'loading'" class="is-loading"> <mat-option *ngIf="targets().state === 'loading'" class="is-loading">
@@ -19,7 +19,7 @@
<mat-option <mat-option
*ngFor="let target of selectableTargets(); trackBy: trackTarget" *ngFor="let target of selectableTargets(); trackBy: trackTarget"
#option #option
(click)="addTarget(target); option.deselect()" (click)="addTarget(target, form); option.deselect()"
[value]="target.name" [value]="target.name"
> >
{{ target.name }} {{ target.name }}
@@ -27,7 +27,7 @@
</mat-autocomplete> </mat-autocomplete>
</cnsl-form-field> </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"> <ng-container matColumnDef="order">
<th mat-header-cell *matHeaderCellDef>Reorder</th> <th mat-header-cell *matHeaderCellDef>Reorder</th>
<td mat-cell *cnslCellDef="let row; let i = index; dataSource: dataSource"> <td mat-cell *cnslCellDef="let row; let i = index; dataSource: dataSource">
@@ -48,7 +48,7 @@
actions actions
matTooltip="{{ 'ACTIONS.REMOVE' | translate }}" matTooltip="{{ 'ACTIONS.REMOVE' | translate }}"
color="warn" color="warn"
(click)="removeTarget(i)" (click)="removeTarget(i, form)"
mat-icon-button mat-icon-button
> >
<i class="las la-trash"></i> <i class="las la-trash"></i>
@@ -65,7 +65,7 @@
{{ 'ACTIONS.BACK' | translate }} {{ 'ACTIONS.BACK' | translate }}
</button> </button>
<span class="fill-space"></span> <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 }} {{ 'ACTIONS.CONTINUE' | translate }}
</button> </button>
</div> </div>

View File

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

View File

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

View File

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

View File

@@ -1,4 +1,7 @@
<h2>{{ 'ACTIONSTWO.TARGET.TITLE' | translate }}</h2> <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> <p class="cnsl-secondary-text">{{ 'ACTIONSTWO.TARGET.DESCRIPTION' | translate }}</p>
<cnsl-actions-two-targets-table <cnsl-actions-two-targets-table

View File

@@ -12,6 +12,7 @@ import {
CreateTargetRequestSchema, CreateTargetRequestSchema,
UpdateTargetRequestSchema, UpdateTargetRequestSchema,
} from '@zitadel/proto/zitadel/action/v2beta/action_service_pb'; } from '@zitadel/proto/zitadel/action/v2beta/action_service_pb';
import { InfoSectionType } from '../../info-section/info-section.component';
@Component({ @Component({
selector: 'cnsl-actions-two-targets', selector: 'cnsl-actions-two-targets',
@@ -76,7 +77,8 @@ export class ActionsTwoTargetsComponent {
if ('id' in request) { if ('id' in request) {
await this.actionService.updateTarget(request); await this.actionService.updateTarget(request);
} else { } 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)); await new Promise((res) => setTimeout(res, 1000));
@@ -86,4 +88,6 @@ export class ActionsTwoTargetsComponent {
this.toast.showError(error); 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 { ActionConditionPipeModule } from 'src/app/pipes/action-condition-pipe/action-condition-pipe.module';
import { MatSelectModule } from '@angular/material/select'; import { MatSelectModule } from '@angular/material/select';
import { MatIconModule } from '@angular/material/icon'; import { MatIconModule } from '@angular/material/icon';
import { InfoSectionModule } from '../info-section/info-section.module';
@NgModule({ @NgModule({
declarations: [ declarations: [
@@ -47,6 +48,7 @@ import { MatIconModule } from '@angular/material/icon';
TypeSafeCellDefModule, TypeSafeCellDefModule,
ProjectRoleChipModule, ProjectRoleChipModule,
ActionConditionPipeModule, ActionConditionPipeModule,
InfoSectionModule,
], ],
exports: [ActionsTwoActionsComponent, ActionsTwoTargetsComponent, ActionsTwoTargetsTableComponent], exports: [ActionsTwoActionsComponent, ActionsTwoTargetsComponent, ActionsTwoTargetsTableComponent],
}) })

View File

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

View File

@@ -69,4 +69,19 @@
</cnsl-form-field> </cnsl-form-field>
</div> </div>
</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> </cnsl-filter>

View File

@@ -3,7 +3,14 @@ import { MatCheckboxChange } from '@angular/material/checkbox';
import { ActivatedRoute, Router } from '@angular/router'; import { ActivatedRoute, Router } from '@angular/router';
import { take } from 'rxjs'; import { take } from 'rxjs';
import { TextQueryMethod } from 'src/app/proto/generated/zitadel/object_pb'; 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 { UserNameQuery } from 'src/app/proto/generated/zitadel/user_pb';
import { FilterComponent } from '../filter/filter.component'; import { FilterComponent } from '../filter/filter.component';
@@ -12,6 +19,7 @@ enum SubQuery {
NAME, NAME,
STATE, STATE,
DOMAIN, DOMAIN,
ID,
} }
@Component({ @Component({
@@ -61,6 +69,12 @@ export class FilterOrgComponent extends FilterComponent implements OnInit {
orgDomainQuery.setMethod(filter.domainQuery.method); orgDomainQuery.setMethod(filter.domainQuery.method);
orgQuery.setDomainQuery(orgDomainQuery); orgQuery.setDomainQuery(orgDomainQuery);
return orgQuery; 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 { } else {
return undefined; return undefined;
} }
@@ -100,6 +114,13 @@ export class FilterOrgComponent extends FilterComponent implements OnInit {
odq.setDomainQuery(dq); odq.setDomainQuery(dq);
this.searchQueries.push(odq); this.searchQueries.push(odq);
break; break;
case SubQuery.ID:
const idq = new OrgIDQuery();
idq.setId('');
const oidq = new OrgQuery();
oidq.setIdQuery(idq);
this.searchQueries.push(oidq);
break;
} }
} else { } else {
switch (subquery) { switch (subquery) {
@@ -121,6 +142,12 @@ export class FilterOrgComponent extends FilterComponent implements OnInit {
this.searchQueries.splice(index_pdn, 1); this.searchQueries.splice(index_pdn, 1);
} }
break; 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); (query as OrgDomainQuery).setDomain(value);
this.filterChanged.emit(this.searchQueries ? this.searchQueries : []); this.filterChanged.emit(this.searchQueries ? this.searchQueries : []);
break; 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 { } else {
return undefined; 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 { ComponentFixture, TestBed } from '@angular/core/testing';
import { FilterUserComponent } from './filter-user.component'; import { FilterProjectComponent } from './filter-project.component';
describe('FilterUserComponent', () => { describe('FilterUserComponent', () => {
let component: FilterUserComponent; let component: FilterProjectComponent;
let fixture: ComponentFixture<FilterUserComponent>; let fixture: ComponentFixture<FilterProjectComponent>;
beforeEach(async () => { beforeEach(async () => {
await TestBed.configureTestingModule({ await TestBed.configureTestingModule({
declarations: [FilterUserComponent], declarations: [FilterProjectComponent],
}).compileComponents(); }).compileComponents();
}); });
beforeEach(() => { beforeEach(() => {
fixture = TestBed.createComponent(FilterUserComponent); fixture = TestBed.createComponent(FilterProjectComponent);
component = fixture.componentInstance; component = fixture.componentInstance;
fixture.detectChanges(); fixture.detectChanges();
}); });

View File

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

View File

@@ -1,19 +1,19 @@
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { AvatarComponent } from './avatar.component'; import { LabelComponent } from './label.component';
describe('AvatarComponent', () => { describe('AvatarComponent', () => {
let component: AvatarComponent; let component: LabelComponent;
let fixture: ComponentFixture<AvatarComponent>; let fixture: ComponentFixture<LabelComponent>;
beforeEach(waitForAsync(() => { beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({ TestBed.configureTestingModule({
declarations: [AvatarComponent], declarations: [LabelComponent],
}).compileComponents(); }).compileComponents();
})); }));
beforeEach(() => { beforeEach(() => {
fixture = TestBed.createComponent(AvatarComponent); fixture = TestBed.createComponent(LabelComponent);
component = fixture.componentInstance; component = fixture.componentInstance;
fixture.detectChanges(); 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 { ToastService } from 'src/app/services/toast.service';
import { Metadata as MetadataV2 } from '@zitadel/proto/zitadel/metadata_pb'; import { Metadata as MetadataV2 } from '@zitadel/proto/zitadel/metadata_pb';
import { Metadata } from 'src/app/proto/generated/zitadel/metadata_pb'; import { Metadata } from 'src/app/proto/generated/zitadel/metadata_pb';
import { Buffer } from 'buffer';
export type MetadataDialogData = { export type MetadataDialogData = {
metadata: (Metadata.AsObject | MetadataV2)[]; metadata: (Metadata.AsObject | MetadataV2)[];
@@ -26,9 +25,10 @@ export class MetadataDialogComponent {
public dialogRef: MatDialogRef<MetadataDialogComponent>, public dialogRef: MatDialogRef<MetadataDialogComponent>,
@Inject(MAT_DIALOG_DATA) public data: MetadataDialogData, @Inject(MAT_DIALOG_DATA) public data: MetadataDialogData,
) { ) {
const decoder = new TextDecoder();
this.metadata = data.metadata.map(({ key, value }) => ({ this.metadata = data.metadata.map(({ key, value }) => ({
key, 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 { Metadata as MetadataV2 } from '@zitadel/proto/zitadel/metadata_pb';
import { map, startWith } from 'rxjs/operators'; import { map, startWith } from 'rxjs/operators';
import { Metadata } from 'src/app/proto/generated/zitadel/metadata_pb'; import { Metadata } from 'src/app/proto/generated/zitadel/metadata_pb';
import { Buffer } from 'buffer';
type StringMetadata = { type StringMetadata = {
key: string; key: string;
@@ -37,12 +36,13 @@ export class MetadataComponent implements OnInit {
ngOnInit() { ngOnInit() {
this.dataSource$ = this.metadata$.pipe( this.dataSource$ = this.metadata$.pipe(
map((metadata) => map((metadata) => {
metadata.map(({ key, value }) => ({ const decoder = new TextDecoder();
return metadata.map(({ key, value }) => ({
key, key,
value: Buffer.from(value as any as string, 'base64').toString('utf-8'), value: typeof value === 'string' ? value : decoder.decode(value),
})), }));
), }),
startWith([] as StringMetadata[]), startWith([] as StringMetadata[]),
map((metadata) => new MatTableDataSource(metadata)), 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 { AbstractControl, UntypedFormBuilder, UntypedFormGroup } from '@angular/forms';
import { MatDialog } from '@angular/material/dialog'; import { MatDialog } from '@angular/material/dialog';
import { Duration } from 'google-protobuf/google/protobuf/duration_pb'; 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 { import {
GetLoginPolicyResponse as AdminGetLoginPolicyResponse, GetLoginPolicyResponse as AdminGetLoginPolicyResponse,
UpdateLoginPolicyRequest, UpdateLoginPolicyRequest,
UpdateLoginPolicyResponse,
} from 'src/app/proto/generated/zitadel/admin_pb'; } from 'src/app/proto/generated/zitadel/admin_pb';
import { import {
AddCustomLoginPolicyRequest,
GetLoginPolicyResponse as MgmtGetLoginPolicyResponse, GetLoginPolicyResponse as MgmtGetLoginPolicyResponse,
UpdateCustomLoginPolicyRequest, UpdateCustomLoginPolicyRequest,
} from 'src/app/proto/generated/zitadel/management_pb'; } 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 { WarnDialogComponent } from '../../warn-dialog/warn-dialog.component';
import { PolicyComponentServiceType } from '../policy-component-types.enum'; import { PolicyComponentServiceType } from '../policy-component-types.enum';
import { LoginMethodComponentType } from './factor-table/factor-table.component'; import { LoginMethodComponentType } from './factor-table/factor-table.component';
import { catchError, map, takeUntil } from 'rxjs/operators'; import { map, takeUntil } from 'rxjs/operators';
import { error } from 'console';
import { LoginPolicyService } from '../../../services/login-policy.service'; import { LoginPolicyService } from '../../../services/login-policy.service';
const minValueValidator = (minValue: number) => (control: AbstractControl) => { const minValueValidator = (minValue: number) => (control: AbstractControl) => {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -127,6 +127,7 @@ export class ProviderSamlSpComponent {
withSignedRequest: new UntypedFormControl(true, [requiredValidator]), withSignedRequest: new UntypedFormControl(true, [requiredValidator]),
nameIdFormat: new UntypedFormControl(SAMLNameIDFormat.SAML_NAME_ID_FORMAT_PERSISTENT, []), nameIdFormat: new UntypedFormControl(SAMLNameIDFormat.SAML_NAME_ID_FORMAT_PERSISTENT, []),
transientMappingAttributeName: new UntypedFormControl('', []), transientMappingAttributeName: new UntypedFormControl('', []),
federatedLogoutEnabled: new UntypedFormControl(false, []),
}, },
atLeastOneIsFilled('metadataXml', 'metadataUrl'), atLeastOneIsFilled('metadataXml', 'metadataUrl'),
); );
@@ -210,6 +211,7 @@ export class ProviderSamlSpComponent {
// @ts-ignore // @ts-ignore
req.setNameIdFormat(SAMLNameIDFormat[this.nameIDFormat?.value]); req.setNameIdFormat(SAMLNameIDFormat[this.nameIDFormat?.value]);
req.setTransientMappingAttributeName(this.transientMapping?.value); req.setTransientMappingAttributeName(this.transientMapping?.value);
req.setFederatedLogoutEnabled(this.federatedLogoutEnabled?.value);
req.setProviderOptions(this.options); req.setProviderOptions(this.options);
this.loading = true; this.loading = true;
@@ -250,6 +252,7 @@ export class ProviderSamlSpComponent {
req.setNameIdFormat(SAMLNameIDFormat[this.nameIDFormat.value]); req.setNameIdFormat(SAMLNameIDFormat[this.nameIDFormat.value]);
} }
req.setTransientMappingAttributeName(this.transientMapping?.value); req.setTransientMappingAttributeName(this.transientMapping?.value);
req.setFederatedLogoutEnabled(this.federatedLogoutEnabled?.value);
this.loading = true; this.loading = true;
this.service this.service
.addSAMLProvider(req) .addSAMLProvider(req)
@@ -335,4 +338,8 @@ export class ProviderSamlSpComponent {
private get transientMapping(): AbstractControl | null { private get transientMapping(): AbstractControl | null {
return this.form.get('transientMappingAttributeName'); 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', i18nKey: 'SETTINGS.LIST.ACTIONS',
groupI18nKey: 'SETTINGS.GROUPS.ACTIONS', groupI18nKey: 'SETTINGS.GROUPS.ACTIONS',
requiredRoles: { requiredRoles: {
// todo: figure out roles [PolicyComponentServiceType.ADMIN]: ['action.execution.write', 'action.target.write'],
[PolicyComponentServiceType.ADMIN]: ['iam.policy.read'],
}, },
beta: true,
}; };
export const ACTIONS_TARGETS: SidenavSetting = { export const ACTIONS_TARGETS: SidenavSetting = {
@@ -238,7 +238,7 @@ export const ACTIONS_TARGETS: SidenavSetting = {
i18nKey: 'SETTINGS.LIST.TARGETS', i18nKey: 'SETTINGS.LIST.TARGETS',
groupI18nKey: 'SETTINGS.GROUPS.ACTIONS', groupI18nKey: 'SETTINGS.GROUPS.ACTIONS',
requiredRoles: { requiredRoles: {
// todo: figure out roles [PolicyComponentServiceType.ADMIN]: ['action.execution.write', 'action.target.write'],
[PolicyComponentServiceType.ADMIN]: ['iam.policy.read'],
}, },
beta: true,
}; };

View File

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

View File

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

View File

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

View File

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

View File

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

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