mirror of
https://github.com/zitadel/zitadel.git
synced 2025-08-14 11:37:59 +00:00
Merge branch 'instance_table_2' into org_table
This commit is contained in:
2
.github/workflows/build.yml
vendored
2
.github/workflows/build.yml
vendored
@@ -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 }}
|
||||||
|
|
||||||
|
164
API_DESIGN.md
164
API_DESIGN.md
@@ -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
|
||||||
|
@@ -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)
|
||||||
[]
|

|
||||||
|
|
||||||
## Security
|
## Security
|
||||||
|
|
||||||
|
@@ -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
|
||||||
|
// }
|
||||||
|
@@ -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
|
||||||
}
|
// }
|
||||||
|
@@ -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
|
||||||
|
// }
|
||||||
|
@@ -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
|
||||||
}
|
// }
|
||||||
|
@@ -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
|
||||||
|
// }
|
||||||
|
@@ -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)
|
||||||
|
@@ -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
|
||||||
}
|
// }
|
||||||
|
@@ -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)
|
||||||
|
@@ -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
|
||||||
|
// }
|
||||||
|
@@ -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)
|
||||||
})
|
// })
|
||||||
}
|
// }
|
||||||
|
@@ -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
|
||||||
}
|
// }
|
||||||
|
@@ -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
|
||||||
|
@@ -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
|
||||||
}
|
// }
|
||||||
|
@@ -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)
|
||||||
)
|
// )
|
||||||
|
@@ -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
|
||||||
}
|
// }
|
||||||
|
@@ -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)
|
||||||
|
@@ -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)
|
||||||
}
|
}
|
||||||
|
@@ -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].
|
||||||
|
@@ -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();
|
||||||
|
@@ -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
|
||||||
|
@@ -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)
|
||||||
|
@@ -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),
|
||||||
)
|
)
|
||||||
|
@@ -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) {
|
||||||
|
@@ -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())
|
||||||
|
|
||||||
|
@@ -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)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -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
|
||||||
}
|
}
|
||||||
|
@@ -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
|
||||||
}
|
}
|
||||||
|
@@ -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)
|
||||||
}
|
// }
|
||||||
|
@@ -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
@@ -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 {
|
||||||
|
@@ -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
|
||||||
|
@@ -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:
|
||||||
|
@@ -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(
|
||||||
|
@@ -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)
|
||||||
|
@@ -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 {
|
||||||
|
@@ -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")
|
||||||
}
|
}
|
||||||
|
@@ -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 {
|
||||||
|
@@ -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
1
cmd/setup/54.sql
Normal 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
27
cmd/setup/55.go
Normal 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
22
cmd/setup/55.sql
Normal 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
27
cmd/setup/56.go
Normal 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
1
cmd/setup/56.sql
Normal 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
27
cmd/setup/57.go
Normal 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
106
cmd/setup/57.sql
Normal 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
49
cmd/setup/58.go
Normal 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"
|
||||||
|
}
|
36
cmd/setup/58/01_update_login_names3_view.sql
Normal file
36
cmd/setup/58/01_update_login_names3_view.sql
Normal 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
|
1
cmd/setup/58/02_create_index.sql
Normal file
1
cmd/setup/58/02_create_index.sql
Normal 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
32
cmd/setup/59.go
Normal 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
|
||||||
|
}
|
@@ -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 {
|
||||||
|
@@ -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
125
cmd/setup/trigger_steps.go
Normal 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",
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
@@ -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)
|
||||||
|
@@ -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())
|
||||||
|
|
||||||
|
@@ -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())
|
||||||
|
|
||||||
|
@@ -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",
|
||||||
|
@@ -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();
|
||||||
});
|
});
|
||||||
|
@@ -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>
|
||||||
|
@@ -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 };
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@@ -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
|
||||||
|
@@ -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;
|
||||||
}
|
}
|
||||||
|
@@ -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>
|
||||||
|
@@ -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
|
||||||
}
|
}
|
||||||
|
@@ -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>
|
||||||
|
@@ -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) {
|
||||||
|
@@ -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">
|
||||||
|
@@ -23,3 +23,7 @@
|
|||||||
.name-hint {
|
.name-hint {
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.types-description {
|
||||||
|
white-space: pre-line;
|
||||||
|
}
|
||||||
|
@@ -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
|
||||||
|
@@ -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;
|
||||||
}
|
}
|
||||||
|
@@ -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],
|
||||||
})
|
})
|
||||||
|
@@ -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();
|
||||||
});
|
});
|
||||||
|
@@ -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>
|
||||||
|
@@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -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();
|
||||||
});
|
});
|
||||||
|
@@ -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 }"
|
||||||
>
|
>
|
||||||
|
@@ -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();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@@ -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();
|
||||||
});
|
});
|
||||||
|
@@ -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),
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -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)),
|
||||||
);
|
);
|
||||||
|
@@ -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) => {
|
||||||
|
@@ -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();
|
||||||
});
|
});
|
||||||
|
@@ -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();
|
||||||
});
|
});
|
||||||
|
@@ -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();
|
||||||
});
|
});
|
||||||
|
@@ -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();
|
||||||
});
|
});
|
||||||
|
@@ -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();
|
||||||
});
|
});
|
||||||
|
@@ -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();
|
||||||
});
|
});
|
||||||
|
@@ -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>
|
||||||
|
|
||||||
|
@@ -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
|
||||||
|
@@ -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;
|
||||||
}
|
}
|
||||||
|
@@ -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');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -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,
|
||||||
};
|
};
|
||||||
|
@@ -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();
|
||||||
});
|
});
|
||||||
|
@@ -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>
|
||||||
|
@@ -90,6 +90,10 @@
|
|||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.state {
|
||||||
|
margin-left: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
span {
|
span {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
|
@@ -11,6 +11,7 @@ export interface SidenavSetting {
|
|||||||
[PolicyComponentServiceType.ADMIN]?: string[];
|
[PolicyComponentServiceType.ADMIN]?: string[];
|
||||||
};
|
};
|
||||||
showWarn?: boolean;
|
showWarn?: boolean;
|
||||||
|
beta?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
|
@@ -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
Reference in New Issue
Block a user