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

|
||||
|
||||
## Security
|
||||
|
||||
|
@@ -1,19 +1,21 @@
|
||||
package v2
|
||||
|
||||
import (
|
||||
"github.com/zitadel/zitadel/backend/v3/telemetry/logging"
|
||||
"github.com/zitadel/zitadel/backend/v3/telemetry/tracing"
|
||||
)
|
||||
// this file has been commented out to pass the linter
|
||||
|
||||
var (
|
||||
logger logging.Logger
|
||||
tracer tracing.Tracer
|
||||
)
|
||||
// import (
|
||||
// "github.com/zitadel/zitadel/backend/v3/telemetry/logging"
|
||||
// "github.com/zitadel/zitadel/backend/v3/telemetry/tracing"
|
||||
// )
|
||||
|
||||
func SetLogger(l logging.Logger) {
|
||||
logger = l
|
||||
}
|
||||
// var (
|
||||
// logger logging.Logger
|
||||
// tracer tracing.Tracer
|
||||
// )
|
||||
|
||||
func SetTracer(t tracing.Tracer) {
|
||||
tracer = t
|
||||
}
|
||||
// func SetLogger(l logging.Logger) {
|
||||
// logger = l
|
||||
// }
|
||||
|
||||
// func SetTracer(t tracing.Tracer) {
|
||||
// tracer = t
|
||||
// }
|
||||
|
@@ -1,33 +1,33 @@
|
||||
package orgv2
|
||||
|
||||
import (
|
||||
"context"
|
||||
// import (
|
||||
// "context"
|
||||
|
||||
"github.com/zitadel/zitadel/backend/v3/domain"
|
||||
"github.com/zitadel/zitadel/pkg/grpc/org/v2"
|
||||
)
|
||||
// "github.com/zitadel/zitadel/backend/v3/domain"
|
||||
// "github.com/zitadel/zitadel/pkg/grpc/org/v2"
|
||||
// )
|
||||
|
||||
func CreateOrg(ctx context.Context, req *org.AddOrganizationRequest) (resp *org.AddOrganizationResponse, err error) {
|
||||
cmd := domain.NewAddOrgCommand(
|
||||
req.GetName(),
|
||||
addOrgAdminToCommand(req.GetAdmins()...)...,
|
||||
)
|
||||
err = domain.Invoke(ctx, cmd)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &org.AddOrganizationResponse{
|
||||
OrganizationId: cmd.ID,
|
||||
}, nil
|
||||
}
|
||||
// func CreateOrg(ctx context.Context, req *org.AddOrganizationRequest) (resp *org.AddOrganizationResponse, err error) {
|
||||
// cmd := domain.NewAddOrgCommand(
|
||||
// req.GetName(),
|
||||
// addOrgAdminToCommand(req.GetAdmins()...)...,
|
||||
// )
|
||||
// err = domain.Invoke(ctx, cmd)
|
||||
// if err != nil {
|
||||
// return nil, err
|
||||
// }
|
||||
// return &org.AddOrganizationResponse{
|
||||
// OrganizationId: cmd.ID,
|
||||
// }, nil
|
||||
// }
|
||||
|
||||
func addOrgAdminToCommand(admins ...*org.AddOrganizationRequest_Admin) []*domain.AddMemberCommand {
|
||||
cmds := make([]*domain.AddMemberCommand, len(admins))
|
||||
for i, admin := range admins {
|
||||
cmds[i] = &domain.AddMemberCommand{
|
||||
UserID: admin.GetUserId(),
|
||||
Roles: admin.GetRoles(),
|
||||
}
|
||||
}
|
||||
return cmds
|
||||
}
|
||||
// func addOrgAdminToCommand(admins ...*org.AddOrganizationRequest_Admin) []*domain.AddMemberCommand {
|
||||
// cmds := make([]*domain.AddMemberCommand, len(admins))
|
||||
// for i, admin := range admins {
|
||||
// cmds[i] = &domain.AddMemberCommand{
|
||||
// UserID: admin.GetUserId(),
|
||||
// Roles: admin.GetRoles(),
|
||||
// }
|
||||
// }
|
||||
// return cmds
|
||||
// }
|
||||
|
@@ -1,19 +1,21 @@
|
||||
package orgv2
|
||||
|
||||
import (
|
||||
"github.com/zitadel/zitadel/backend/v3/telemetry/logging"
|
||||
"github.com/zitadel/zitadel/backend/v3/telemetry/tracing"
|
||||
)
|
||||
// this file has been commented out to pass the linter
|
||||
|
||||
var (
|
||||
logger logging.Logger
|
||||
tracer tracing.Tracer
|
||||
)
|
||||
// import (
|
||||
// "github.com/zitadel/zitadel/backend/v3/telemetry/logging"
|
||||
// "github.com/zitadel/zitadel/backend/v3/telemetry/tracing"
|
||||
// )
|
||||
|
||||
func SetLogger(l logging.Logger) {
|
||||
logger = l
|
||||
}
|
||||
// var (
|
||||
// logger logging.Logger
|
||||
// tracer tracing.Tracer
|
||||
// )
|
||||
|
||||
func SetTracer(t tracing.Tracer) {
|
||||
tracer = t
|
||||
}
|
||||
// func SetLogger(l logging.Logger) {
|
||||
// logger = l
|
||||
// }
|
||||
|
||||
// func SetTracer(t tracing.Tracer) {
|
||||
// tracer = t
|
||||
// }
|
||||
|
@@ -1,93 +1,93 @@
|
||||
package userv2
|
||||
|
||||
import (
|
||||
"context"
|
||||
// import (
|
||||
// "context"
|
||||
|
||||
"github.com/zitadel/zitadel/backend/v3/domain"
|
||||
"github.com/zitadel/zitadel/pkg/grpc/user/v2"
|
||||
)
|
||||
// "github.com/zitadel/zitadel/backend/v3/domain"
|
||||
// "github.com/zitadel/zitadel/pkg/grpc/user/v2"
|
||||
// )
|
||||
|
||||
func SetEmail(ctx context.Context, req *user.SetEmailRequest) (resp *user.SetEmailResponse, err error) {
|
||||
var (
|
||||
verification domain.SetEmailOpt
|
||||
returnCode *domain.ReturnCodeCommand
|
||||
)
|
||||
// func SetEmail(ctx context.Context, req *user.SetEmailRequest) (resp *user.SetEmailResponse, err error) {
|
||||
// var (
|
||||
// verification domain.SetEmailOpt
|
||||
// returnCode *domain.ReturnCodeCommand
|
||||
// )
|
||||
|
||||
switch req.GetVerification().(type) {
|
||||
case *user.SetEmailRequest_IsVerified:
|
||||
verification = domain.NewEmailVerifiedCommand(req.GetUserId(), req.GetIsVerified())
|
||||
case *user.SetEmailRequest_SendCode:
|
||||
verification = domain.NewSendCodeCommand(req.GetUserId(), req.GetSendCode().UrlTemplate)
|
||||
case *user.SetEmailRequest_ReturnCode:
|
||||
returnCode = domain.NewReturnCodeCommand(req.GetUserId())
|
||||
verification = returnCode
|
||||
default:
|
||||
verification = domain.NewSendCodeCommand(req.GetUserId(), nil)
|
||||
}
|
||||
// switch req.GetVerification().(type) {
|
||||
// case *user.SetEmailRequest_IsVerified:
|
||||
// verification = domain.NewEmailVerifiedCommand(req.GetUserId(), req.GetIsVerified())
|
||||
// case *user.SetEmailRequest_SendCode:
|
||||
// verification = domain.NewSendCodeCommand(req.GetUserId(), req.GetSendCode().UrlTemplate)
|
||||
// case *user.SetEmailRequest_ReturnCode:
|
||||
// returnCode = domain.NewReturnCodeCommand(req.GetUserId())
|
||||
// verification = returnCode
|
||||
// default:
|
||||
// verification = domain.NewSendCodeCommand(req.GetUserId(), nil)
|
||||
// }
|
||||
|
||||
err = domain.Invoke(ctx, domain.NewSetEmailCommand(req.GetUserId(), req.GetEmail(), verification))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// err = domain.Invoke(ctx, domain.NewSetEmailCommand(req.GetUserId(), req.GetEmail(), verification))
|
||||
// if err != nil {
|
||||
// return nil, err
|
||||
// }
|
||||
|
||||
var code *string
|
||||
if returnCode != nil && returnCode.Code != "" {
|
||||
code = &returnCode.Code
|
||||
}
|
||||
// var code *string
|
||||
// if returnCode != nil && returnCode.Code != "" {
|
||||
// code = &returnCode.Code
|
||||
// }
|
||||
|
||||
return &user.SetEmailResponse{
|
||||
VerificationCode: code,
|
||||
}, nil
|
||||
}
|
||||
// return &user.SetEmailResponse{
|
||||
// VerificationCode: code,
|
||||
// }, nil
|
||||
// }
|
||||
|
||||
func SendEmailCode(ctx context.Context, req *user.SendEmailCodeRequest) (resp *user.SendEmailCodeResponse, err error) {
|
||||
var (
|
||||
returnCode *domain.ReturnCodeCommand
|
||||
cmd domain.Commander
|
||||
)
|
||||
// func SendEmailCode(ctx context.Context, req *user.SendEmailCodeRequest) (resp *user.SendEmailCodeResponse, err error) {
|
||||
// var (
|
||||
// returnCode *domain.ReturnCodeCommand
|
||||
// cmd domain.Commander
|
||||
// )
|
||||
|
||||
switch req.GetVerification().(type) {
|
||||
case *user.SendEmailCodeRequest_SendCode:
|
||||
cmd = domain.NewSendCodeCommand(req.GetUserId(), req.GetSendCode().UrlTemplate)
|
||||
case *user.SendEmailCodeRequest_ReturnCode:
|
||||
returnCode = domain.NewReturnCodeCommand(req.GetUserId())
|
||||
cmd = returnCode
|
||||
default:
|
||||
cmd = domain.NewSendCodeCommand(req.GetUserId(), req.GetSendCode().UrlTemplate)
|
||||
}
|
||||
err = domain.Invoke(ctx, cmd)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
resp = new(user.SendEmailCodeResponse)
|
||||
if returnCode != nil {
|
||||
resp.VerificationCode = &returnCode.Code
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
// switch req.GetVerification().(type) {
|
||||
// case *user.SendEmailCodeRequest_SendCode:
|
||||
// cmd = domain.NewSendCodeCommand(req.GetUserId(), req.GetSendCode().UrlTemplate)
|
||||
// case *user.SendEmailCodeRequest_ReturnCode:
|
||||
// returnCode = domain.NewReturnCodeCommand(req.GetUserId())
|
||||
// cmd = returnCode
|
||||
// default:
|
||||
// cmd = domain.NewSendCodeCommand(req.GetUserId(), req.GetSendCode().UrlTemplate)
|
||||
// }
|
||||
// err = domain.Invoke(ctx, cmd)
|
||||
// if err != nil {
|
||||
// return nil, err
|
||||
// }
|
||||
// resp = new(user.SendEmailCodeResponse)
|
||||
// if returnCode != nil {
|
||||
// resp.VerificationCode = &returnCode.Code
|
||||
// }
|
||||
// return resp, nil
|
||||
// }
|
||||
|
||||
func ResendEmailCode(ctx context.Context, req *user.ResendEmailCodeRequest) (resp *user.SendEmailCodeResponse, err error) {
|
||||
var (
|
||||
returnCode *domain.ReturnCodeCommand
|
||||
cmd domain.Commander
|
||||
)
|
||||
// func ResendEmailCode(ctx context.Context, req *user.ResendEmailCodeRequest) (resp *user.SendEmailCodeResponse, err error) {
|
||||
// var (
|
||||
// returnCode *domain.ReturnCodeCommand
|
||||
// cmd domain.Commander
|
||||
// )
|
||||
|
||||
switch req.GetVerification().(type) {
|
||||
case *user.ResendEmailCodeRequest_SendCode:
|
||||
cmd = domain.NewSendCodeCommand(req.GetUserId(), req.GetSendCode().UrlTemplate)
|
||||
case *user.ResendEmailCodeRequest_ReturnCode:
|
||||
returnCode = domain.NewReturnCodeCommand(req.GetUserId())
|
||||
cmd = returnCode
|
||||
default:
|
||||
cmd = domain.NewSendCodeCommand(req.GetUserId(), req.GetSendCode().UrlTemplate)
|
||||
}
|
||||
err = domain.Invoke(ctx, cmd)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
resp = new(user.SendEmailCodeResponse)
|
||||
if returnCode != nil {
|
||||
resp.VerificationCode = &returnCode.Code
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
// switch req.GetVerification().(type) {
|
||||
// case *user.ResendEmailCodeRequest_SendCode:
|
||||
// cmd = domain.NewSendCodeCommand(req.GetUserId(), req.GetSendCode().UrlTemplate)
|
||||
// case *user.ResendEmailCodeRequest_ReturnCode:
|
||||
// returnCode = domain.NewReturnCodeCommand(req.GetUserId())
|
||||
// cmd = returnCode
|
||||
// default:
|
||||
// cmd = domain.NewSendCodeCommand(req.GetUserId(), req.GetSendCode().UrlTemplate)
|
||||
// }
|
||||
// err = domain.Invoke(ctx, cmd)
|
||||
// if err != nil {
|
||||
// return nil, err
|
||||
// }
|
||||
// resp = new(user.SendEmailCodeResponse)
|
||||
// if returnCode != nil {
|
||||
// resp.VerificationCode = &returnCode.Code
|
||||
// }
|
||||
// return resp, nil
|
||||
// }
|
||||
|
@@ -1,19 +1,19 @@
|
||||
package userv2
|
||||
|
||||
import (
|
||||
"github.com/zitadel/zitadel/backend/v3/telemetry/logging"
|
||||
"github.com/zitadel/zitadel/backend/v3/telemetry/tracing"
|
||||
)
|
||||
// this file has been commented out to pass the linter
|
||||
|
||||
var (
|
||||
logger logging.Logger
|
||||
tracer tracing.Tracer
|
||||
)
|
||||
// import (
|
||||
// "github.com/zitadel/zitadel/backend/v3/telemetry/logging"
|
||||
// "github.com/zitadel/zitadel/backend/v3/telemetry/tracing"
|
||||
// )
|
||||
|
||||
func SetLogger(l logging.Logger) {
|
||||
logger = l
|
||||
}
|
||||
// logger logging.Logger
|
||||
// var tracer tracing.Tracer
|
||||
|
||||
func SetTracer(t tracing.Tracer) {
|
||||
tracer = t
|
||||
}
|
||||
// func SetLogger(l logging.Logger) {
|
||||
// logger = l
|
||||
// }
|
||||
|
||||
// func SetTracer(t tracing.Tracer) {
|
||||
// tracer = t
|
||||
// }
|
||||
|
@@ -1,131 +1,131 @@
|
||||
package domain
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
// import (
|
||||
// "context"
|
||||
// "fmt"
|
||||
|
||||
"github.com/zitadel/zitadel/backend/v3/storage/database"
|
||||
)
|
||||
// "github.com/zitadel/zitadel/backend/v3/storage/database"
|
||||
// )
|
||||
|
||||
// Commander is the all it needs to implement the command pattern.
|
||||
// It is the interface all manipulations need to implement.
|
||||
// If possible it should also be used for queries. We will find out if this is possible in the future.
|
||||
type Commander interface {
|
||||
Execute(ctx context.Context, opts *CommandOpts) (err error)
|
||||
fmt.Stringer
|
||||
}
|
||||
// // Commander is the all it needs to implement the command pattern.
|
||||
// // It is the interface all manipulations need to implement.
|
||||
// // If possible it should also be used for queries. We will find out if this is possible in the future.
|
||||
// type Commander interface {
|
||||
// Execute(ctx context.Context, opts *CommandOpts) (err error)
|
||||
// fmt.Stringer
|
||||
// }
|
||||
|
||||
// Invoker is part of the command pattern.
|
||||
// It is the interface that is used to execute commands.
|
||||
type Invoker interface {
|
||||
Invoke(ctx context.Context, command Commander, opts *CommandOpts) error
|
||||
}
|
||||
// // Invoker is part of the command pattern.
|
||||
// // It is the interface that is used to execute commands.
|
||||
// type Invoker interface {
|
||||
// Invoke(ctx context.Context, command Commander, opts *CommandOpts) error
|
||||
// }
|
||||
|
||||
// CommandOpts are passed to each command
|
||||
// the provide common fields used by commands like the database client.
|
||||
type CommandOpts struct {
|
||||
DB database.QueryExecutor
|
||||
Invoker Invoker
|
||||
}
|
||||
// // CommandOpts are passed to each command
|
||||
// // the provide common fields used by commands like the database client.
|
||||
// type CommandOpts struct {
|
||||
// DB database.QueryExecutor
|
||||
// Invoker Invoker
|
||||
// }
|
||||
|
||||
type ensureTxOpts struct {
|
||||
*database.TransactionOptions
|
||||
}
|
||||
// type ensureTxOpts struct {
|
||||
// *database.TransactionOptions
|
||||
// }
|
||||
|
||||
type EnsureTransactionOpt func(*ensureTxOpts)
|
||||
// type EnsureTransactionOpt func(*ensureTxOpts)
|
||||
|
||||
// EnsureTx ensures that the DB is a transaction. If it is not, it will start a new transaction.
|
||||
// The returned close function will end the transaction. If the DB is already a transaction, the close function
|
||||
// will do nothing because another [Commander] is already responsible for ending the transaction.
|
||||
func (o *CommandOpts) EnsureTx(ctx context.Context, opts ...EnsureTransactionOpt) (close func(context.Context, error) error, err error) {
|
||||
beginner, ok := o.DB.(database.Beginner)
|
||||
if !ok {
|
||||
// db is already a transaction
|
||||
return func(_ context.Context, err error) error {
|
||||
return err
|
||||
}, nil
|
||||
}
|
||||
// // EnsureTx ensures that the DB is a transaction. If it is not, it will start a new transaction.
|
||||
// // The returned close function will end the transaction. If the DB is already a transaction, the close function
|
||||
// // will do nothing because another [Commander] is already responsible for ending the transaction.
|
||||
// func (o *CommandOpts) EnsureTx(ctx context.Context, opts ...EnsureTransactionOpt) (close func(context.Context, error) error, err error) {
|
||||
// beginner, ok := o.DB.(database.Beginner)
|
||||
// if !ok {
|
||||
// // db is already a transaction
|
||||
// return func(_ context.Context, err error) error {
|
||||
// return err
|
||||
// }, nil
|
||||
// }
|
||||
|
||||
txOpts := &ensureTxOpts{
|
||||
TransactionOptions: new(database.TransactionOptions),
|
||||
}
|
||||
for _, opt := range opts {
|
||||
opt(txOpts)
|
||||
}
|
||||
// txOpts := &ensureTxOpts{
|
||||
// TransactionOptions: new(database.TransactionOptions),
|
||||
// }
|
||||
// for _, opt := range opts {
|
||||
// opt(txOpts)
|
||||
// }
|
||||
|
||||
tx, err := beginner.Begin(ctx, txOpts.TransactionOptions)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
o.DB = tx
|
||||
// tx, err := beginner.Begin(ctx, txOpts.TransactionOptions)
|
||||
// if err != nil {
|
||||
// return nil, err
|
||||
// }
|
||||
// o.DB = tx
|
||||
|
||||
return func(ctx context.Context, err error) error {
|
||||
return tx.End(ctx, err)
|
||||
}, nil
|
||||
}
|
||||
// return func(ctx context.Context, err error) error {
|
||||
// return tx.End(ctx, err)
|
||||
// }, nil
|
||||
// }
|
||||
|
||||
// EnsureClient ensures that the o.DB is a client. If it is not, it will get a new client from the [database.Pool].
|
||||
// The returned close function will release the client. If the o.DB is already a client or transaction, the close function
|
||||
// will do nothing because another [Commander] is already responsible for releasing the client.
|
||||
func (o *CommandOpts) EnsureClient(ctx context.Context) (close func(_ context.Context) error, err error) {
|
||||
pool, ok := o.DB.(database.Pool)
|
||||
if !ok {
|
||||
// o.DB is already a client
|
||||
return func(_ context.Context) error {
|
||||
return nil
|
||||
}, nil
|
||||
}
|
||||
client, err := pool.Acquire(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
o.DB = client
|
||||
return func(ctx context.Context) error {
|
||||
return client.Release(ctx)
|
||||
}, nil
|
||||
}
|
||||
// // EnsureClient ensures that the o.DB is a client. If it is not, it will get a new client from the [database.Pool].
|
||||
// // The returned close function will release the client. If the o.DB is already a client or transaction, the close function
|
||||
// // will do nothing because another [Commander] is already responsible for releasing the client.
|
||||
// func (o *CommandOpts) EnsureClient(ctx context.Context) (close func(_ context.Context) error, err error) {
|
||||
// pool, ok := o.DB.(database.Pool)
|
||||
// if !ok {
|
||||
// // o.DB is already a client
|
||||
// return func(_ context.Context) error {
|
||||
// return nil
|
||||
// }, nil
|
||||
// }
|
||||
// client, err := pool.Acquire(ctx)
|
||||
// if err != nil {
|
||||
// return nil, err
|
||||
// }
|
||||
// o.DB = client
|
||||
// return func(ctx context.Context) error {
|
||||
// return client.Release(ctx)
|
||||
// }, nil
|
||||
// }
|
||||
|
||||
func (o *CommandOpts) Invoke(ctx context.Context, command Commander) error {
|
||||
if o.Invoker == nil {
|
||||
return command.Execute(ctx, o)
|
||||
}
|
||||
return o.Invoker.Invoke(ctx, command, o)
|
||||
}
|
||||
// func (o *CommandOpts) Invoke(ctx context.Context, command Commander) error {
|
||||
// if o.Invoker == nil {
|
||||
// return command.Execute(ctx, o)
|
||||
// }
|
||||
// return o.Invoker.Invoke(ctx, command, o)
|
||||
// }
|
||||
|
||||
func DefaultOpts(invoker Invoker) *CommandOpts {
|
||||
if invoker == nil {
|
||||
invoker = &noopInvoker{}
|
||||
}
|
||||
return &CommandOpts{
|
||||
DB: pool,
|
||||
Invoker: invoker,
|
||||
}
|
||||
}
|
||||
// func DefaultOpts(invoker Invoker) *CommandOpts {
|
||||
// if invoker == nil {
|
||||
// invoker = &noopInvoker{}
|
||||
// }
|
||||
// return &CommandOpts{
|
||||
// DB: pool,
|
||||
// Invoker: invoker,
|
||||
// }
|
||||
// }
|
||||
|
||||
// commandBatch is a batch of commands.
|
||||
// It uses the [Invoker] provided by the opts to execute each command.
|
||||
type commandBatch struct {
|
||||
Commands []Commander
|
||||
}
|
||||
// // commandBatch is a batch of commands.
|
||||
// // It uses the [Invoker] provided by the opts to execute each command.
|
||||
// type commandBatch struct {
|
||||
// Commands []Commander
|
||||
// }
|
||||
|
||||
func BatchCommands(cmds ...Commander) *commandBatch {
|
||||
return &commandBatch{
|
||||
Commands: cmds,
|
||||
}
|
||||
}
|
||||
// func BatchCommands(cmds ...Commander) *commandBatch {
|
||||
// return &commandBatch{
|
||||
// Commands: cmds,
|
||||
// }
|
||||
// }
|
||||
|
||||
// String implements [Commander].
|
||||
func (cmd *commandBatch) String() string {
|
||||
return "commandBatch"
|
||||
}
|
||||
// // String implements [Commander].
|
||||
// func (cmd *commandBatch) String() string {
|
||||
// return "commandBatch"
|
||||
// }
|
||||
|
||||
func (b *commandBatch) Execute(ctx context.Context, opts *CommandOpts) (err error) {
|
||||
for _, cmd := range b.Commands {
|
||||
if err = opts.Invoke(ctx, cmd); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
// func (b *commandBatch) Execute(ctx context.Context, opts *CommandOpts) (err error) {
|
||||
// for _, cmd := range b.Commands {
|
||||
// if err = opts.Invoke(ctx, cmd); err != nil {
|
||||
// return err
|
||||
// }
|
||||
// }
|
||||
// return nil
|
||||
// }
|
||||
|
||||
var _ Commander = (*commandBatch)(nil)
|
||||
// var _ Commander = (*commandBatch)(nil)
|
||||
|
@@ -1,90 +1,90 @@
|
||||
package domain
|
||||
|
||||
import (
|
||||
"context"
|
||||
// import (
|
||||
// "context"
|
||||
|
||||
"github.com/zitadel/zitadel/backend/v3/storage/eventstore"
|
||||
)
|
||||
// "github.com/zitadel/zitadel/backend/v3/storage/eventstore"
|
||||
// )
|
||||
|
||||
// CreateUserCommand adds a new user including the email verification for humans.
|
||||
// In the future it might make sense to separate the command into two commands:
|
||||
// - CreateHumanCommand: creates a new human user
|
||||
// - CreateMachineCommand: creates a new machine user
|
||||
type CreateUserCommand struct {
|
||||
user *User
|
||||
email *SetEmailCommand
|
||||
}
|
||||
// // CreateUserCommand adds a new user including the email verification for humans.
|
||||
// // In the future it might make sense to separate the command into two commands:
|
||||
// // - CreateHumanCommand: creates a new human user
|
||||
// // - CreateMachineCommand: creates a new machine user
|
||||
// type CreateUserCommand struct {
|
||||
// user *User
|
||||
// email *SetEmailCommand
|
||||
// }
|
||||
|
||||
var (
|
||||
_ Commander = (*CreateUserCommand)(nil)
|
||||
_ eventer = (*CreateUserCommand)(nil)
|
||||
)
|
||||
// var (
|
||||
// _ Commander = (*CreateUserCommand)(nil)
|
||||
// _ eventer = (*CreateUserCommand)(nil)
|
||||
// )
|
||||
|
||||
// opts heavily reduces the complexity for email verification because each type of verification is a simple option which implements the [Commander] interface.
|
||||
func NewCreateHumanCommand(username string, opts ...CreateHumanOpt) *CreateUserCommand {
|
||||
cmd := &CreateUserCommand{
|
||||
user: &User{
|
||||
Username: username,
|
||||
Traits: &Human{},
|
||||
},
|
||||
}
|
||||
// // opts heavily reduces the complexity for email verification because each type of verification is a simple option which implements the [Commander] interface.
|
||||
// func NewCreateHumanCommand(username string, opts ...CreateHumanOpt) *CreateUserCommand {
|
||||
// cmd := &CreateUserCommand{
|
||||
// user: &User{
|
||||
// Username: username,
|
||||
// Traits: &Human{},
|
||||
// },
|
||||
// }
|
||||
|
||||
for _, opt := range opts {
|
||||
opt.applyOnCreateHuman(cmd)
|
||||
}
|
||||
return cmd
|
||||
}
|
||||
// for _, opt := range opts {
|
||||
// opt.applyOnCreateHuman(cmd)
|
||||
// }
|
||||
// return cmd
|
||||
// }
|
||||
|
||||
// String implements [Commander].
|
||||
func (cmd *CreateUserCommand) String() string {
|
||||
return "CreateUserCommand"
|
||||
}
|
||||
// // String implements [Commander].
|
||||
// func (cmd *CreateUserCommand) String() string {
|
||||
// return "CreateUserCommand"
|
||||
// }
|
||||
|
||||
// Events implements [eventer].
|
||||
func (c *CreateUserCommand) Events() []*eventstore.Event {
|
||||
return []*eventstore.Event{
|
||||
{
|
||||
AggregateType: "user",
|
||||
AggregateID: c.user.ID,
|
||||
Type: "user.added",
|
||||
Payload: c.user,
|
||||
},
|
||||
}
|
||||
}
|
||||
// // Events implements [eventer].
|
||||
// func (c *CreateUserCommand) Events() []*eventstore.Event {
|
||||
// return []*eventstore.Event{
|
||||
// {
|
||||
// AggregateType: "user",
|
||||
// AggregateID: c.user.ID,
|
||||
// Type: "user.added",
|
||||
// Payload: c.user,
|
||||
// },
|
||||
// }
|
||||
// }
|
||||
|
||||
// Execute implements [Commander].
|
||||
func (c *CreateUserCommand) Execute(ctx context.Context, opts *CommandOpts) error {
|
||||
if err := c.ensureUserID(); err != nil {
|
||||
return err
|
||||
}
|
||||
c.email.UserID = c.user.ID
|
||||
if err := opts.Invoke(ctx, c.email); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
// // Execute implements [Commander].
|
||||
// func (c *CreateUserCommand) Execute(ctx context.Context, opts *CommandOpts) error {
|
||||
// if err := c.ensureUserID(); err != nil {
|
||||
// return err
|
||||
// }
|
||||
// c.email.UserID = c.user.ID
|
||||
// if err := opts.Invoke(ctx, c.email); err != nil {
|
||||
// return err
|
||||
// }
|
||||
// return nil
|
||||
// }
|
||||
|
||||
type CreateHumanOpt interface {
|
||||
applyOnCreateHuman(*CreateUserCommand)
|
||||
}
|
||||
// type CreateHumanOpt interface {
|
||||
// applyOnCreateHuman(*CreateUserCommand)
|
||||
// }
|
||||
|
||||
type createHumanIDOpt string
|
||||
// type createHumanIDOpt string
|
||||
|
||||
// applyOnCreateHuman implements [CreateHumanOpt].
|
||||
func (c createHumanIDOpt) applyOnCreateHuman(cmd *CreateUserCommand) {
|
||||
cmd.user.ID = string(c)
|
||||
}
|
||||
// // applyOnCreateHuman implements [CreateHumanOpt].
|
||||
// func (c createHumanIDOpt) applyOnCreateHuman(cmd *CreateUserCommand) {
|
||||
// cmd.user.ID = string(c)
|
||||
// }
|
||||
|
||||
var _ CreateHumanOpt = (*createHumanIDOpt)(nil)
|
||||
// var _ CreateHumanOpt = (*createHumanIDOpt)(nil)
|
||||
|
||||
func CreateHumanWithID(id string) CreateHumanOpt {
|
||||
return createHumanIDOpt(id)
|
||||
}
|
||||
// func CreateHumanWithID(id string) CreateHumanOpt {
|
||||
// return createHumanIDOpt(id)
|
||||
// }
|
||||
|
||||
func (c *CreateUserCommand) ensureUserID() (err error) {
|
||||
if c.user.ID != "" {
|
||||
return nil
|
||||
}
|
||||
c.user.ID, err = generateID()
|
||||
return err
|
||||
}
|
||||
// func (c *CreateUserCommand) ensureUserID() (err error) {
|
||||
// if c.user.ID != "" {
|
||||
// return nil
|
||||
// }
|
||||
// c.user.ID, err = generateID()
|
||||
// return err
|
||||
// }
|
||||
|
@@ -1,37 +1,37 @@
|
||||
package domain
|
||||
|
||||
import (
|
||||
"context"
|
||||
// import (
|
||||
// "context"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/crypto"
|
||||
)
|
||||
// "github.com/zitadel/zitadel/internal/crypto"
|
||||
// )
|
||||
|
||||
type generateCodeCommand struct {
|
||||
code string
|
||||
value *crypto.CryptoValue
|
||||
}
|
||||
// type generateCodeCommand struct {
|
||||
// code string
|
||||
// value *crypto.CryptoValue
|
||||
// }
|
||||
|
||||
// I didn't update this repository to the solution proposed please view one of the following interfaces for correct usage:
|
||||
// - [UserRepository]
|
||||
// - [InstanceRepository]
|
||||
// - [OrgRepository]
|
||||
type CryptoRepository interface {
|
||||
GetEncryptionConfig(ctx context.Context) (*crypto.GeneratorConfig, error)
|
||||
}
|
||||
// // I didn't update this repository to the solution proposed please view one of the following interfaces for correct usage:
|
||||
// // - [UserRepository]
|
||||
// // - [InstanceRepository]
|
||||
// // - [OrgRepository]
|
||||
// type CryptoRepository interface {
|
||||
// GetEncryptionConfig(ctx context.Context) (*crypto.GeneratorConfig, error)
|
||||
// }
|
||||
|
||||
// String implements [Commander].
|
||||
func (cmd *generateCodeCommand) String() string {
|
||||
return "generateCodeCommand"
|
||||
}
|
||||
// // String implements [Commander].
|
||||
// func (cmd *generateCodeCommand) String() string {
|
||||
// return "generateCodeCommand"
|
||||
// }
|
||||
|
||||
func (cmd *generateCodeCommand) Execute(ctx context.Context, opts *CommandOpts) error {
|
||||
config, err := cryptoRepo(opts.DB).GetEncryptionConfig(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
generator := crypto.NewEncryptionGenerator(*config, userCodeAlgorithm)
|
||||
cmd.value, cmd.code, err = crypto.NewCode(generator)
|
||||
return err
|
||||
}
|
||||
// func (cmd *generateCodeCommand) Execute(ctx context.Context, opts *CommandOpts) error {
|
||||
// config, err := cryptoRepo(opts.DB).GetEncryptionConfig(ctx)
|
||||
// if err != nil {
|
||||
// return err
|
||||
// }
|
||||
// generator := crypto.NewEncryptionGenerator(*config, userCodeAlgorithm)
|
||||
// cmd.value, cmd.code, err = crypto.NewCode(generator)
|
||||
// return err
|
||||
// }
|
||||
|
||||
var _ Commander = (*generateCodeCommand)(nil)
|
||||
// var _ Commander = (*generateCodeCommand)(nil)
|
||||
|
@@ -1,65 +1,66 @@
|
||||
package domain
|
||||
|
||||
import (
|
||||
"math/rand/v2"
|
||||
"strconv"
|
||||
// import (
|
||||
// "math/rand/v2"
|
||||
// "strconv"
|
||||
|
||||
"github.com/zitadel/zitadel/backend/v3/storage/cache"
|
||||
"github.com/zitadel/zitadel/backend/v3/storage/database"
|
||||
"github.com/zitadel/zitadel/backend/v3/telemetry/logging"
|
||||
"github.com/zitadel/zitadel/backend/v3/telemetry/tracing"
|
||||
"github.com/zitadel/zitadel/internal/crypto"
|
||||
)
|
||||
// "github.com/zitadel/zitadel/backend/v3/storage/cache"
|
||||
// "github.com/zitadel/zitadel/backend/v3/storage/database"
|
||||
|
||||
// The variables could also be moved to a struct.
|
||||
// I just started with the singleton pattern and kept it like this.
|
||||
var (
|
||||
pool database.Pool
|
||||
userCodeAlgorithm crypto.EncryptionAlgorithm
|
||||
tracer tracing.Tracer
|
||||
logger logging.Logger
|
||||
// // "github.com/zitadel/zitadel/backend/v3/telemetry/logging"
|
||||
// "github.com/zitadel/zitadel/backend/v3/telemetry/tracing"
|
||||
// "github.com/zitadel/zitadel/internal/crypto"
|
||||
// )
|
||||
|
||||
userRepo func(database.QueryExecutor) UserRepository
|
||||
instanceRepo func(database.QueryExecutor) InstanceRepository
|
||||
cryptoRepo func(database.QueryExecutor) CryptoRepository
|
||||
orgRepo func(database.QueryExecutor) OrgRepository
|
||||
// // The variables could also be moved to a struct.
|
||||
// // I just started with the singleton pattern and kept it like this.
|
||||
// var (
|
||||
// pool database.Pool
|
||||
// userCodeAlgorithm crypto.EncryptionAlgorithm
|
||||
// tracer tracing.Tracer
|
||||
// // logger logging.Logger
|
||||
|
||||
instanceCache cache.Cache[instanceCacheIndex, string, *Instance]
|
||||
orgCache cache.Cache[orgCacheIndex, string, *Org]
|
||||
// userRepo func(database.QueryExecutor) UserRepository
|
||||
// // instanceRepo func(database.QueryExecutor) InstanceRepository
|
||||
// cryptoRepo func(database.QueryExecutor) CryptoRepository
|
||||
// orgRepo func(database.QueryExecutor) OrgRepository
|
||||
|
||||
generateID func() (string, error) = func() (string, error) {
|
||||
return strconv.FormatUint(rand.Uint64(), 10), nil
|
||||
}
|
||||
)
|
||||
// // instanceCache cache.Cache[instanceCacheIndex, string, *Instance]
|
||||
// orgCache cache.Cache[orgCacheIndex, string, *Org]
|
||||
|
||||
func SetPool(p database.Pool) {
|
||||
pool = p
|
||||
}
|
||||
// generateID func() (string, error) = func() (string, error) {
|
||||
// return strconv.FormatUint(rand.Uint64(), 10), nil
|
||||
// }
|
||||
// )
|
||||
|
||||
func SetUserCodeAlgorithm(algorithm crypto.EncryptionAlgorithm) {
|
||||
userCodeAlgorithm = algorithm
|
||||
}
|
||||
// func SetPool(p database.Pool) {
|
||||
// pool = p
|
||||
// }
|
||||
|
||||
func SetTracer(t tracing.Tracer) {
|
||||
tracer = t
|
||||
}
|
||||
// func SetUserCodeAlgorithm(algorithm crypto.EncryptionAlgorithm) {
|
||||
// userCodeAlgorithm = algorithm
|
||||
// }
|
||||
|
||||
func SetLogger(l logging.Logger) {
|
||||
logger = l
|
||||
}
|
||||
// func SetTracer(t tracing.Tracer) {
|
||||
// tracer = t
|
||||
// }
|
||||
|
||||
func SetUserRepository(repo func(database.QueryExecutor) UserRepository) {
|
||||
userRepo = repo
|
||||
}
|
||||
// // func SetLogger(l logging.Logger) {
|
||||
// // logger = l
|
||||
// // }
|
||||
|
||||
func SetOrgRepository(repo func(database.QueryExecutor) OrgRepository) {
|
||||
orgRepo = repo
|
||||
}
|
||||
// func SetUserRepository(repo func(database.QueryExecutor) UserRepository) {
|
||||
// userRepo = repo
|
||||
// }
|
||||
|
||||
func SetInstanceRepository(repo func(database.QueryExecutor) InstanceRepository) {
|
||||
instanceRepo = repo
|
||||
}
|
||||
// func SetOrgRepository(repo func(database.QueryExecutor) OrgRepository) {
|
||||
// orgRepo = repo
|
||||
// }
|
||||
|
||||
func SetCryptoRepository(repo func(database.QueryExecutor) CryptoRepository) {
|
||||
cryptoRepo = repo
|
||||
}
|
||||
// // func SetInstanceRepository(repo func(database.QueryExecutor) InstanceRepository) {
|
||||
// // instanceRepo = repo
|
||||
// // }
|
||||
|
||||
// func SetCryptoRepository(repo func(database.QueryExecutor) CryptoRepository) {
|
||||
// cryptoRepo = repo
|
||||
// }
|
||||
|
@@ -1,67 +1,67 @@
|
||||
package domain_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log/slog"
|
||||
"testing"
|
||||
// import (
|
||||
// "context"
|
||||
// "log/slog"
|
||||
// "testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"go.opentelemetry.io/otel"
|
||||
"go.opentelemetry.io/otel/exporters/stdout/stdouttrace"
|
||||
sdktrace "go.opentelemetry.io/otel/sdk/trace"
|
||||
"go.uber.org/mock/gomock"
|
||||
// "github.com/stretchr/testify/assert"
|
||||
// "github.com/stretchr/testify/require"
|
||||
// "go.opentelemetry.io/otel"
|
||||
// "go.opentelemetry.io/otel/exporters/stdout/stdouttrace"
|
||||
// sdktrace "go.opentelemetry.io/otel/sdk/trace"
|
||||
// "go.uber.org/mock/gomock"
|
||||
|
||||
. "github.com/zitadel/zitadel/backend/v3/domain"
|
||||
"github.com/zitadel/zitadel/backend/v3/storage/database/dbmock"
|
||||
"github.com/zitadel/zitadel/backend/v3/storage/database/repository"
|
||||
"github.com/zitadel/zitadel/backend/v3/telemetry/logging"
|
||||
"github.com/zitadel/zitadel/backend/v3/telemetry/tracing"
|
||||
)
|
||||
// . "github.com/zitadel/zitadel/backend/v3/domain"
|
||||
// "github.com/zitadel/zitadel/backend/v3/storage/database/dbmock"
|
||||
// "github.com/zitadel/zitadel/backend/v3/storage/database/repository"
|
||||
// "github.com/zitadel/zitadel/backend/v3/telemetry/logging"
|
||||
// "github.com/zitadel/zitadel/backend/v3/telemetry/tracing"
|
||||
// )
|
||||
|
||||
// These tests give an overview of how to use the domain package.
|
||||
func TestExample(t *testing.T) {
|
||||
t.Skip("skip example test because it is not a real test")
|
||||
ctx := context.Background()
|
||||
// func TestExample(t *testing.T) {
|
||||
// t.Skip("skip example test because it is not a real test")
|
||||
// ctx := context.Background()
|
||||
|
||||
ctrl := gomock.NewController(t)
|
||||
pool := dbmock.NewMockPool(ctrl)
|
||||
tx := dbmock.NewMockTransaction(ctrl)
|
||||
// ctrl := gomock.NewController(t)
|
||||
// pool := dbmock.NewMockPool(ctrl)
|
||||
// tx := dbmock.NewMockTransaction(ctrl)
|
||||
|
||||
pool.EXPECT().Begin(gomock.Any(), gomock.Any()).Return(tx, nil)
|
||||
tx.EXPECT().End(gomock.Any(), gomock.Any()).Return(nil)
|
||||
SetPool(pool)
|
||||
// pool.EXPECT().Begin(gomock.Any(), gomock.Any()).Return(tx, nil)
|
||||
// tx.EXPECT().End(gomock.Any(), gomock.Any()).Return(nil)
|
||||
// SetPool(pool)
|
||||
|
||||
exporter, err := stdouttrace.New(stdouttrace.WithPrettyPrint())
|
||||
require.NoError(t, err)
|
||||
tracerProvider := sdktrace.NewTracerProvider(
|
||||
sdktrace.WithSyncer(exporter),
|
||||
)
|
||||
otel.SetTracerProvider(tracerProvider)
|
||||
SetTracer(tracing.Tracer{Tracer: tracerProvider.Tracer("test")})
|
||||
defer func() { assert.NoError(t, tracerProvider.Shutdown(ctx)) }()
|
||||
// exporter, err := stdouttrace.New(stdouttrace.WithPrettyPrint())
|
||||
// require.NoError(t, err)
|
||||
// tracerProvider := sdktrace.NewTracerProvider(
|
||||
// sdktrace.WithSyncer(exporter),
|
||||
// )
|
||||
// otel.SetTracerProvider(tracerProvider)
|
||||
// SetTracer(tracing.Tracer{Tracer: tracerProvider.Tracer("test")})
|
||||
// defer func() { assert.NoError(t, tracerProvider.Shutdown(ctx)) }()
|
||||
|
||||
SetLogger(logging.Logger{Logger: slog.Default()})
|
||||
// SetLogger(logging.Logger{Logger: slog.Default()})
|
||||
|
||||
SetUserRepository(repository.UserRepository)
|
||||
SetOrgRepository(repository.OrgRepository)
|
||||
// SetInstanceRepository(repository.Instance)
|
||||
// SetCryptoRepository(repository.Crypto)
|
||||
// SetUserRepository(repository.UserRepository)
|
||||
// SetOrgRepository(repository.OrgRepository)
|
||||
// // SetInstanceRepository(repository.Instance)
|
||||
// // SetCryptoRepository(repository.Crypto)
|
||||
|
||||
t.Run("create org", func(t *testing.T) {
|
||||
org := NewAddOrgCommand("testorg", NewAddMemberCommand("testuser", "ORG_OWNER"))
|
||||
user := NewCreateHumanCommand("testuser")
|
||||
err := Invoke(ctx, BatchCommands(org, user))
|
||||
assert.NoError(t, err)
|
||||
})
|
||||
// t.Run("create org", func(t *testing.T) {
|
||||
// org := NewAddOrgCommand("testorg", NewAddMemberCommand("testuser", "ORG_OWNER"))
|
||||
// user := NewCreateHumanCommand("testuser")
|
||||
// err := Invoke(ctx, BatchCommands(org, user))
|
||||
// assert.NoError(t, err)
|
||||
// })
|
||||
|
||||
t.Run("verified email", func(t *testing.T) {
|
||||
err := Invoke(ctx, NewSetEmailCommand("u1", "test@example.com", NewEmailVerifiedCommand("u1", true)))
|
||||
assert.NoError(t, err)
|
||||
})
|
||||
// t.Run("verified email", func(t *testing.T) {
|
||||
// err := Invoke(ctx, NewSetEmailCommand("u1", "test@example.com", NewEmailVerifiedCommand("u1", true)))
|
||||
// assert.NoError(t, err)
|
||||
// })
|
||||
|
||||
t.Run("unverified email", func(t *testing.T) {
|
||||
err := Invoke(ctx, NewSetEmailCommand("u2", "test2@example.com", NewEmailVerifiedCommand("u2", false)))
|
||||
assert.NoError(t, err)
|
||||
})
|
||||
}
|
||||
// t.Run("unverified email", func(t *testing.T) {
|
||||
// err := Invoke(ctx, NewSetEmailCommand("u2", "test2@example.com", NewEmailVerifiedCommand("u2", false)))
|
||||
// assert.NoError(t, err)
|
||||
// })
|
||||
// }
|
||||
|
@@ -1,175 +1,175 @@
|
||||
package domain
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
)
|
||||
// import (
|
||||
// "context"
|
||||
// "time"
|
||||
// )
|
||||
|
||||
// EmailVerifiedCommand verifies an email address for a user.
|
||||
type EmailVerifiedCommand struct {
|
||||
UserID string `json:"userId"`
|
||||
Email *Email `json:"email"`
|
||||
}
|
||||
// // EmailVerifiedCommand verifies an email address for a user.
|
||||
// type EmailVerifiedCommand struct {
|
||||
// UserID string `json:"userId"`
|
||||
// Email *Email `json:"email"`
|
||||
// }
|
||||
|
||||
func NewEmailVerifiedCommand(userID string, isVerified bool) *EmailVerifiedCommand {
|
||||
return &EmailVerifiedCommand{
|
||||
UserID: userID,
|
||||
Email: &Email{
|
||||
VerifiedAt: time.Time{},
|
||||
},
|
||||
}
|
||||
}
|
||||
// func NewEmailVerifiedCommand(userID string, isVerified bool) *EmailVerifiedCommand {
|
||||
// return &EmailVerifiedCommand{
|
||||
// UserID: userID,
|
||||
// Email: &Email{
|
||||
// VerifiedAt: time.Time{},
|
||||
// },
|
||||
// }
|
||||
// }
|
||||
|
||||
// String implements [Commander].
|
||||
func (cmd *EmailVerifiedCommand) String() string {
|
||||
return "EmailVerifiedCommand"
|
||||
}
|
||||
// // String implements [Commander].
|
||||
// func (cmd *EmailVerifiedCommand) String() string {
|
||||
// return "EmailVerifiedCommand"
|
||||
// }
|
||||
|
||||
var (
|
||||
_ Commander = (*EmailVerifiedCommand)(nil)
|
||||
_ SetEmailOpt = (*EmailVerifiedCommand)(nil)
|
||||
)
|
||||
// var (
|
||||
// _ Commander = (*EmailVerifiedCommand)(nil)
|
||||
// _ SetEmailOpt = (*EmailVerifiedCommand)(nil)
|
||||
// )
|
||||
|
||||
// Execute implements [Commander]
|
||||
func (cmd *EmailVerifiedCommand) Execute(ctx context.Context, opts *CommandOpts) error {
|
||||
repo := userRepo(opts.DB).Human()
|
||||
return repo.Update(ctx, repo.IDCondition(cmd.UserID), repo.SetEmailVerifiedAt(time.Time{}))
|
||||
}
|
||||
// // Execute implements [Commander]
|
||||
// func (cmd *EmailVerifiedCommand) Execute(ctx context.Context, opts *CommandOpts) error {
|
||||
// repo := userRepo(opts.DB).Human()
|
||||
// return repo.Update(ctx, repo.IDCondition(cmd.UserID), repo.SetEmailVerifiedAt(time.Time{}))
|
||||
// }
|
||||
|
||||
// applyOnSetEmail implements [SetEmailOpt]
|
||||
func (cmd *EmailVerifiedCommand) applyOnSetEmail(setEmailCmd *SetEmailCommand) {
|
||||
cmd.UserID = setEmailCmd.UserID
|
||||
cmd.Email.Address = setEmailCmd.Email
|
||||
setEmailCmd.verification = cmd
|
||||
}
|
||||
// // applyOnSetEmail implements [SetEmailOpt]
|
||||
// func (cmd *EmailVerifiedCommand) applyOnSetEmail(setEmailCmd *SetEmailCommand) {
|
||||
// cmd.UserID = setEmailCmd.UserID
|
||||
// cmd.Email.Address = setEmailCmd.Email
|
||||
// setEmailCmd.verification = cmd
|
||||
// }
|
||||
|
||||
// SendCodeCommand sends a verification code to the user's email address.
|
||||
// If the URLTemplate is not set it will use the default of the organization / instance.
|
||||
type SendCodeCommand struct {
|
||||
UserID string `json:"userId"`
|
||||
Email string `json:"email"`
|
||||
URLTemplate *string `json:"urlTemplate"`
|
||||
generator *generateCodeCommand
|
||||
}
|
||||
// // SendCodeCommand sends a verification code to the user's email address.
|
||||
// // If the URLTemplate is not set it will use the default of the organization / instance.
|
||||
// type SendCodeCommand struct {
|
||||
// UserID string `json:"userId"`
|
||||
// Email string `json:"email"`
|
||||
// URLTemplate *string `json:"urlTemplate"`
|
||||
// generator *generateCodeCommand
|
||||
// }
|
||||
|
||||
var (
|
||||
_ Commander = (*SendCodeCommand)(nil)
|
||||
_ SetEmailOpt = (*SendCodeCommand)(nil)
|
||||
)
|
||||
// var (
|
||||
// _ Commander = (*SendCodeCommand)(nil)
|
||||
// _ SetEmailOpt = (*SendCodeCommand)(nil)
|
||||
// )
|
||||
|
||||
func NewSendCodeCommand(userID string, urlTemplate *string) *SendCodeCommand {
|
||||
return &SendCodeCommand{
|
||||
UserID: userID,
|
||||
generator: &generateCodeCommand{},
|
||||
URLTemplate: urlTemplate,
|
||||
}
|
||||
}
|
||||
// func NewSendCodeCommand(userID string, urlTemplate *string) *SendCodeCommand {
|
||||
// return &SendCodeCommand{
|
||||
// UserID: userID,
|
||||
// generator: &generateCodeCommand{},
|
||||
// URLTemplate: urlTemplate,
|
||||
// }
|
||||
// }
|
||||
|
||||
// String implements [Commander].
|
||||
func (cmd *SendCodeCommand) String() string {
|
||||
return "SendCodeCommand"
|
||||
}
|
||||
// // String implements [Commander].
|
||||
// func (cmd *SendCodeCommand) String() string {
|
||||
// return "SendCodeCommand"
|
||||
// }
|
||||
|
||||
// Execute implements [Commander]
|
||||
func (cmd *SendCodeCommand) Execute(ctx context.Context, opts *CommandOpts) error {
|
||||
if err := cmd.ensureEmail(ctx, opts); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := cmd.ensureURL(ctx, opts); err != nil {
|
||||
return err
|
||||
}
|
||||
// // Execute implements [Commander]
|
||||
// func (cmd *SendCodeCommand) Execute(ctx context.Context, opts *CommandOpts) error {
|
||||
// if err := cmd.ensureEmail(ctx, opts); err != nil {
|
||||
// return err
|
||||
// }
|
||||
// if err := cmd.ensureURL(ctx, opts); err != nil {
|
||||
// return err
|
||||
// }
|
||||
|
||||
if err := opts.Invoker.Invoke(ctx, cmd.generator, opts); err != nil {
|
||||
return err
|
||||
}
|
||||
// TODO: queue notification
|
||||
// if err := opts.Invoker.Invoke(ctx, cmd.generator, opts); err != nil {
|
||||
// return err
|
||||
// }
|
||||
// // TODO: queue notification
|
||||
|
||||
return nil
|
||||
}
|
||||
// return nil
|
||||
// }
|
||||
|
||||
func (cmd *SendCodeCommand) ensureEmail(ctx context.Context, opts *CommandOpts) error {
|
||||
if cmd.Email != "" {
|
||||
return nil
|
||||
}
|
||||
repo := userRepo(opts.DB).Human()
|
||||
email, err := repo.GetEmail(ctx, repo.IDCondition(cmd.UserID))
|
||||
if err != nil || !email.VerifiedAt.IsZero() {
|
||||
return err
|
||||
}
|
||||
cmd.Email = email.Address
|
||||
return nil
|
||||
}
|
||||
// func (cmd *SendCodeCommand) ensureEmail(ctx context.Context, opts *CommandOpts) error {
|
||||
// if cmd.Email != "" {
|
||||
// return nil
|
||||
// }
|
||||
// repo := userRepo(opts.DB).Human()
|
||||
// email, err := repo.GetEmail(ctx, repo.IDCondition(cmd.UserID))
|
||||
// if err != nil || !email.VerifiedAt.IsZero() {
|
||||
// return err
|
||||
// }
|
||||
// cmd.Email = email.Address
|
||||
// return nil
|
||||
// }
|
||||
|
||||
func (cmd *SendCodeCommand) ensureURL(ctx context.Context, opts *CommandOpts) error {
|
||||
if cmd.URLTemplate != nil && *cmd.URLTemplate != "" {
|
||||
return nil
|
||||
}
|
||||
_, _ = ctx, opts
|
||||
// TODO: load default template
|
||||
return nil
|
||||
}
|
||||
// func (cmd *SendCodeCommand) ensureURL(ctx context.Context, opts *CommandOpts) error {
|
||||
// if cmd.URLTemplate != nil && *cmd.URLTemplate != "" {
|
||||
// return nil
|
||||
// }
|
||||
// _, _ = ctx, opts
|
||||
// // TODO: load default template
|
||||
// return nil
|
||||
// }
|
||||
|
||||
// applyOnSetEmail implements [SetEmailOpt]
|
||||
func (cmd *SendCodeCommand) applyOnSetEmail(setEmailCmd *SetEmailCommand) {
|
||||
cmd.UserID = setEmailCmd.UserID
|
||||
cmd.Email = setEmailCmd.Email
|
||||
setEmailCmd.verification = cmd
|
||||
}
|
||||
// // applyOnSetEmail implements [SetEmailOpt]
|
||||
// func (cmd *SendCodeCommand) applyOnSetEmail(setEmailCmd *SetEmailCommand) {
|
||||
// cmd.UserID = setEmailCmd.UserID
|
||||
// cmd.Email = setEmailCmd.Email
|
||||
// setEmailCmd.verification = cmd
|
||||
// }
|
||||
|
||||
// ReturnCodeCommand creates the code and returns it to the caller.
|
||||
// The caller gets the code by calling the Code field after the command got executed.
|
||||
type ReturnCodeCommand struct {
|
||||
UserID string `json:"userId"`
|
||||
Email string `json:"email"`
|
||||
Code string `json:"code"`
|
||||
generator *generateCodeCommand
|
||||
}
|
||||
// // ReturnCodeCommand creates the code and returns it to the caller.
|
||||
// // The caller gets the code by calling the Code field after the command got executed.
|
||||
// type ReturnCodeCommand struct {
|
||||
// UserID string `json:"userId"`
|
||||
// Email string `json:"email"`
|
||||
// Code string `json:"code"`
|
||||
// generator *generateCodeCommand
|
||||
// }
|
||||
|
||||
var (
|
||||
_ Commander = (*ReturnCodeCommand)(nil)
|
||||
_ SetEmailOpt = (*ReturnCodeCommand)(nil)
|
||||
)
|
||||
// var (
|
||||
// _ Commander = (*ReturnCodeCommand)(nil)
|
||||
// _ SetEmailOpt = (*ReturnCodeCommand)(nil)
|
||||
// )
|
||||
|
||||
func NewReturnCodeCommand(userID string) *ReturnCodeCommand {
|
||||
return &ReturnCodeCommand{
|
||||
UserID: userID,
|
||||
generator: &generateCodeCommand{},
|
||||
}
|
||||
}
|
||||
// func NewReturnCodeCommand(userID string) *ReturnCodeCommand {
|
||||
// return &ReturnCodeCommand{
|
||||
// UserID: userID,
|
||||
// generator: &generateCodeCommand{},
|
||||
// }
|
||||
// }
|
||||
|
||||
// String implements [Commander].
|
||||
func (cmd *ReturnCodeCommand) String() string {
|
||||
return "ReturnCodeCommand"
|
||||
}
|
||||
// // String implements [Commander].
|
||||
// func (cmd *ReturnCodeCommand) String() string {
|
||||
// return "ReturnCodeCommand"
|
||||
// }
|
||||
|
||||
// Execute implements [Commander]
|
||||
func (cmd *ReturnCodeCommand) Execute(ctx context.Context, opts *CommandOpts) error {
|
||||
if err := cmd.ensureEmail(ctx, opts); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := opts.Invoker.Invoke(ctx, cmd.generator, opts); err != nil {
|
||||
return err
|
||||
}
|
||||
cmd.Code = cmd.generator.code
|
||||
return nil
|
||||
}
|
||||
// // Execute implements [Commander]
|
||||
// func (cmd *ReturnCodeCommand) Execute(ctx context.Context, opts *CommandOpts) error {
|
||||
// if err := cmd.ensureEmail(ctx, opts); err != nil {
|
||||
// return err
|
||||
// }
|
||||
// if err := opts.Invoker.Invoke(ctx, cmd.generator, opts); err != nil {
|
||||
// return err
|
||||
// }
|
||||
// cmd.Code = cmd.generator.code
|
||||
// return nil
|
||||
// }
|
||||
|
||||
func (cmd *ReturnCodeCommand) ensureEmail(ctx context.Context, opts *CommandOpts) error {
|
||||
if cmd.Email != "" {
|
||||
return nil
|
||||
}
|
||||
repo := userRepo(opts.DB).Human()
|
||||
email, err := repo.GetEmail(ctx, repo.IDCondition(cmd.UserID))
|
||||
if err != nil || !email.VerifiedAt.IsZero() {
|
||||
return err
|
||||
}
|
||||
cmd.Email = email.Address
|
||||
return nil
|
||||
}
|
||||
// func (cmd *ReturnCodeCommand) ensureEmail(ctx context.Context, opts *CommandOpts) error {
|
||||
// if cmd.Email != "" {
|
||||
// return nil
|
||||
// }
|
||||
// repo := userRepo(opts.DB).Human()
|
||||
// email, err := repo.GetEmail(ctx, repo.IDCondition(cmd.UserID))
|
||||
// if err != nil || !email.VerifiedAt.IsZero() {
|
||||
// return err
|
||||
// }
|
||||
// cmd.Email = email.Address
|
||||
// return nil
|
||||
// }
|
||||
|
||||
// applyOnSetEmail implements [SetEmailOpt]
|
||||
func (cmd *ReturnCodeCommand) applyOnSetEmail(setEmailCmd *SetEmailCommand) {
|
||||
cmd.UserID = setEmailCmd.UserID
|
||||
cmd.Email = setEmailCmd.Email
|
||||
setEmailCmd.verification = cmd
|
||||
}
|
||||
// // applyOnSetEmail implements [SetEmailOpt]
|
||||
// func (cmd *ReturnCodeCommand) applyOnSetEmail(setEmailCmd *SetEmailCommand) {
|
||||
// cmd.UserID = setEmailCmd.UserID
|
||||
// cmd.Email = setEmailCmd.Email
|
||||
// setEmailCmd.verification = cmd
|
||||
// }
|
||||
|
@@ -11,14 +11,14 @@ import (
|
||||
type Instance struct {
|
||||
ID string `json:"id,omitempty" db:"id"`
|
||||
Name string `json:"name,omitempty" db:"name"`
|
||||
DefaultOrgID string `json:"default_org_id,omitempty" db:"default_org_id"`
|
||||
IAMProjectID string `json:"iam_project_id,omitempty" db:"iam_project_id"`
|
||||
ConsoleClientID string `json:"console_client_id,omitempty" db:"console_client_id"`
|
||||
ConsoleAppID string `json:"console_app_id,omitempty" db:"console_app_id"`
|
||||
DefaultLanguage string `json:"default_language,omitempty" db:"default_language"`
|
||||
CreatedAt time.Time `json:"-,omitempty" db:"created_at"`
|
||||
UpdatedAt time.Time `json:"-,omitempty" db:"updated_at"`
|
||||
DeletedAt *time.Time `json:"-,omitempty" db:"deleted_at"`
|
||||
DefaultOrgID string `json:"defaultOrgId,omitempty" db:"default_org_id"`
|
||||
IAMProjectID string `json:"iamProjectId,omitempty" db:"iam_project_id"`
|
||||
ConsoleClientID string `json:"consoleClientId,omitempty" db:"console_client_id"`
|
||||
ConsoleAppID string `json:"consoleAppId,omitempty" db:"console_app_id"`
|
||||
DefaultLanguage string `json:"defaultLanguage,omitempty" db:"default_language"`
|
||||
CreatedAt time.Time `json:"createdAt" db:"created_at"`
|
||||
UpdatedAt time.Time `json:"updatedAt" db:"updated_at"`
|
||||
DeletedAt *time.Time `json:"deletedAt" db:"deleted_at"`
|
||||
}
|
||||
|
||||
type instanceCacheIndex uint8
|
||||
|
@@ -1,158 +1,158 @@
|
||||
package domain
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
// import (
|
||||
// "context"
|
||||
// "fmt"
|
||||
|
||||
"github.com/zitadel/zitadel/backend/v3/storage/eventstore"
|
||||
)
|
||||
// "github.com/zitadel/zitadel/backend/v3/storage/eventstore"
|
||||
// )
|
||||
|
||||
// Invoke provides a way to execute commands within the domain package.
|
||||
// It uses a chain of responsibility pattern to handle the command execution.
|
||||
// The default chain includes logging, tracing, and event publishing.
|
||||
// If you want to invoke multiple commands in a single transaction, you can use the [commandBatch].
|
||||
func Invoke(ctx context.Context, cmd Commander) error {
|
||||
invoker := newEventStoreInvoker(newLoggingInvoker(newTraceInvoker(nil)))
|
||||
opts := &CommandOpts{
|
||||
Invoker: invoker.collector,
|
||||
DB: pool,
|
||||
}
|
||||
return invoker.Invoke(ctx, cmd, opts)
|
||||
}
|
||||
// // Invoke provides a way to execute commands within the domain package.
|
||||
// // It uses a chain of responsibility pattern to handle the command execution.
|
||||
// // The default chain includes logging, tracing, and event publishing.
|
||||
// // If you want to invoke multiple commands in a single transaction, you can use the [commandBatch].
|
||||
// func Invoke(ctx context.Context, cmd Commander) error {
|
||||
// invoker := newEventStoreInvoker(newLoggingInvoker(newTraceInvoker(nil)))
|
||||
// opts := &CommandOpts{
|
||||
// Invoker: invoker.collector,
|
||||
// DB: pool,
|
||||
// }
|
||||
// return invoker.Invoke(ctx, cmd, opts)
|
||||
// }
|
||||
|
||||
// eventStoreInvoker checks if the command implements the [eventer] interface.
|
||||
// If it does, it collects the events and publishes them to the event store.
|
||||
type eventStoreInvoker struct {
|
||||
collector *eventCollector
|
||||
}
|
||||
// // eventStoreInvoker checks if the command implements the [eventer] interface.
|
||||
// // If it does, it collects the events and publishes them to the event store.
|
||||
// type eventStoreInvoker struct {
|
||||
// collector *eventCollector
|
||||
// }
|
||||
|
||||
func newEventStoreInvoker(next Invoker) *eventStoreInvoker {
|
||||
return &eventStoreInvoker{collector: &eventCollector{next: next}}
|
||||
}
|
||||
// func newEventStoreInvoker(next Invoker) *eventStoreInvoker {
|
||||
// return &eventStoreInvoker{collector: &eventCollector{next: next}}
|
||||
// }
|
||||
|
||||
func (i *eventStoreInvoker) Invoke(ctx context.Context, command Commander, opts *CommandOpts) (err error) {
|
||||
err = i.collector.Invoke(ctx, command, opts)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(i.collector.events) > 0 {
|
||||
err = eventstore.Publish(ctx, i.collector.events, opts.DB)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
// func (i *eventStoreInvoker) Invoke(ctx context.Context, command Commander, opts *CommandOpts) (err error) {
|
||||
// err = i.collector.Invoke(ctx, command, opts)
|
||||
// if err != nil {
|
||||
// return err
|
||||
// }
|
||||
// if len(i.collector.events) > 0 {
|
||||
// err = eventstore.Publish(ctx, i.collector.events, opts.DB)
|
||||
// if err != nil {
|
||||
// return err
|
||||
// }
|
||||
// }
|
||||
// return nil
|
||||
// }
|
||||
|
||||
// eventCollector collects events from all commands. The [eventStoreInvoker] pushes the collected events after all commands are executed.
|
||||
type eventCollector struct {
|
||||
next Invoker
|
||||
events []*eventstore.Event
|
||||
}
|
||||
// // eventCollector collects events from all commands. The [eventStoreInvoker] pushes the collected events after all commands are executed.
|
||||
// type eventCollector struct {
|
||||
// next Invoker
|
||||
// events []*eventstore.Event
|
||||
// }
|
||||
|
||||
type eventer interface {
|
||||
Events() []*eventstore.Event
|
||||
}
|
||||
// type eventer interface {
|
||||
// Events() []*eventstore.Event
|
||||
// }
|
||||
|
||||
func (i *eventCollector) Invoke(ctx context.Context, command Commander, opts *CommandOpts) (err error) {
|
||||
if e, ok := command.(eventer); ok && len(e.Events()) > 0 {
|
||||
// we need to ensure all commands are executed in the same transaction
|
||||
close, err := opts.EnsureTx(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() { err = close(ctx, err) }()
|
||||
// func (i *eventCollector) Invoke(ctx context.Context, command Commander, opts *CommandOpts) (err error) {
|
||||
// if e, ok := command.(eventer); ok && len(e.Events()) > 0 {
|
||||
// // we need to ensure all commands are executed in the same transaction
|
||||
// close, err := opts.EnsureTx(ctx)
|
||||
// if err != nil {
|
||||
// return err
|
||||
// }
|
||||
// defer func() { err = close(ctx, err) }()
|
||||
|
||||
i.events = append(i.events, e.Events()...)
|
||||
}
|
||||
if i.next != nil {
|
||||
return i.next.Invoke(ctx, command, opts)
|
||||
}
|
||||
return command.Execute(ctx, opts)
|
||||
}
|
||||
// i.events = append(i.events, e.Events()...)
|
||||
// }
|
||||
// if i.next != nil {
|
||||
// return i.next.Invoke(ctx, command, opts)
|
||||
// }
|
||||
// return command.Execute(ctx, opts)
|
||||
// }
|
||||
|
||||
// traceInvoker decorates each command with tracing.
|
||||
type traceInvoker struct {
|
||||
next Invoker
|
||||
}
|
||||
// // traceInvoker decorates each command with tracing.
|
||||
// type traceInvoker struct {
|
||||
// next Invoker
|
||||
// }
|
||||
|
||||
func newTraceInvoker(next Invoker) *traceInvoker {
|
||||
return &traceInvoker{next: next}
|
||||
}
|
||||
// func newTraceInvoker(next Invoker) *traceInvoker {
|
||||
// return &traceInvoker{next: next}
|
||||
// }
|
||||
|
||||
func (i *traceInvoker) Invoke(ctx context.Context, command Commander, opts *CommandOpts) (err error) {
|
||||
ctx, span := tracer.Start(ctx, fmt.Sprintf("%T", command))
|
||||
defer func() {
|
||||
if err != nil {
|
||||
span.RecordError(err)
|
||||
}
|
||||
span.End()
|
||||
}()
|
||||
// func (i *traceInvoker) Invoke(ctx context.Context, command Commander, opts *CommandOpts) (err error) {
|
||||
// ctx, span := tracer.Start(ctx, fmt.Sprintf("%T", command))
|
||||
// defer func() {
|
||||
// if err != nil {
|
||||
// span.RecordError(err)
|
||||
// }
|
||||
// span.End()
|
||||
// }()
|
||||
|
||||
if i.next != nil {
|
||||
return i.next.Invoke(ctx, command, opts)
|
||||
}
|
||||
return command.Execute(ctx, opts)
|
||||
}
|
||||
// if i.next != nil {
|
||||
// return i.next.Invoke(ctx, command, opts)
|
||||
// }
|
||||
// return command.Execute(ctx, opts)
|
||||
// }
|
||||
|
||||
// loggingInvoker decorates each command with logging.
|
||||
// It is an example implementation and logs the command name at the beginning and success or failure after the command got executed.
|
||||
type loggingInvoker struct {
|
||||
next Invoker
|
||||
}
|
||||
// // loggingInvoker decorates each command with logging.
|
||||
// // It is an example implementation and logs the command name at the beginning and success or failure after the command got executed.
|
||||
// type loggingInvoker struct {
|
||||
// next Invoker
|
||||
// }
|
||||
|
||||
func newLoggingInvoker(next Invoker) *loggingInvoker {
|
||||
return &loggingInvoker{next: next}
|
||||
}
|
||||
// func newLoggingInvoker(next Invoker) *loggingInvoker {
|
||||
// return &loggingInvoker{next: next}
|
||||
// }
|
||||
|
||||
func (i *loggingInvoker) Invoke(ctx context.Context, command Commander, opts *CommandOpts) (err error) {
|
||||
logger.InfoContext(ctx, "Invoking command", "command", command.String())
|
||||
// func (i *loggingInvoker) Invoke(ctx context.Context, command Commander, opts *CommandOpts) (err error) {
|
||||
// logger.InfoContext(ctx, "Invoking command", "command", command.String())
|
||||
|
||||
if i.next != nil {
|
||||
err = i.next.Invoke(ctx, command, opts)
|
||||
} else {
|
||||
err = command.Execute(ctx, opts)
|
||||
}
|
||||
// if i.next != nil {
|
||||
// err = i.next.Invoke(ctx, command, opts)
|
||||
// } else {
|
||||
// err = command.Execute(ctx, opts)
|
||||
// }
|
||||
|
||||
if err != nil {
|
||||
logger.ErrorContext(ctx, "Command invocation failed", "command", command.String(), "error", err)
|
||||
return err
|
||||
}
|
||||
logger.InfoContext(ctx, "Command invocation succeeded", "command", command.String())
|
||||
return nil
|
||||
}
|
||||
// if err != nil {
|
||||
// logger.ErrorContext(ctx, "Command invocation failed", "command", command.String(), "error", err)
|
||||
// return err
|
||||
// }
|
||||
// logger.InfoContext(ctx, "Command invocation succeeded", "command", command.String())
|
||||
// return nil
|
||||
// }
|
||||
|
||||
type noopInvoker struct {
|
||||
next Invoker
|
||||
}
|
||||
// type noopInvoker struct {
|
||||
// next Invoker
|
||||
// }
|
||||
|
||||
func (i *noopInvoker) Invoke(ctx context.Context, command Commander, opts *CommandOpts) error {
|
||||
if i.next != nil {
|
||||
return i.next.Invoke(ctx, command, opts)
|
||||
}
|
||||
return command.Execute(ctx, opts)
|
||||
}
|
||||
// func (i *noopInvoker) Invoke(ctx context.Context, command Commander, opts *CommandOpts) error {
|
||||
// if i.next != nil {
|
||||
// return i.next.Invoke(ctx, command, opts)
|
||||
// }
|
||||
// return command.Execute(ctx, opts)
|
||||
// }
|
||||
|
||||
// cacheInvoker could be used in the future to do the caching.
|
||||
// My goal would be to have two interfaces:
|
||||
// - cacheSetter: which caches an object
|
||||
// - cacheGetter: which gets an object from the cache, this should also skip the command execution
|
||||
type cacheInvoker struct {
|
||||
next Invoker
|
||||
}
|
||||
// // cacheInvoker could be used in the future to do the caching.
|
||||
// // My goal would be to have two interfaces:
|
||||
// // - cacheSetter: which caches an object
|
||||
// // - cacheGetter: which gets an object from the cache, this should also skip the command execution
|
||||
// type cacheInvoker struct {
|
||||
// next Invoker
|
||||
// }
|
||||
|
||||
type cacher interface {
|
||||
Cache(opts *CommandOpts)
|
||||
}
|
||||
// type cacher interface {
|
||||
// Cache(opts *CommandOpts)
|
||||
// }
|
||||
|
||||
func (i *cacheInvoker) Invoke(ctx context.Context, command Commander, opts *CommandOpts) (err error) {
|
||||
if c, ok := command.(cacher); ok {
|
||||
c.Cache(opts)
|
||||
}
|
||||
if i.next != nil {
|
||||
err = i.next.Invoke(ctx, command, opts)
|
||||
} else {
|
||||
err = command.Execute(ctx, opts)
|
||||
}
|
||||
return err
|
||||
}
|
||||
// func (i *cacheInvoker) Invoke(ctx context.Context, command Commander, opts *CommandOpts) (err error) {
|
||||
// if c, ok := command.(cacher); ok {
|
||||
// c.Cache(opts)
|
||||
// }
|
||||
// if i.next != nil {
|
||||
// err = i.next.Invoke(ctx, command, opts)
|
||||
// } else {
|
||||
// err = command.Execute(ctx, opts)
|
||||
// }
|
||||
// return err
|
||||
// }
|
||||
|
@@ -1,137 +1,137 @@
|
||||
package domain
|
||||
|
||||
import (
|
||||
"context"
|
||||
// import (
|
||||
// "context"
|
||||
|
||||
"github.com/zitadel/zitadel/backend/v3/storage/eventstore"
|
||||
)
|
||||
// "github.com/zitadel/zitadel/backend/v3/storage/eventstore"
|
||||
// )
|
||||
|
||||
// AddOrgCommand adds a new organization.
|
||||
// I'm unsure if we should add the Admins here or if this should be a separate command.
|
||||
type AddOrgCommand struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Admins []*AddMemberCommand `json:"admins"`
|
||||
}
|
||||
// // AddOrgCommand adds a new organization.
|
||||
// // I'm unsure if we should add the Admins here or if this should be a separate command.
|
||||
// type AddOrgCommand struct {
|
||||
// ID string `json:"id"`
|
||||
// Name string `json:"name"`
|
||||
// Admins []*AddMemberCommand `json:"admins"`
|
||||
// }
|
||||
|
||||
func NewAddOrgCommand(name string, admins ...*AddMemberCommand) *AddOrgCommand {
|
||||
return &AddOrgCommand{
|
||||
Name: name,
|
||||
Admins: admins,
|
||||
}
|
||||
}
|
||||
// func NewAddOrgCommand(name string, admins ...*AddMemberCommand) *AddOrgCommand {
|
||||
// return &AddOrgCommand{
|
||||
// Name: name,
|
||||
// Admins: admins,
|
||||
// }
|
||||
// }
|
||||
|
||||
// String implements [Commander].
|
||||
func (cmd *AddOrgCommand) String() string {
|
||||
return "AddOrgCommand"
|
||||
}
|
||||
// // String implements [Commander].
|
||||
// func (cmd *AddOrgCommand) String() string {
|
||||
// return "AddOrgCommand"
|
||||
// }
|
||||
|
||||
// Execute implements Commander.
|
||||
func (cmd *AddOrgCommand) Execute(ctx context.Context, opts *CommandOpts) (err error) {
|
||||
if len(cmd.Admins) == 0 {
|
||||
return ErrNoAdminSpecified
|
||||
}
|
||||
if err = cmd.ensureID(); err != nil {
|
||||
return err
|
||||
}
|
||||
// // Execute implements Commander.
|
||||
// func (cmd *AddOrgCommand) Execute(ctx context.Context, opts *CommandOpts) (err error) {
|
||||
// if len(cmd.Admins) == 0 {
|
||||
// return ErrNoAdminSpecified
|
||||
// }
|
||||
// if err = cmd.ensureID(); err != nil {
|
||||
// return err
|
||||
// }
|
||||
|
||||
close, err := opts.EnsureTx(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() { err = close(ctx, err) }()
|
||||
err = orgRepo(opts.DB).Create(ctx, &Org{
|
||||
ID: cmd.ID,
|
||||
Name: cmd.Name,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// close, err := opts.EnsureTx(ctx)
|
||||
// if err != nil {
|
||||
// return err
|
||||
// }
|
||||
// defer func() { err = close(ctx, err) }()
|
||||
// err = orgRepo(opts.DB).Create(ctx, &Org{
|
||||
// ID: cmd.ID,
|
||||
// Name: cmd.Name,
|
||||
// })
|
||||
// if err != nil {
|
||||
// return err
|
||||
// }
|
||||
|
||||
for _, admin := range cmd.Admins {
|
||||
admin.orgID = cmd.ID
|
||||
if err = opts.Invoke(ctx, admin); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
// for _, admin := range cmd.Admins {
|
||||
// admin.orgID = cmd.ID
|
||||
// if err = opts.Invoke(ctx, admin); err != nil {
|
||||
// return err
|
||||
// }
|
||||
// }
|
||||
|
||||
orgCache.Set(ctx, &Org{
|
||||
ID: cmd.ID,
|
||||
Name: cmd.Name,
|
||||
})
|
||||
// orgCache.Set(ctx, &Org{
|
||||
// ID: cmd.ID,
|
||||
// Name: cmd.Name,
|
||||
// })
|
||||
|
||||
return nil
|
||||
}
|
||||
// return nil
|
||||
// }
|
||||
|
||||
// Events implements [eventer].
|
||||
func (cmd *AddOrgCommand) Events() []*eventstore.Event {
|
||||
return []*eventstore.Event{
|
||||
{
|
||||
AggregateType: "org",
|
||||
AggregateID: cmd.ID,
|
||||
Type: "org.added",
|
||||
Payload: cmd,
|
||||
},
|
||||
}
|
||||
}
|
||||
// // Events implements [eventer].
|
||||
// func (cmd *AddOrgCommand) Events() []*eventstore.Event {
|
||||
// return []*eventstore.Event{
|
||||
// {
|
||||
// AggregateType: "org",
|
||||
// AggregateID: cmd.ID,
|
||||
// Type: "org.added",
|
||||
// Payload: cmd,
|
||||
// },
|
||||
// }
|
||||
// }
|
||||
|
||||
var (
|
||||
_ Commander = (*AddOrgCommand)(nil)
|
||||
_ eventer = (*AddOrgCommand)(nil)
|
||||
)
|
||||
// var (
|
||||
// _ Commander = (*AddOrgCommand)(nil)
|
||||
// _ eventer = (*AddOrgCommand)(nil)
|
||||
// )
|
||||
|
||||
func (cmd *AddOrgCommand) ensureID() (err error) {
|
||||
if cmd.ID != "" {
|
||||
return nil
|
||||
}
|
||||
cmd.ID, err = generateID()
|
||||
return err
|
||||
}
|
||||
// func (cmd *AddOrgCommand) ensureID() (err error) {
|
||||
// if cmd.ID != "" {
|
||||
// return nil
|
||||
// }
|
||||
// cmd.ID, err = generateID()
|
||||
// return err
|
||||
// }
|
||||
|
||||
// AddMemberCommand adds a new member to an organization.
|
||||
// I'm not sure if we should make it more generic to also use it for instances.
|
||||
type AddMemberCommand struct {
|
||||
orgID string
|
||||
UserID string `json:"userId"`
|
||||
Roles []string `json:"roles"`
|
||||
}
|
||||
// // AddMemberCommand adds a new member to an organization.
|
||||
// // I'm not sure if we should make it more generic to also use it for instances.
|
||||
// type AddMemberCommand struct {
|
||||
// orgID string
|
||||
// UserID string `json:"userId"`
|
||||
// Roles []string `json:"roles"`
|
||||
// }
|
||||
|
||||
func NewAddMemberCommand(userID string, roles ...string) *AddMemberCommand {
|
||||
return &AddMemberCommand{
|
||||
UserID: userID,
|
||||
Roles: roles,
|
||||
}
|
||||
}
|
||||
// func NewAddMemberCommand(userID string, roles ...string) *AddMemberCommand {
|
||||
// return &AddMemberCommand{
|
||||
// UserID: userID,
|
||||
// Roles: roles,
|
||||
// }
|
||||
// }
|
||||
|
||||
// String implements [Commander].
|
||||
func (cmd *AddMemberCommand) String() string {
|
||||
return "AddMemberCommand"
|
||||
}
|
||||
// // String implements [Commander].
|
||||
// func (cmd *AddMemberCommand) String() string {
|
||||
// return "AddMemberCommand"
|
||||
// }
|
||||
|
||||
// Execute implements Commander.
|
||||
func (a *AddMemberCommand) Execute(ctx context.Context, opts *CommandOpts) (err error) {
|
||||
close, err := opts.EnsureTx(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() { err = close(ctx, err) }()
|
||||
// // Execute implements Commander.
|
||||
// func (a *AddMemberCommand) Execute(ctx context.Context, opts *CommandOpts) (err error) {
|
||||
// close, err := opts.EnsureTx(ctx)
|
||||
// if err != nil {
|
||||
// return err
|
||||
// }
|
||||
// defer func() { err = close(ctx, err) }()
|
||||
|
||||
return orgRepo(opts.DB).Member().AddMember(ctx, a.orgID, a.UserID, a.Roles)
|
||||
}
|
||||
// return orgRepo(opts.DB).Member().AddMember(ctx, a.orgID, a.UserID, a.Roles)
|
||||
// }
|
||||
|
||||
// Events implements [eventer].
|
||||
func (a *AddMemberCommand) Events() []*eventstore.Event {
|
||||
return []*eventstore.Event{
|
||||
{
|
||||
AggregateType: "org",
|
||||
AggregateID: a.UserID,
|
||||
Type: "member.added",
|
||||
Payload: a,
|
||||
},
|
||||
}
|
||||
}
|
||||
// // Events implements [eventer].
|
||||
// func (a *AddMemberCommand) Events() []*eventstore.Event {
|
||||
// return []*eventstore.Event{
|
||||
// {
|
||||
// AggregateType: "org",
|
||||
// AggregateID: a.UserID,
|
||||
// Type: "member.added",
|
||||
// Payload: a,
|
||||
// },
|
||||
// }
|
||||
// }
|
||||
|
||||
var (
|
||||
_ Commander = (*AddMemberCommand)(nil)
|
||||
_ eventer = (*AddMemberCommand)(nil)
|
||||
)
|
||||
// var (
|
||||
// _ Commander = (*AddMemberCommand)(nil)
|
||||
// _ eventer = (*AddMemberCommand)(nil)
|
||||
// )
|
||||
|
@@ -1,74 +1,74 @@
|
||||
package domain
|
||||
|
||||
import (
|
||||
"context"
|
||||
// import (
|
||||
// "context"
|
||||
|
||||
"github.com/zitadel/zitadel/backend/v3/storage/eventstore"
|
||||
)
|
||||
// "github.com/zitadel/zitadel/backend/v3/storage/eventstore"
|
||||
// )
|
||||
|
||||
// SetEmailCommand sets the email address of a user.
|
||||
// If allows verification as a sub command.
|
||||
// The verification command is executed after the email address is set.
|
||||
// The verification command is executed in the same transaction as the email address update.
|
||||
type SetEmailCommand struct {
|
||||
UserID string `json:"userId"`
|
||||
Email string `json:"email"`
|
||||
verification Commander
|
||||
}
|
||||
// // SetEmailCommand sets the email address of a user.
|
||||
// // If allows verification as a sub command.
|
||||
// // The verification command is executed after the email address is set.
|
||||
// // The verification command is executed in the same transaction as the email address update.
|
||||
// type SetEmailCommand struct {
|
||||
// UserID string `json:"userId"`
|
||||
// Email string `json:"email"`
|
||||
// verification Commander
|
||||
// }
|
||||
|
||||
var (
|
||||
_ Commander = (*SetEmailCommand)(nil)
|
||||
_ eventer = (*SetEmailCommand)(nil)
|
||||
_ CreateHumanOpt = (*SetEmailCommand)(nil)
|
||||
)
|
||||
// var (
|
||||
// _ Commander = (*SetEmailCommand)(nil)
|
||||
// _ eventer = (*SetEmailCommand)(nil)
|
||||
// _ CreateHumanOpt = (*SetEmailCommand)(nil)
|
||||
// )
|
||||
|
||||
type SetEmailOpt interface {
|
||||
applyOnSetEmail(*SetEmailCommand)
|
||||
}
|
||||
// type SetEmailOpt interface {
|
||||
// applyOnSetEmail(*SetEmailCommand)
|
||||
// }
|
||||
|
||||
func NewSetEmailCommand(userID, email string, verificationType SetEmailOpt) *SetEmailCommand {
|
||||
cmd := &SetEmailCommand{
|
||||
UserID: userID,
|
||||
Email: email,
|
||||
}
|
||||
verificationType.applyOnSetEmail(cmd)
|
||||
return cmd
|
||||
}
|
||||
// func NewSetEmailCommand(userID, email string, verificationType SetEmailOpt) *SetEmailCommand {
|
||||
// cmd := &SetEmailCommand{
|
||||
// UserID: userID,
|
||||
// Email: email,
|
||||
// }
|
||||
// verificationType.applyOnSetEmail(cmd)
|
||||
// return cmd
|
||||
// }
|
||||
|
||||
// String implements [Commander].
|
||||
func (cmd *SetEmailCommand) String() string {
|
||||
return "SetEmailCommand"
|
||||
}
|
||||
// // String implements [Commander].
|
||||
// func (cmd *SetEmailCommand) String() string {
|
||||
// return "SetEmailCommand"
|
||||
// }
|
||||
|
||||
func (cmd *SetEmailCommand) Execute(ctx context.Context, opts *CommandOpts) error {
|
||||
close, err := opts.EnsureTx(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() { err = close(ctx, err) }()
|
||||
// userStatement(opts.DB).Human().ByID(cmd.UserID).SetEmail(ctx, cmd.Email)
|
||||
repo := userRepo(opts.DB).Human()
|
||||
err = repo.Update(ctx, repo.IDCondition(cmd.UserID), repo.SetEmailAddress(cmd.Email))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// func (cmd *SetEmailCommand) Execute(ctx context.Context, opts *CommandOpts) error {
|
||||
// close, err := opts.EnsureTx(ctx)
|
||||
// if err != nil {
|
||||
// return err
|
||||
// }
|
||||
// defer func() { err = close(ctx, err) }()
|
||||
// // userStatement(opts.DB).Human().ByID(cmd.UserID).SetEmail(ctx, cmd.Email)
|
||||
// repo := userRepo(opts.DB).Human()
|
||||
// err = repo.Update(ctx, repo.IDCondition(cmd.UserID), repo.SetEmailAddress(cmd.Email))
|
||||
// if err != nil {
|
||||
// return err
|
||||
// }
|
||||
|
||||
return opts.Invoke(ctx, cmd.verification)
|
||||
}
|
||||
// return opts.Invoke(ctx, cmd.verification)
|
||||
// }
|
||||
|
||||
// Events implements [eventer].
|
||||
func (cmd *SetEmailCommand) Events() []*eventstore.Event {
|
||||
return []*eventstore.Event{
|
||||
{
|
||||
AggregateType: "user",
|
||||
AggregateID: cmd.UserID,
|
||||
Type: "user.email.set",
|
||||
Payload: cmd,
|
||||
},
|
||||
}
|
||||
}
|
||||
// // Events implements [eventer].
|
||||
// func (cmd *SetEmailCommand) Events() []*eventstore.Event {
|
||||
// return []*eventstore.Event{
|
||||
// {
|
||||
// AggregateType: "user",
|
||||
// AggregateID: cmd.UserID,
|
||||
// Type: "user.email.set",
|
||||
// Payload: cmd,
|
||||
// },
|
||||
// }
|
||||
// }
|
||||
|
||||
// applyOnCreateHuman implements [CreateHumanOpt].
|
||||
func (cmd *SetEmailCommand) applyOnCreateHuman(createUserCmd *CreateUserCommand) {
|
||||
createUserCmd.email = cmd
|
||||
}
|
||||
// // applyOnCreateHuman implements [CreateHumanOpt].
|
||||
// func (cmd *SetEmailCommand) applyOnCreateHuman(createUserCmd *CreateUserCommand) {
|
||||
// createUserCmd.email = cmd
|
||||
// }
|
||||
|
@@ -20,7 +20,7 @@ func (a *and) Write(builder *StatementBuilder) {
|
||||
if i > 0 {
|
||||
builder.WriteString(" AND ")
|
||||
}
|
||||
condition.(Condition).Write(builder)
|
||||
condition.Write(builder)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,7 +45,7 @@ func (o *or) Write(builder *StatementBuilder) {
|
||||
if i > 0 {
|
||||
builder.WriteString(" OR ")
|
||||
}
|
||||
condition.(Condition).Write(builder)
|
||||
condition.Write(builder)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -85,7 +85,7 @@ func (i *isNotNull) Write(builder *StatementBuilder) {
|
||||
|
||||
// IsNotNull creates a condition that checks if a column is NOT NULL.
|
||||
func IsNotNull(column Column) *isNotNull {
|
||||
return &isNotNull{column: column.(Column)}
|
||||
return &isNotNull{column: column}
|
||||
}
|
||||
|
||||
var _ Condition = (*isNotNull)(nil)
|
||||
|
@@ -16,6 +16,7 @@ type Pool interface {
|
||||
|
||||
type PoolTest interface {
|
||||
Pool
|
||||
// MigrateTest is the same as [Migrator] but executes the migrations multiple times instead of only once.
|
||||
MigrateTest(ctx context.Context) error
|
||||
}
|
||||
|
||||
@@ -35,6 +36,7 @@ type Querier interface {
|
||||
}
|
||||
|
||||
// Executor is a database client that can execute statements.
|
||||
// It returns the number of rows affected or an error
|
||||
type Executor interface {
|
||||
Exec(ctx context.Context, stmt string, args ...any) (int64, error)
|
||||
}
|
||||
|
@@ -34,7 +34,7 @@ type Config struct {
|
||||
// // The value will be taken as is. Multiple options are space separated.
|
||||
// Options string
|
||||
|
||||
configuredFields []string
|
||||
// configuredFields []string
|
||||
}
|
||||
|
||||
// Connect implements [database.Connector].
|
||||
|
@@ -10,3 +10,17 @@ CREATE TABLE IF NOT EXISTS zitadel.instances(
|
||||
updated_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
deleted_at TIMESTAMPTZ DEFAULT NULL
|
||||
);
|
||||
|
||||
CREATE OR REPLACE FUNCTION zitadel.set_updated_at()
|
||||
RETURNS TRIGGER AS $$
|
||||
BEGIN
|
||||
NEW.updated_at := NOW();
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
CREATE TRIGGER trigger_set_updated_at
|
||||
BEFORE UPDATE ON zitadel.instances
|
||||
FOR EACH ROW
|
||||
WHEN (OLD.updated_at IS NOT DISTINCT FROM NEW.updated_at)
|
||||
EXECUTE FUNCTION zitadel.set_updated_at();
|
||||
|
@@ -83,11 +83,6 @@ func (c *pgxPool) Migrate(ctx context.Context) error {
|
||||
|
||||
// Migrate implements [database.PoolTest].
|
||||
func (c *pgxPool) MigrateTest(ctx context.Context) error {
|
||||
// allow multiple migrations
|
||||
// if isMigrated {
|
||||
// return nil
|
||||
// }
|
||||
|
||||
client, err := c.Pool.Acquire(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
|
@@ -2,6 +2,7 @@ package postgres
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
|
||||
"github.com/jackc/pgx/v5"
|
||||
|
||||
@@ -25,7 +26,10 @@ func (tx *pgxTx) Rollback(ctx context.Context) error {
|
||||
// End implements [database.Transaction].
|
||||
func (tx *pgxTx) End(ctx context.Context, err error) error {
|
||||
if err != nil {
|
||||
tx.Rollback(ctx)
|
||||
rollbackErr := tx.Rollback(ctx)
|
||||
if rollbackErr != nil {
|
||||
err = errors.Join(err, rollbackErr)
|
||||
}
|
||||
return err
|
||||
}
|
||||
return tx.Commit(ctx)
|
||||
|
@@ -105,14 +105,25 @@ func TestServer_TestInstanceDeleteReduces(t *testing.T) {
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
instanceRepo := repository.InstanceRepository(pool)
|
||||
|
||||
// check instance exists
|
||||
retryDuration, tick := integration.WaitForAndTickWithMaxDuration(CTX, time.Minute)
|
||||
assert.EventuallyWithT(t, func(ttt *assert.CollectT) {
|
||||
instance, err := instanceRepo.Get(CTX,
|
||||
instanceRepo.NameCondition(database.TextOperationEqual, instanceName),
|
||||
)
|
||||
require.NoError(ttt, err)
|
||||
require.Equal(ttt, instanceName, instance.Name)
|
||||
}, retryDuration, tick)
|
||||
|
||||
_, err = SystemClient.RemoveInstance(CTX, &system.RemoveInstanceRequest{
|
||||
InstanceId: res.InstanceId,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
instanceRepo := repository.InstanceRepository(pool)
|
||||
retryDuration, tick := integration.WaitForAndTickWithMaxDuration(CTX, time.Minute)
|
||||
assert.EventuallyWithT(t, func(t *assert.CollectT) {
|
||||
retryDuration, tick = integration.WaitForAndTickWithMaxDuration(CTX, time.Minute)
|
||||
assert.EventuallyWithT(t, func(ttt *assert.CollectT) {
|
||||
instance, err := instanceRepo.Get(CTX,
|
||||
instanceRepo.NameCondition(database.TextOperationEqual, instanceName),
|
||||
)
|
||||
|
@@ -46,7 +46,7 @@ func (opts *QueryOpts) WriteOrderBy(builder *StatementBuilder) {
|
||||
return
|
||||
}
|
||||
builder.WriteString(" ORDER BY ")
|
||||
Columns(opts.OrderBy).Write(builder)
|
||||
opts.OrderBy.Write(builder)
|
||||
}
|
||||
|
||||
func (opts *QueryOpts) WriteLimit(builder *StatementBuilder) {
|
||||
|
@@ -6,6 +6,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/jackc/pgx/v5/pgconn"
|
||||
|
||||
"github.com/zitadel/zitadel/backend/v3/domain"
|
||||
"github.com/zitadel/zitadel/backend/v3/storage/database"
|
||||
)
|
||||
@@ -33,28 +34,27 @@ const queryInstanceStmt = `SELECT id, name, default_org_id, iam_project_id, cons
|
||||
|
||||
// Get implements [domain.InstanceRepository].
|
||||
func (i *instance) Get(ctx context.Context, opts ...database.Condition) (*domain.Instance, error) {
|
||||
builder := database.StatementBuilder{}
|
||||
var builder database.StatementBuilder
|
||||
|
||||
builder.WriteString(queryInstanceStmt)
|
||||
|
||||
// return only non deleted isntances
|
||||
// return only non deleted instances
|
||||
opts = append(opts, database.IsNull(i.DeletedAtColumn()))
|
||||
andCondition := database.And(opts...)
|
||||
i.writeCondition(&builder, andCondition)
|
||||
i.writeCondition(&builder, database.And(opts...))
|
||||
|
||||
return scanInstance(ctx, i.client, &builder)
|
||||
}
|
||||
|
||||
// List implements [domain.InstanceRepository].
|
||||
func (i *instance) List(ctx context.Context, opts ...database.Condition) ([]*domain.Instance, error) {
|
||||
builder := database.StatementBuilder{}
|
||||
var builder database.StatementBuilder
|
||||
|
||||
builder.WriteString(queryInstanceStmt)
|
||||
|
||||
// return only non deleted isntances
|
||||
// return only non deleted instances
|
||||
opts = append(opts, database.IsNull(i.DeletedAtColumn()))
|
||||
andCondition := database.And(opts...)
|
||||
i.writeCondition(&builder, andCondition)
|
||||
notDeletedCondition := database.And(opts...)
|
||||
i.writeCondition(&builder, notDeletedCondition)
|
||||
|
||||
return scanInstances(ctx, i.client, &builder)
|
||||
}
|
||||
@@ -65,7 +65,8 @@ const createInstanceStmt = `INSERT INTO zitadel.instances (id, name, default_org
|
||||
|
||||
// Create implements [domain.InstanceRepository].
|
||||
func (i *instance) Create(ctx context.Context, instance *domain.Instance) error {
|
||||
builder := database.StatementBuilder{}
|
||||
var builder database.StatementBuilder
|
||||
|
||||
builder.AppendArgs(instance.ID, instance.Name, instance.DefaultOrgID, instance.IAMProjectID, instance.ConsoleClientID, instance.ConsoleAppID, instance.DefaultLanguage)
|
||||
builder.WriteString(createInstanceStmt)
|
||||
|
||||
@@ -95,10 +96,14 @@ func (i *instance) Create(ctx context.Context, instance *domain.Instance) error
|
||||
|
||||
// Update implements [domain.InstanceRepository].
|
||||
func (i instance) Update(ctx context.Context, condition database.Condition, changes ...database.Change) (int64, error) {
|
||||
builder := database.StatementBuilder{}
|
||||
var builder database.StatementBuilder
|
||||
|
||||
builder.WriteString(`UPDATE zitadel.instances SET `)
|
||||
|
||||
// don't update deleted instances
|
||||
conditions := []database.Condition{condition, database.IsNull(i.DeletedAtColumn())}
|
||||
database.Changes(changes).Write(&builder)
|
||||
i.writeCondition(&builder, condition)
|
||||
i.writeCondition(&builder, database.And(conditions...))
|
||||
|
||||
stmt := builder.String()
|
||||
|
||||
@@ -111,7 +116,8 @@ func (i instance) Delete(ctx context.Context, condition database.Condition) erro
|
||||
if condition == nil {
|
||||
return errors.New("Delete must contain a condition") // (otherwise ALL instances will be deleted)
|
||||
}
|
||||
builder := database.StatementBuilder{}
|
||||
var builder database.StatementBuilder
|
||||
|
||||
builder.WriteString(`UPDATE zitadel.instances SET deleted_at = $1`)
|
||||
builder.AppendArgs(time.Now())
|
||||
|
||||
|
@@ -7,7 +7,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/brianvoe/gofakeit/v6"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/zitadel/zitadel/backend/v3/domain"
|
||||
"github.com/zitadel/zitadel/backend/v3/storage/database"
|
||||
@@ -17,7 +17,7 @@ import (
|
||||
func TestCreateInstance(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
testFunc func() *domain.Instance
|
||||
testFunc func(ctx context.Context, t *testing.T) *domain.Instance
|
||||
instance domain.Instance
|
||||
err error
|
||||
}{
|
||||
@@ -39,7 +39,7 @@ func TestCreateInstance(t *testing.T) {
|
||||
}(),
|
||||
},
|
||||
{
|
||||
name: "create instance wihtout name",
|
||||
name: "create instance without name",
|
||||
instance: func() domain.Instance {
|
||||
instanceId := gofakeit.Name()
|
||||
// instanceName := gofakeit.Name()
|
||||
@@ -58,12 +58,11 @@ func TestCreateInstance(t *testing.T) {
|
||||
},
|
||||
{
|
||||
name: "adding same instance twice",
|
||||
testFunc: func() *domain.Instance {
|
||||
testFunc: func(ctx context.Context, t *testing.T) *domain.Instance {
|
||||
instanceRepo := repository.InstanceRepository(pool)
|
||||
instanceId := gofakeit.Name()
|
||||
instanceName := gofakeit.Name()
|
||||
|
||||
ctx := context.Background()
|
||||
inst := domain.Instance{
|
||||
ID: instanceId,
|
||||
Name: instanceName,
|
||||
@@ -75,7 +74,7 @@ func TestCreateInstance(t *testing.T) {
|
||||
}
|
||||
|
||||
err := instanceRepo.Create(ctx, &inst)
|
||||
assert.NoError(t, err)
|
||||
require.NoError(t, err)
|
||||
return &inst
|
||||
},
|
||||
err: errors.New("instance id already exists"),
|
||||
@@ -105,7 +104,7 @@ func TestCreateInstance(t *testing.T) {
|
||||
|
||||
var instance *domain.Instance
|
||||
if tt.testFunc != nil {
|
||||
instance = tt.testFunc()
|
||||
instance = tt.testFunc(ctx, t)
|
||||
} else {
|
||||
instance = &tt.instance
|
||||
}
|
||||
@@ -114,7 +113,7 @@ func TestCreateInstance(t *testing.T) {
|
||||
// create instance
|
||||
beforeCreate := time.Now()
|
||||
err := instanceRepo.Create(ctx, instance)
|
||||
assert.Equal(t, tt.err, err)
|
||||
require.Equal(t, tt.err, err)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
@@ -124,17 +123,18 @@ func TestCreateInstance(t *testing.T) {
|
||||
instance, err = instanceRepo.Get(ctx,
|
||||
instanceRepo.NameCondition(database.TextOperationEqual, instance.Name),
|
||||
)
|
||||
assert.Equal(t, tt.instance.ID, instance.ID)
|
||||
assert.Equal(t, tt.instance.Name, instance.Name)
|
||||
assert.Equal(t, tt.instance.DefaultOrgID, instance.DefaultOrgID)
|
||||
assert.Equal(t, tt.instance.IAMProjectID, instance.IAMProjectID)
|
||||
assert.Equal(t, tt.instance.ConsoleClientID, instance.ConsoleClientID)
|
||||
assert.Equal(t, tt.instance.ConsoleAppID, instance.ConsoleAppID)
|
||||
assert.Equal(t, tt.instance.DefaultLanguage, instance.DefaultLanguage)
|
||||
assert.WithinRange(t, instance.CreatedAt, beforeCreate, afterCreate)
|
||||
assert.WithinRange(t, instance.UpdatedAt, beforeCreate, afterCreate)
|
||||
assert.Nil(t, instance.DeletedAt)
|
||||
assert.NoError(t, err)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Equal(t, tt.instance.ID, instance.ID)
|
||||
require.Equal(t, tt.instance.Name, instance.Name)
|
||||
require.Equal(t, tt.instance.DefaultOrgID, instance.DefaultOrgID)
|
||||
require.Equal(t, tt.instance.IAMProjectID, instance.IAMProjectID)
|
||||
require.Equal(t, tt.instance.ConsoleClientID, instance.ConsoleClientID)
|
||||
require.Equal(t, tt.instance.ConsoleAppID, instance.ConsoleAppID)
|
||||
require.Equal(t, tt.instance.DefaultLanguage, instance.DefaultLanguage)
|
||||
require.WithinRange(t, instance.CreatedAt, beforeCreate, afterCreate)
|
||||
require.WithinRange(t, instance.UpdatedAt, beforeCreate, afterCreate)
|
||||
require.Nil(t, instance.DeletedAt)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -142,17 +142,16 @@ func TestCreateInstance(t *testing.T) {
|
||||
func TestUpdateInstance(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
testFunc func() *domain.Instance
|
||||
testFunc func(ctx context.Context, t *testing.T) *domain.Instance
|
||||
rowsAffected int64
|
||||
}{
|
||||
{
|
||||
name: "happy path",
|
||||
testFunc: func() *domain.Instance {
|
||||
testFunc: func(ctx context.Context, t *testing.T) *domain.Instance {
|
||||
instanceRepo := repository.InstanceRepository(pool)
|
||||
instanceId := gofakeit.Name()
|
||||
instanceName := gofakeit.Name()
|
||||
|
||||
ctx := context.Background()
|
||||
inst := domain.Instance{
|
||||
ID: instanceId,
|
||||
Name: instanceName,
|
||||
@@ -165,14 +164,45 @@ func TestUpdateInstance(t *testing.T) {
|
||||
|
||||
// create instance
|
||||
err := instanceRepo.Create(ctx, &inst)
|
||||
assert.NoError(t, err)
|
||||
require.NoError(t, err)
|
||||
return &inst
|
||||
},
|
||||
rowsAffected: 1,
|
||||
},
|
||||
{
|
||||
name: "update deleted instance",
|
||||
testFunc: func(ctx context.Context, t *testing.T) *domain.Instance {
|
||||
instanceRepo := repository.InstanceRepository(pool)
|
||||
instanceId := gofakeit.Name()
|
||||
instanceName := gofakeit.Name()
|
||||
|
||||
inst := domain.Instance{
|
||||
ID: instanceId,
|
||||
Name: instanceName,
|
||||
DefaultOrgID: "defaultOrgId",
|
||||
IAMProjectID: "iamProject",
|
||||
ConsoleClientID: "consoleCLient",
|
||||
ConsoleAppID: "consoleApp",
|
||||
DefaultLanguage: "defaultLanguage",
|
||||
}
|
||||
|
||||
// create instance
|
||||
err := instanceRepo.Create(ctx, &inst)
|
||||
require.NoError(t, err)
|
||||
|
||||
// delete instance
|
||||
err = instanceRepo.Delete(ctx,
|
||||
instanceRepo.IDCondition(inst.ID),
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
return &inst
|
||||
},
|
||||
rowsAffected: 0,
|
||||
},
|
||||
{
|
||||
name: "update non existent instance",
|
||||
testFunc: func() *domain.Instance {
|
||||
testFunc: func(ctx context.Context, t *testing.T) *domain.Instance {
|
||||
instanceId := gofakeit.Name()
|
||||
|
||||
inst := domain.Instance{
|
||||
@@ -185,13 +215,12 @@ func TestUpdateInstance(t *testing.T) {
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
beforeUpdate := time.Now()
|
||||
|
||||
ctx := context.Background()
|
||||
instanceRepo := repository.InstanceRepository(pool)
|
||||
|
||||
instance := tt.testFunc()
|
||||
instance := tt.testFunc(ctx, t)
|
||||
|
||||
beforeUpdate := time.Now()
|
||||
// update name
|
||||
newName := "new_" + instance.Name
|
||||
rowsAffected, err := instanceRepo.Update(ctx,
|
||||
@@ -199,9 +228,9 @@ func TestUpdateInstance(t *testing.T) {
|
||||
instanceRepo.SetName(newName),
|
||||
)
|
||||
afterUpdate := time.Now()
|
||||
assert.NoError(t, err)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, tt.rowsAffected, rowsAffected)
|
||||
require.Equal(t, tt.rowsAffected, rowsAffected)
|
||||
|
||||
if rowsAffected == 0 {
|
||||
return
|
||||
@@ -211,11 +240,11 @@ func TestUpdateInstance(t *testing.T) {
|
||||
instance, err = instanceRepo.Get(ctx,
|
||||
instanceRepo.IDCondition(instance.ID),
|
||||
)
|
||||
assert.NoError(t, err)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, newName, instance.Name)
|
||||
assert.WithinRange(t, instance.UpdatedAt, beforeUpdate, afterUpdate)
|
||||
assert.Nil(t, instance.DeletedAt)
|
||||
require.Equal(t, newName, instance.Name)
|
||||
require.WithinRange(t, instance.UpdatedAt, beforeUpdate, afterUpdate)
|
||||
require.Nil(t, instance.DeletedAt)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -224,9 +253,8 @@ func TestGetInstance(t *testing.T) {
|
||||
instanceRepo := repository.InstanceRepository(pool)
|
||||
type test struct {
|
||||
name string
|
||||
testFunc func() *domain.Instance
|
||||
testFunc func(ctx context.Context, t *testing.T) *domain.Instance
|
||||
conditionClauses []database.Condition
|
||||
noInstanceReturned bool
|
||||
}
|
||||
|
||||
tests := []test{
|
||||
@@ -234,10 +262,9 @@ func TestGetInstance(t *testing.T) {
|
||||
instanceId := gofakeit.Name()
|
||||
return test{
|
||||
name: "happy path get using id",
|
||||
testFunc: func() *domain.Instance {
|
||||
testFunc: func(ctx context.Context, t *testing.T) *domain.Instance {
|
||||
instanceName := gofakeit.Name()
|
||||
|
||||
ctx := context.Background()
|
||||
inst := domain.Instance{
|
||||
ID: instanceId,
|
||||
Name: instanceName,
|
||||
@@ -250,7 +277,7 @@ func TestGetInstance(t *testing.T) {
|
||||
|
||||
// create instance
|
||||
err := instanceRepo.Create(ctx, &inst)
|
||||
assert.NoError(t, err)
|
||||
require.NoError(t, err)
|
||||
return &inst
|
||||
},
|
||||
conditionClauses: []database.Condition{instanceRepo.IDCondition(instanceId)},
|
||||
@@ -260,10 +287,9 @@ func TestGetInstance(t *testing.T) {
|
||||
instanceName := gofakeit.Name()
|
||||
return test{
|
||||
name: "happy path get using name",
|
||||
testFunc: func() *domain.Instance {
|
||||
testFunc: func(ctx context.Context, t *testing.T) *domain.Instance {
|
||||
instanceId := gofakeit.Name()
|
||||
|
||||
ctx := context.Background()
|
||||
inst := domain.Instance{
|
||||
ID: instanceId,
|
||||
Name: instanceName,
|
||||
@@ -276,7 +302,7 @@ func TestGetInstance(t *testing.T) {
|
||||
|
||||
// create instance
|
||||
err := instanceRepo.Create(ctx, &inst)
|
||||
assert.NoError(t, err)
|
||||
require.NoError(t, err)
|
||||
return &inst
|
||||
},
|
||||
conditionClauses: []database.Condition{instanceRepo.NameCondition(database.TextOperationEqual, instanceName)},
|
||||
@@ -284,16 +310,15 @@ func TestGetInstance(t *testing.T) {
|
||||
}(),
|
||||
{
|
||||
name: "get non existent instance",
|
||||
testFunc: func() *domain.Instance {
|
||||
testFunc: func(ctx context.Context, t *testing.T) *domain.Instance {
|
||||
instanceId := gofakeit.Name()
|
||||
|
||||
inst := domain.Instance{
|
||||
_ = domain.Instance{
|
||||
ID: instanceId,
|
||||
}
|
||||
return &inst
|
||||
return nil
|
||||
},
|
||||
conditionClauses: []database.Condition{instanceRepo.NameCondition(database.TextOperationEqual, "non-existent-instance-name")},
|
||||
noInstanceReturned: true,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
@@ -303,58 +328,55 @@ func TestGetInstance(t *testing.T) {
|
||||
|
||||
var instance *domain.Instance
|
||||
if tt.testFunc != nil {
|
||||
instance = tt.testFunc()
|
||||
instance = tt.testFunc(ctx, t)
|
||||
}
|
||||
|
||||
// check instance values
|
||||
returnedInstance, err := instanceRepo.Get(ctx,
|
||||
tt.conditionClauses...,
|
||||
)
|
||||
assert.NoError(t, err)
|
||||
if tt.noInstanceReturned {
|
||||
assert.Nil(t, returnedInstance)
|
||||
require.NoError(t, err)
|
||||
if instance == nil {
|
||||
require.Nil(t, instance, returnedInstance)
|
||||
return
|
||||
}
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, returnedInstance.ID, instance.ID)
|
||||
assert.Equal(t, returnedInstance.Name, instance.Name)
|
||||
assert.Equal(t, returnedInstance.DefaultOrgID, instance.DefaultOrgID)
|
||||
assert.Equal(t, returnedInstance.IAMProjectID, instance.IAMProjectID)
|
||||
assert.Equal(t, returnedInstance.ConsoleClientID, instance.ConsoleClientID)
|
||||
assert.Equal(t, returnedInstance.ConsoleAppID, instance.ConsoleAppID)
|
||||
assert.Equal(t, returnedInstance.DefaultLanguage, instance.DefaultLanguage)
|
||||
assert.NoError(t, err)
|
||||
require.Equal(t, returnedInstance.ID, instance.ID)
|
||||
require.Equal(t, returnedInstance.Name, instance.Name)
|
||||
require.Equal(t, returnedInstance.DefaultOrgID, instance.DefaultOrgID)
|
||||
require.Equal(t, returnedInstance.IAMProjectID, instance.IAMProjectID)
|
||||
require.Equal(t, returnedInstance.ConsoleClientID, instance.ConsoleClientID)
|
||||
require.Equal(t, returnedInstance.ConsoleAppID, instance.ConsoleAppID)
|
||||
require.Equal(t, returnedInstance.DefaultLanguage, instance.DefaultLanguage)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestListInstance(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
pool, stop, err := newEmbeddedDB(ctx)
|
||||
require.NoError(t, err)
|
||||
defer stop()
|
||||
|
||||
type test struct {
|
||||
name string
|
||||
testFunc func() ([]*domain.Instance, database.PoolTest, func())
|
||||
testFunc func(ctx context.Context, t *testing.T) []*domain.Instance
|
||||
conditionClauses []database.Condition
|
||||
noInstanceReturned bool
|
||||
}
|
||||
tests := []test{
|
||||
{
|
||||
name: "happy path single instance no filter",
|
||||
testFunc: func() ([]*domain.Instance, database.PoolTest, func()) {
|
||||
ctx := context.Background()
|
||||
// create new db to make sure no instances exist
|
||||
pool, stop, err := newEmbeededDB()
|
||||
assert.NoError(t, err)
|
||||
|
||||
testFunc: func(ctx context.Context, t *testing.T) []*domain.Instance {
|
||||
instanceRepo := repository.InstanceRepository(pool)
|
||||
noOfInstances := 1
|
||||
instances := make([]*domain.Instance, noOfInstances)
|
||||
for i := range noOfInstances {
|
||||
|
||||
instanceId := gofakeit.Name()
|
||||
instanceName := gofakeit.Name()
|
||||
|
||||
inst := domain.Instance{
|
||||
ID: instanceId,
|
||||
Name: instanceName,
|
||||
ID: gofakeit.Name(),
|
||||
Name: gofakeit.Name(),
|
||||
DefaultOrgID: "defaultOrgId",
|
||||
IAMProjectID: "iamProject",
|
||||
ConsoleClientID: "consoleCLient",
|
||||
@@ -364,33 +386,25 @@ func TestListInstance(t *testing.T) {
|
||||
|
||||
// create instance
|
||||
err := instanceRepo.Create(ctx, &inst)
|
||||
assert.NoError(t, err)
|
||||
require.NoError(t, err)
|
||||
|
||||
instances[i] = &inst
|
||||
}
|
||||
|
||||
return instances, pool, stop
|
||||
return instances
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "happy path multiple instance no filter",
|
||||
testFunc: func() ([]*domain.Instance, database.PoolTest, func()) {
|
||||
ctx := context.Background()
|
||||
// create new db to make sure no instances exist
|
||||
pool, stop, err := newEmbeededDB()
|
||||
assert.NoError(t, err)
|
||||
|
||||
testFunc: func(ctx context.Context, t *testing.T) []*domain.Instance {
|
||||
instanceRepo := repository.InstanceRepository(pool)
|
||||
noOfInstances := 5
|
||||
instances := make([]*domain.Instance, noOfInstances)
|
||||
for i := range noOfInstances {
|
||||
|
||||
instanceId := gofakeit.Name()
|
||||
instanceName := gofakeit.Name()
|
||||
|
||||
inst := domain.Instance{
|
||||
ID: instanceId,
|
||||
Name: instanceName,
|
||||
ID: gofakeit.Name(),
|
||||
Name: gofakeit.Name(),
|
||||
DefaultOrgID: "defaultOrgId",
|
||||
IAMProjectID: "iamProject",
|
||||
ConsoleClientID: "consoleCLient",
|
||||
@@ -400,12 +414,12 @@ func TestListInstance(t *testing.T) {
|
||||
|
||||
// create instance
|
||||
err := instanceRepo.Create(ctx, &inst)
|
||||
assert.NoError(t, err)
|
||||
require.NoError(t, err)
|
||||
|
||||
instances[i] = &inst
|
||||
}
|
||||
|
||||
return instances, pool, stop
|
||||
return instances
|
||||
},
|
||||
},
|
||||
func() test {
|
||||
@@ -413,18 +427,14 @@ func TestListInstance(t *testing.T) {
|
||||
instanceId := gofakeit.Name()
|
||||
return test{
|
||||
name: "instance filter on id",
|
||||
testFunc: func() ([]*domain.Instance, database.PoolTest, func()) {
|
||||
ctx := context.Background()
|
||||
|
||||
testFunc: func(ctx context.Context, t *testing.T) []*domain.Instance {
|
||||
noOfInstances := 1
|
||||
instances := make([]*domain.Instance, noOfInstances)
|
||||
for i := range noOfInstances {
|
||||
|
||||
instanceName := gofakeit.Name()
|
||||
|
||||
inst := domain.Instance{
|
||||
ID: instanceId,
|
||||
Name: instanceName,
|
||||
Name: gofakeit.Name(),
|
||||
DefaultOrgID: "defaultOrgId",
|
||||
IAMProjectID: "iamProject",
|
||||
ConsoleClientID: "consoleCLient",
|
||||
@@ -434,12 +444,12 @@ func TestListInstance(t *testing.T) {
|
||||
|
||||
// create instance
|
||||
err := instanceRepo.Create(ctx, &inst)
|
||||
assert.NoError(t, err)
|
||||
require.NoError(t, err)
|
||||
|
||||
instances[i] = &inst
|
||||
}
|
||||
|
||||
return instances, nil, nil
|
||||
return instances
|
||||
},
|
||||
conditionClauses: []database.Condition{instanceRepo.IDCondition(instanceId)},
|
||||
}
|
||||
@@ -449,17 +459,13 @@ func TestListInstance(t *testing.T) {
|
||||
instanceName := gofakeit.Name()
|
||||
return test{
|
||||
name: "multiple instance filter on name",
|
||||
testFunc: func() ([]*domain.Instance, database.PoolTest, func()) {
|
||||
ctx := context.Background()
|
||||
|
||||
testFunc: func(ctx context.Context, t *testing.T) []*domain.Instance {
|
||||
noOfInstances := 5
|
||||
instances := make([]*domain.Instance, noOfInstances)
|
||||
for i := range noOfInstances {
|
||||
|
||||
instanceId := gofakeit.Name()
|
||||
|
||||
inst := domain.Instance{
|
||||
ID: instanceId,
|
||||
ID: gofakeit.Name(),
|
||||
Name: instanceName,
|
||||
DefaultOrgID: "defaultOrgId",
|
||||
IAMProjectID: "iamProject",
|
||||
@@ -470,12 +476,12 @@ func TestListInstance(t *testing.T) {
|
||||
|
||||
// create instance
|
||||
err := instanceRepo.Create(ctx, &inst)
|
||||
assert.NoError(t, err)
|
||||
require.NoError(t, err)
|
||||
|
||||
instances[i] = &inst
|
||||
}
|
||||
|
||||
return instances, nil, nil
|
||||
return instances
|
||||
},
|
||||
conditionClauses: []database.Condition{instanceRepo.NameCondition(database.TextOperationEqual, instanceName)},
|
||||
}
|
||||
@@ -483,42 +489,34 @@ func TestListInstance(t *testing.T) {
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
t.Cleanup(func() {
|
||||
_, err := pool.Exec(ctx, "DELETE FROM zitadel.instances")
|
||||
require.NoError(t, err)
|
||||
})
|
||||
|
||||
var instances []*domain.Instance
|
||||
instances := tt.testFunc(ctx, t)
|
||||
|
||||
pool := pool
|
||||
if tt.testFunc != nil {
|
||||
var stop func()
|
||||
var pool_ database.PoolTest
|
||||
instances, pool_, stop = tt.testFunc()
|
||||
if pool_ != nil {
|
||||
pool = pool_
|
||||
defer stop()
|
||||
}
|
||||
}
|
||||
instanceRepo := repository.InstanceRepository(pool)
|
||||
|
||||
// check instance values
|
||||
returnedInstances, err := instanceRepo.List(ctx,
|
||||
tt.conditionClauses...,
|
||||
)
|
||||
assert.NoError(t, err)
|
||||
require.NoError(t, err)
|
||||
if tt.noInstanceReturned {
|
||||
assert.Nil(t, returnedInstances)
|
||||
require.Nil(t, returnedInstances)
|
||||
return
|
||||
}
|
||||
|
||||
assert.Equal(t, len(instances), len(returnedInstances))
|
||||
require.Equal(t, len(instances), len(returnedInstances))
|
||||
for i, instance := range instances {
|
||||
assert.Equal(t, returnedInstances[i].ID, instance.ID)
|
||||
assert.Equal(t, returnedInstances[i].Name, instance.Name)
|
||||
assert.Equal(t, returnedInstances[i].DefaultOrgID, instance.DefaultOrgID)
|
||||
assert.Equal(t, returnedInstances[i].IAMProjectID, instance.IAMProjectID)
|
||||
assert.Equal(t, returnedInstances[i].ConsoleClientID, instance.ConsoleClientID)
|
||||
assert.Equal(t, returnedInstances[i].ConsoleAppID, instance.ConsoleAppID)
|
||||
assert.Equal(t, returnedInstances[i].DefaultLanguage, instance.DefaultLanguage)
|
||||
assert.NoError(t, err)
|
||||
require.Equal(t, returnedInstances[i].ID, instance.ID)
|
||||
require.Equal(t, returnedInstances[i].Name, instance.Name)
|
||||
require.Equal(t, returnedInstances[i].DefaultOrgID, instance.DefaultOrgID)
|
||||
require.Equal(t, returnedInstances[i].IAMProjectID, instance.IAMProjectID)
|
||||
require.Equal(t, returnedInstances[i].ConsoleClientID, instance.ConsoleClientID)
|
||||
require.Equal(t, returnedInstances[i].ConsoleAppID, instance.ConsoleAppID)
|
||||
require.Equal(t, returnedInstances[i].DefaultLanguage, instance.DefaultLanguage)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -527,7 +525,7 @@ func TestListInstance(t *testing.T) {
|
||||
func TestDeleteInstance(t *testing.T) {
|
||||
type test struct {
|
||||
name string
|
||||
testFunc func()
|
||||
testFunc func(ctx context.Context, t *testing.T)
|
||||
conditionClauses database.Condition
|
||||
}
|
||||
tests := []test{
|
||||
@@ -536,18 +534,14 @@ func TestDeleteInstance(t *testing.T) {
|
||||
instanceId := gofakeit.Name()
|
||||
return test{
|
||||
name: "happy path delete single instance filter id",
|
||||
testFunc: func() {
|
||||
ctx := context.Background()
|
||||
|
||||
testFunc: func(ctx context.Context, t *testing.T) {
|
||||
noOfInstances := 1
|
||||
instances := make([]*domain.Instance, noOfInstances)
|
||||
for i := range noOfInstances {
|
||||
|
||||
instanceName := gofakeit.Name()
|
||||
|
||||
inst := domain.Instance{
|
||||
ID: instanceId,
|
||||
Name: instanceName,
|
||||
Name: gofakeit.Name(),
|
||||
DefaultOrgID: "defaultOrgId",
|
||||
IAMProjectID: "iamProject",
|
||||
ConsoleClientID: "consoleCLient",
|
||||
@@ -557,7 +551,7 @@ func TestDeleteInstance(t *testing.T) {
|
||||
|
||||
// create instance
|
||||
err := instanceRepo.Create(ctx, &inst)
|
||||
assert.NoError(t, err)
|
||||
require.NoError(t, err)
|
||||
|
||||
instances[i] = &inst
|
||||
}
|
||||
@@ -570,17 +564,13 @@ func TestDeleteInstance(t *testing.T) {
|
||||
instanceName := gofakeit.Name()
|
||||
return test{
|
||||
name: "happy path delete single instance filter name",
|
||||
testFunc: func() {
|
||||
ctx := context.Background()
|
||||
|
||||
testFunc: func(ctx context.Context, t *testing.T) {
|
||||
noOfInstances := 1
|
||||
instances := make([]*domain.Instance, noOfInstances)
|
||||
for i := range noOfInstances {
|
||||
|
||||
instanceId := gofakeit.Name()
|
||||
|
||||
inst := domain.Instance{
|
||||
ID: instanceId,
|
||||
ID: gofakeit.Name(),
|
||||
Name: instanceName,
|
||||
DefaultOrgID: "defaultOrgId",
|
||||
IAMProjectID: "iamProject",
|
||||
@@ -591,7 +581,7 @@ func TestDeleteInstance(t *testing.T) {
|
||||
|
||||
// create instance
|
||||
err := instanceRepo.Create(ctx, &inst)
|
||||
assert.NoError(t, err)
|
||||
require.NoError(t, err)
|
||||
|
||||
instances[i] = &inst
|
||||
}
|
||||
@@ -612,17 +602,13 @@ func TestDeleteInstance(t *testing.T) {
|
||||
instanceName := gofakeit.Name()
|
||||
return test{
|
||||
name: "multiple instance filter on name",
|
||||
testFunc: func() {
|
||||
ctx := context.Background()
|
||||
|
||||
testFunc: func(ctx context.Context, t *testing.T) {
|
||||
noOfInstances := 5
|
||||
instances := make([]*domain.Instance, noOfInstances)
|
||||
for i := range noOfInstances {
|
||||
|
||||
instanceId := gofakeit.Name()
|
||||
|
||||
inst := domain.Instance{
|
||||
ID: instanceId,
|
||||
ID: gofakeit.Name(),
|
||||
Name: instanceName,
|
||||
DefaultOrgID: "defaultOrgId",
|
||||
IAMProjectID: "iamProject",
|
||||
@@ -633,7 +619,7 @@ func TestDeleteInstance(t *testing.T) {
|
||||
|
||||
// create instance
|
||||
err := instanceRepo.Create(ctx, &inst)
|
||||
assert.NoError(t, err)
|
||||
require.NoError(t, err)
|
||||
|
||||
instances[i] = &inst
|
||||
}
|
||||
@@ -646,17 +632,13 @@ func TestDeleteInstance(t *testing.T) {
|
||||
instanceName := gofakeit.Name()
|
||||
return test{
|
||||
name: "deleted already deleted instance",
|
||||
testFunc: func() {
|
||||
ctx := context.Background()
|
||||
|
||||
testFunc: func(ctx context.Context, t *testing.T) {
|
||||
noOfInstances := 1
|
||||
instances := make([]*domain.Instance, noOfInstances)
|
||||
for i := range noOfInstances {
|
||||
|
||||
instanceId := gofakeit.Name()
|
||||
|
||||
inst := domain.Instance{
|
||||
ID: instanceId,
|
||||
ID: gofakeit.Name(),
|
||||
Name: instanceName,
|
||||
DefaultOrgID: "defaultOrgId",
|
||||
IAMProjectID: "iamProject",
|
||||
@@ -667,7 +649,7 @@ func TestDeleteInstance(t *testing.T) {
|
||||
|
||||
// create instance
|
||||
err := instanceRepo.Create(ctx, &inst)
|
||||
assert.NoError(t, err)
|
||||
require.NoError(t, err)
|
||||
|
||||
instances[i] = &inst
|
||||
}
|
||||
@@ -676,7 +658,7 @@ func TestDeleteInstance(t *testing.T) {
|
||||
err := instanceRepo.Delete(ctx,
|
||||
instanceRepo.NameCondition(database.TextOperationEqual, instanceName),
|
||||
)
|
||||
assert.NoError(t, err)
|
||||
require.NoError(t, err)
|
||||
},
|
||||
conditionClauses: instanceRepo.NameCondition(database.TextOperationEqual, instanceName),
|
||||
}
|
||||
@@ -688,21 +670,21 @@ func TestDeleteInstance(t *testing.T) {
|
||||
instanceRepo := repository.InstanceRepository(pool)
|
||||
|
||||
if tt.testFunc != nil {
|
||||
tt.testFunc()
|
||||
tt.testFunc(ctx, t)
|
||||
}
|
||||
|
||||
// delete instance
|
||||
err := instanceRepo.Delete(ctx,
|
||||
tt.conditionClauses,
|
||||
)
|
||||
assert.NoError(t, err)
|
||||
require.NoError(t, err)
|
||||
|
||||
// check instance was deleted
|
||||
instance, err := instanceRepo.Get(ctx,
|
||||
tt.conditionClauses,
|
||||
)
|
||||
assert.NoError(t, err)
|
||||
assert.Nil(t, instance)
|
||||
require.NoError(t, err)
|
||||
require.Nil(t, instance)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@@ -3,6 +3,5 @@ package repository
|
||||
import "github.com/zitadel/zitadel/backend/v3/storage/database"
|
||||
|
||||
type repository struct {
|
||||
// builder database.StatementBuilder
|
||||
client database.QueryExecutor
|
||||
}
|
||||
|
@@ -20,9 +20,10 @@ var pool database.PoolTest
|
||||
func runTests(m *testing.M) int {
|
||||
var stop func()
|
||||
var err error
|
||||
pool, stop, err = newEmbeededDB()
|
||||
ctx := context.Background()
|
||||
pool, stop, err = newEmbeddedDB(ctx)
|
||||
if err != nil {
|
||||
log.Print(err)
|
||||
log.Printf("error with embedded postgres database: %v", err)
|
||||
return 1
|
||||
}
|
||||
defer stop()
|
||||
@@ -30,24 +31,21 @@ func runTests(m *testing.M) int {
|
||||
return m.Run()
|
||||
}
|
||||
|
||||
func newEmbeededDB() (pool database.PoolTest, stop func(), err error) {
|
||||
var connector database.Connector
|
||||
connector, stop, err = embedded.StartEmbedded()
|
||||
func newEmbeddedDB(ctx context.Context) (pool database.PoolTest, stop func(), err error) {
|
||||
connector, stop, err := embedded.StartEmbedded()
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("unable to start embedded postgres: %v", err)
|
||||
return nil, nil, fmt.Errorf("unable to start embedded postgres: %w", err)
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
pool_, err := connector.Connect(ctx)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("unable to connect to embedded postgres: %v", err)
|
||||
return nil, nil, fmt.Errorf("unable to connect to embedded postgres: %w", err)
|
||||
}
|
||||
pool = pool_.(database.PoolTest)
|
||||
|
||||
err = pool.MigrateTest(ctx)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("unable to migrate database: %v", err)
|
||||
return nil, nil, fmt.Errorf("unable to migrate database: %w", err)
|
||||
}
|
||||
return pool, stop, err
|
||||
}
|
||||
|
@@ -191,18 +191,18 @@ func (h userHuman) PhoneVerifiedAtColumn() database.Column {
|
||||
return database.NewColumn("phone_verified_at")
|
||||
}
|
||||
|
||||
func (h userHuman) columns() database.Columns {
|
||||
return append(h.user.columns(),
|
||||
h.FirstNameColumn(),
|
||||
h.LastNameColumn(),
|
||||
h.EmailAddressColumn(),
|
||||
h.EmailVerifiedAtColumn(),
|
||||
h.PhoneNumberColumn(),
|
||||
h.PhoneVerifiedAtColumn(),
|
||||
)
|
||||
}
|
||||
// func (h userHuman) columns() database.Columns {
|
||||
// return append(h.user.columns(),
|
||||
// h.FirstNameColumn(),
|
||||
// h.LastNameColumn(),
|
||||
// h.EmailAddressColumn(),
|
||||
// h.EmailVerifiedAtColumn(),
|
||||
// h.PhoneNumberColumn(),
|
||||
// h.PhoneVerifiedAtColumn(),
|
||||
// )
|
||||
// }
|
||||
|
||||
func (h userHuman) writeReturning(builder *database.StatementBuilder) {
|
||||
builder.WriteString(" RETURNING ")
|
||||
h.columns().Write(builder)
|
||||
}
|
||||
// func (h userHuman) writeReturning(builder *database.StatementBuilder) {
|
||||
// builder.WriteString(" RETURNING ")
|
||||
// h.columns().Write(builder)
|
||||
// }
|
||||
|
@@ -5,11 +5,10 @@ package repository_test
|
||||
// "testing"
|
||||
|
||||
// "github.com/stretchr/testify/assert"
|
||||
// "go.uber.org/mock/gomock"
|
||||
|
||||
// "github.com/zitadel/zitadel/backend/v3/storage/database"
|
||||
// "github.com/zitadel/zitadel/backend/v3/storage/database/dbmock"
|
||||
// "github.com/zitadel/zitadel/backend/v3/storage/database/repository"
|
||||
// "go.uber.org/mock/gomock"
|
||||
// )
|
||||
|
||||
// func TestQueryUser(t *testing.T) {
|
||||
@@ -75,3 +74,4 @@ package repository_test
|
||||
// user.Human().Update(context.Background(), user.IDCondition("test"), user.SetUsername("test"))
|
||||
// })
|
||||
// }
|
||||
|
||||
|
File diff suppressed because one or more lines are too long
@@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
_ "embed"
|
||||
"io"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/jackc/pgx/v5/stdlib"
|
||||
@@ -41,12 +42,16 @@ func copyAuth(ctx context.Context, config *Migration) {
|
||||
logging.OnError(err).Fatal("unable to connect to destination database")
|
||||
defer destClient.Close()
|
||||
|
||||
copyAuthRequests(ctx, sourceClient, destClient)
|
||||
copyAuthRequests(ctx, sourceClient, destClient, config.MaxAuthRequestAge)
|
||||
}
|
||||
|
||||
func copyAuthRequests(ctx context.Context, source, dest *database.DB) {
|
||||
func copyAuthRequests(ctx context.Context, source, dest *database.DB, maxAuthRequestAge time.Duration) {
|
||||
start := time.Now()
|
||||
|
||||
logging.Info("creating index on auth.auth_requests.change_date to speed up copy in source database")
|
||||
_, err := source.ExecContext(ctx, "CREATE INDEX CONCURRENTLY IF NOT EXISTS auth_requests_change_date ON auth.auth_requests (change_date)")
|
||||
logging.OnError(err).Fatal("unable to create index on auth.auth_requests.change_date")
|
||||
|
||||
sourceConn, err := source.Conn(ctx)
|
||||
logging.OnError(err).Fatal("unable to acquire connection")
|
||||
defer sourceConn.Close()
|
||||
@@ -55,9 +60,9 @@ func copyAuthRequests(ctx context.Context, source, dest *database.DB) {
|
||||
errs := make(chan error, 1)
|
||||
|
||||
go func() {
|
||||
err = sourceConn.Raw(func(driverConn interface{}) error {
|
||||
err = sourceConn.Raw(func(driverConn any) error {
|
||||
conn := driverConn.(*stdlib.Conn).Conn()
|
||||
_, err := conn.PgConn().CopyTo(ctx, w, "COPY (SELECT id, regexp_replace(request::TEXT, '\\\\u0000', '', 'g')::JSON request, code, request_type, creation_date, change_date, instance_id FROM auth.auth_requests "+instanceClause()+") TO STDOUT")
|
||||
_, err := conn.PgConn().CopyTo(ctx, w, "COPY (SELECT id, regexp_replace(request::TEXT, '\\\\u0000', '', 'g')::JSON request, code, request_type, creation_date, change_date, instance_id FROM auth.auth_requests "+instanceClause()+" AND change_date > NOW() - INTERVAL '"+strconv.FormatFloat(maxAuthRequestAge.Seconds(), 'f', -1, 64)+" seconds') TO STDOUT")
|
||||
w.Close()
|
||||
return err
|
||||
})
|
||||
@@ -69,7 +74,7 @@ func copyAuthRequests(ctx context.Context, source, dest *database.DB) {
|
||||
defer destConn.Close()
|
||||
|
||||
var affected int64
|
||||
err = destConn.Raw(func(driverConn interface{}) error {
|
||||
err = destConn.Raw(func(driverConn any) error {
|
||||
conn := driverConn.(*stdlib.Conn).Conn()
|
||||
|
||||
if shouldReplace {
|
||||
|
@@ -24,6 +24,7 @@ type Migration struct {
|
||||
Destination database.Config
|
||||
|
||||
EventBulkSize uint32
|
||||
MaxAuthRequestAge time.Duration
|
||||
|
||||
Log *logging.Config
|
||||
Machine *id.Config
|
||||
|
@@ -1,61 +1,64 @@
|
||||
Source:
|
||||
cockroach:
|
||||
Host: localhost # ZITADEL_DATABASE_COCKROACH_HOST
|
||||
Port: 26257 # ZITADEL_DATABASE_COCKROACH_PORT
|
||||
Database: zitadel # ZITADEL_DATABASE_COCKROACH_DATABASE
|
||||
MaxOpenConns: 6 # ZITADEL_DATABASE_COCKROACH_MAXOPENCONNS
|
||||
MaxIdleConns: 6 # ZITADEL_DATABASE_COCKROACH_MAXIDLECONNS
|
||||
MaxConnLifetime: 30m # ZITADEL_DATABASE_COCKROACH_MAXCONNLIFETIME
|
||||
MaxConnIdleTime: 5m # ZITADEL_DATABASE_COCKROACH_MAXCONNIDLETIME
|
||||
Options: "" # ZITADEL_DATABASE_COCKROACH_OPTIONS
|
||||
Host: localhost # ZITADEL_SOURCE_COCKROACH_HOST
|
||||
Port: 26257 # ZITADEL_SOURCE_COCKROACH_PORT
|
||||
Database: zitadel # ZITADEL_SOURCE_COCKROACH_DATABASE
|
||||
MaxOpenConns: 6 # ZITADEL_SOURCE_COCKROACH_MAXOPENCONNS
|
||||
MaxIdleConns: 6 # ZITADEL_SOURCE_COCKROACH_MAXIDLECONNS
|
||||
MaxConnLifetime: 30m # ZITADEL_SOURCE_COCKROACH_MAXCONNLIFETIME
|
||||
MaxConnIdleTime: 5m # ZITADEL_SOURCE_COCKROACH_MAXCONNIDLETIME
|
||||
Options: "" # ZITADEL_SOURCE_COCKROACH_OPTIONS
|
||||
User:
|
||||
Username: zitadel # ZITADEL_DATABASE_COCKROACH_USER_USERNAME
|
||||
Password: "" # ZITADEL_DATABASE_COCKROACH_USER_PASSWORD
|
||||
Username: zitadel # ZITADEL_SOURCE_COCKROACH_USER_USERNAME
|
||||
Password: "" # ZITADEL_SOURCE_COCKROACH_USER_PASSWORD
|
||||
SSL:
|
||||
Mode: disable # ZITADEL_DATABASE_COCKROACH_USER_SSL_MODE
|
||||
RootCert: "" # ZITADEL_DATABASE_COCKROACH_USER_SSL_ROOTCERT
|
||||
Cert: "" # ZITADEL_DATABASE_COCKROACH_USER_SSL_CERT
|
||||
Key: "" # ZITADEL_DATABASE_COCKROACH_USER_SSL_KEY
|
||||
Mode: disable # ZITADEL_SOURCE_COCKROACH_USER_SSL_MODE
|
||||
RootCert: "" # ZITADEL_SOURCE_COCKROACH_USER_SSL_ROOTCERT
|
||||
Cert: "" # ZITADEL_SOURCE_COCKROACH_USER_SSL_CERT
|
||||
Key: "" # ZITADEL_SOURCE_COCKROACH_USER_SSL_KEY
|
||||
# Postgres is used as soon as a value is set
|
||||
# The values describe the possible fields to set values
|
||||
postgres:
|
||||
Host: # ZITADEL_DATABASE_POSTGRES_HOST
|
||||
Port: # ZITADEL_DATABASE_POSTGRES_PORT
|
||||
Database: # ZITADEL_DATABASE_POSTGRES_DATABASE
|
||||
MaxOpenConns: # ZITADEL_DATABASE_POSTGRES_MAXOPENCONNS
|
||||
MaxIdleConns: # ZITADEL_DATABASE_POSTGRES_MAXIDLECONNS
|
||||
MaxConnLifetime: # ZITADEL_DATABASE_POSTGRES_MAXCONNLIFETIME
|
||||
MaxConnIdleTime: # ZITADEL_DATABASE_POSTGRES_MAXCONNIDLETIME
|
||||
Options: # ZITADEL_DATABASE_POSTGRES_OPTIONS
|
||||
Host: # ZITADEL_SOURCE_POSTGRES_HOST
|
||||
Port: # ZITADEL_SOURCE_POSTGRES_PORT
|
||||
Database: # ZITADEL_SOURCE_POSTGRES_DATABASE
|
||||
MaxOpenConns: # ZITADEL_SOURCE_POSTGRES_MAXOPENCONNS
|
||||
MaxIdleConns: # ZITADEL_SOURCE_POSTGRES_MAXIDLECONNS
|
||||
MaxConnLifetime: # ZITADEL_SOURCE_POSTGRES_MAXCONNLIFETIME
|
||||
MaxConnIdleTime: # ZITADEL_SOURCE_POSTGRES_MAXCONNIDLETIME
|
||||
Options: # ZITADEL_SOURCE_POSTGRES_OPTIONS
|
||||
User:
|
||||
Username: # ZITADEL_DATABASE_POSTGRES_USER_USERNAME
|
||||
Password: # ZITADEL_DATABASE_POSTGRES_USER_PASSWORD
|
||||
Username: # ZITADEL_SOURCE_POSTGRES_USER_USERNAME
|
||||
Password: # ZITADEL_SOURCE_POSTGRES_USER_PASSWORD
|
||||
SSL:
|
||||
Mode: # ZITADEL_DATABASE_POSTGRES_USER_SSL_MODE
|
||||
RootCert: # ZITADEL_DATABASE_POSTGRES_USER_SSL_ROOTCERT
|
||||
Cert: # ZITADEL_DATABASE_POSTGRES_USER_SSL_CERT
|
||||
Key: # ZITADEL_DATABASE_POSTGRES_USER_SSL_KEY
|
||||
Mode: # ZITADEL_SOURCE_POSTGRES_USER_SSL_MODE
|
||||
RootCert: # ZITADEL_SOURCE_POSTGRES_USER_SSL_ROOTCERT
|
||||
Cert: # ZITADEL_SOURCE_POSTGRES_USER_SSL_CERT
|
||||
Key: # ZITADEL_SOURCE_POSTGRES_USER_SSL_KEY
|
||||
|
||||
Destination:
|
||||
postgres:
|
||||
Host: localhost # ZITADEL_DATABASE_POSTGRES_HOST
|
||||
Port: 5432 # ZITADEL_DATABASE_POSTGRES_PORT
|
||||
Database: zitadel # ZITADEL_DATABASE_POSTGRES_DATABASE
|
||||
MaxOpenConns: 5 # ZITADEL_DATABASE_POSTGRES_MAXOPENCONNS
|
||||
MaxIdleConns: 2 # ZITADEL_DATABASE_POSTGRES_MAXIDLECONNS
|
||||
MaxConnLifetime: 30m # ZITADEL_DATABASE_POSTGRES_MAXCONNLIFETIME
|
||||
MaxConnIdleTime: 5m # ZITADEL_DATABASE_POSTGRES_MAXCONNIDLETIME
|
||||
Options: "" # ZITADEL_DATABASE_POSTGRES_OPTIONS
|
||||
Host: localhost # ZITADEL_DESTINATION_POSTGRES_HOST
|
||||
Port: 5432 # ZITADEL_DESTINATION_POSTGRES_PORT
|
||||
Database: zitadel # ZITADEL_DESTINATION_POSTGRES_DATABASE
|
||||
MaxOpenConns: 5 # ZITADEL_DESTINATION_POSTGRES_MAXOPENCONNS
|
||||
MaxIdleConns: 2 # ZITADEL_DESTINATION_POSTGRES_MAXIDLECONNS
|
||||
MaxConnLifetime: 30m # ZITADEL_DESTINATION_POSTGRES_MAXCONNLIFETIME
|
||||
MaxConnIdleTime: 5m # ZITADEL_DESTINATION_POSTGRES_MAXCONNIDLETIME
|
||||
Options: "" # ZITADEL_DESTINATION_POSTGRES_OPTIONS
|
||||
User:
|
||||
Username: zitadel # ZITADEL_DATABASE_POSTGRES_USER_USERNAME
|
||||
Password: "" # ZITADEL_DATABASE_POSTGRES_USER_PASSWORD
|
||||
Username: zitadel # ZITADEL_DESTINATION_POSTGRES_USER_USERNAME
|
||||
Password: "" # ZITADEL_DESTINATION_POSTGRES_USER_PASSWORD
|
||||
SSL:
|
||||
Mode: disable # ZITADEL_DATABASE_POSTGRES_USER_SSL_MODE
|
||||
RootCert: "" # ZITADEL_DATABASE_POSTGRES_USER_SSL_ROOTCERT
|
||||
Cert: "" # ZITADEL_DATABASE_POSTGRES_USER_SSL_CERT
|
||||
Key: "" # ZITADEL_DATABASE_POSTGRES_USER_SSL_KEY
|
||||
Mode: disable # ZITADEL_DESTINATION_POSTGRES_USER_SSL_MODE
|
||||
RootCert: "" # ZITADEL_DESTINATION_POSTGRES_USER_SSL_ROOTCERT
|
||||
Cert: "" # ZITADEL_DESTINATION_POSTGRES_USER_SSL_CERT
|
||||
Key: "" # ZITADEL_DESTINATION_POSTGRES_USER_SSL_KEY
|
||||
|
||||
EventBulkSize: 10000
|
||||
EventBulkSize: 10000 # ZITADEL_EVENTBULKSIZE
|
||||
# The maximum duration an auth request was last updated before it gets ignored.
|
||||
# Default is 30 days
|
||||
MaxAuthRequestAge: 720h # ZITADEL_MAXAUTHREQUESTAGE
|
||||
|
||||
Projections:
|
||||
# The maximum duration a transaction remains open
|
||||
@@ -64,14 +67,14 @@ Projections:
|
||||
TransactionDuration: 0s # ZITADEL_PROJECTIONS_TRANSACTIONDURATION
|
||||
# turn off scheduler during operation
|
||||
RequeueEvery: 0s
|
||||
ConcurrentInstances: 7
|
||||
EventBulkLimit: 1000
|
||||
ConcurrentInstances: 7 # ZITADEL_PROJECTIONS_CONCURRENTINSTANCES
|
||||
EventBulkLimit: 1000 # ZITADEL_PROJECTIONS_EVENTBULKLIMIT
|
||||
Customizations:
|
||||
notifications:
|
||||
MaxFailureCount: 1
|
||||
|
||||
Eventstore:
|
||||
MaxRetries: 3
|
||||
MaxRetries: 3 # ZITADEL_EVENTSTORE_MAXRETRIES
|
||||
|
||||
Auth:
|
||||
Spooler:
|
||||
|
@@ -3,6 +3,8 @@ package mirror
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/shopspring/decimal"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/v2/eventstore"
|
||||
"github.com/zitadel/zitadel/internal/v2/readmodel"
|
||||
"github.com/zitadel/zitadel/internal/v2/system"
|
||||
@@ -29,7 +31,7 @@ func queryLastSuccessfulMigration(ctx context.Context, destinationES *eventstore
|
||||
return lastSuccess, nil
|
||||
}
|
||||
|
||||
func writeMigrationSucceeded(ctx context.Context, destinationES *eventstore.EventStore, id, source string, position float64) error {
|
||||
func writeMigrationSucceeded(ctx context.Context, destinationES *eventstore.EventStore, id, source string, position decimal.Decimal) error {
|
||||
return destinationES.Push(
|
||||
ctx,
|
||||
eventstore.NewPushIntent(
|
||||
|
@@ -8,7 +8,9 @@ import (
|
||||
"io"
|
||||
"time"
|
||||
|
||||
"github.com/jackc/pgx/v5/pgconn"
|
||||
"github.com/jackc/pgx/v5/stdlib"
|
||||
"github.com/shopspring/decimal"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
"github.com/zitadel/logging"
|
||||
@@ -69,6 +71,7 @@ func positionQuery(db *db.DB) string {
|
||||
}
|
||||
|
||||
func copyEvents(ctx context.Context, source, dest *db.DB, bulkSize uint32) {
|
||||
logging.Info("starting to copy events")
|
||||
start := time.Now()
|
||||
reader, writer := io.Pipe()
|
||||
|
||||
@@ -88,7 +91,7 @@ func copyEvents(ctx context.Context, source, dest *db.DB, bulkSize uint32) {
|
||||
previousMigration, err := queryLastSuccessfulMigration(ctx, destinationES, source.DatabaseName())
|
||||
logging.OnError(err).Fatal("unable to query latest successful migration")
|
||||
|
||||
var maxPosition float64
|
||||
var maxPosition decimal.Decimal
|
||||
err = source.QueryRowContext(ctx,
|
||||
func(row *sql.Row) error {
|
||||
return row.Scan(&maxPosition)
|
||||
@@ -100,7 +103,7 @@ func copyEvents(ctx context.Context, source, dest *db.DB, bulkSize uint32) {
|
||||
logging.WithFields("from", previousMigration.Position, "to", maxPosition).Info("start event migration")
|
||||
|
||||
nextPos := make(chan bool, 1)
|
||||
pos := make(chan float64, 1)
|
||||
pos := make(chan decimal.Decimal, 1)
|
||||
errs := make(chan error, 3)
|
||||
|
||||
go func() {
|
||||
@@ -130,7 +133,10 @@ func copyEvents(ctx context.Context, source, dest *db.DB, bulkSize uint32) {
|
||||
if err != nil {
|
||||
return zerrors.ThrowUnknownf(err, "MIGRA-KTuSq", "unable to copy events from source during iteration %d", i)
|
||||
}
|
||||
logging.WithFields("batch_count", i).Info("batch of events copied")
|
||||
|
||||
if tag.RowsAffected() < int64(bulkSize) {
|
||||
logging.WithFields("batch_count", i).Info("last batch of events copied")
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -148,7 +154,7 @@ func copyEvents(ctx context.Context, source, dest *db.DB, bulkSize uint32) {
|
||||
go func() {
|
||||
defer close(pos)
|
||||
for range nextPos {
|
||||
var position float64
|
||||
var position decimal.Decimal
|
||||
err := dest.QueryRowContext(
|
||||
ctx,
|
||||
func(row *sql.Row) error {
|
||||
@@ -171,6 +177,10 @@ func copyEvents(ctx context.Context, source, dest *db.DB, bulkSize uint32) {
|
||||
tag, err := conn.PgConn().CopyFrom(ctx, reader, "COPY eventstore.events2 FROM STDIN")
|
||||
eventCount = tag.RowsAffected()
|
||||
if err != nil {
|
||||
pgErr := new(pgconn.PgError)
|
||||
errors.As(err, &pgErr)
|
||||
|
||||
logging.WithError(err).WithField("pg_err_details", pgErr.Detail).Error("unable to copy events into destination")
|
||||
return zerrors.ThrowUnknown(err, "MIGRA-DTHi7", "unable to copy events into destination")
|
||||
}
|
||||
|
||||
@@ -183,7 +193,7 @@ func copyEvents(ctx context.Context, source, dest *db.DB, bulkSize uint32) {
|
||||
logging.WithFields("took", time.Since(start), "count", eventCount).Info("events migrated")
|
||||
}
|
||||
|
||||
func writeCopyEventsDone(ctx context.Context, es *eventstore.EventStore, id, source string, position float64, errs <-chan error) {
|
||||
func writeCopyEventsDone(ctx context.Context, es *eventstore.EventStore, id, source string, position decimal.Decimal, errs <-chan error) {
|
||||
joinedErrs := make([]error, 0, len(errs))
|
||||
for err := range errs {
|
||||
joinedErrs = append(joinedErrs, err)
|
||||
@@ -202,6 +212,7 @@ func writeCopyEventsDone(ctx context.Context, es *eventstore.EventStore, id, sou
|
||||
}
|
||||
|
||||
func copyUniqueConstraints(ctx context.Context, source, dest *db.DB) {
|
||||
logging.Info("starting to copy unique constraints")
|
||||
start := time.Now()
|
||||
reader, writer := io.Pipe()
|
||||
errs := make(chan error, 1)
|
||||
|
@@ -3,6 +3,7 @@ package mirror
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"sync"
|
||||
"time"
|
||||
@@ -104,6 +105,7 @@ func projections(
|
||||
config *ProjectionsConfig,
|
||||
masterKey string,
|
||||
) {
|
||||
logging.Info("starting to fill projections")
|
||||
start := time.Now()
|
||||
|
||||
client, err := database.Connect(config.Destination, false)
|
||||
@@ -255,8 +257,10 @@ func projections(
|
||||
go execProjections(ctx, instances, failedInstances, &wg)
|
||||
}
|
||||
|
||||
for _, instance := range queryInstanceIDs(ctx, client) {
|
||||
existingInstances := queryInstanceIDs(ctx, client)
|
||||
for i, instance := range existingInstances {
|
||||
instances <- instance
|
||||
logging.WithFields("id", instance, "index", fmt.Sprintf("%d/%d", i, len(existingInstances))).Info("instance queued for projection")
|
||||
}
|
||||
close(instances)
|
||||
wg.Wait()
|
||||
@@ -268,7 +272,7 @@ func projections(
|
||||
|
||||
func execProjections(ctx context.Context, instances <-chan string, failedInstances chan<- string, wg *sync.WaitGroup) {
|
||||
for instance := range instances {
|
||||
logging.WithFields("instance", instance).Info("start projections")
|
||||
logging.WithFields("instance", instance).Info("starting projections")
|
||||
ctx = internal_authz.WithInstanceID(ctx, instance)
|
||||
|
||||
err := projection.ProjectInstance(ctx)
|
||||
@@ -292,6 +296,13 @@ func execProjections(ctx context.Context, instances <-chan string, failedInstanc
|
||||
continue
|
||||
}
|
||||
|
||||
err = projection.ProjectInstanceFields(ctx)
|
||||
if err != nil {
|
||||
logging.WithFields("instance", instance).WithError(err).Info("trigger fields failed")
|
||||
failedInstances <- instance
|
||||
continue
|
||||
}
|
||||
|
||||
err = auth_handler.ProjectInstance(ctx)
|
||||
if err != nil {
|
||||
logging.WithFields("instance", instance).WithError(err).Info("trigger auth handler failed")
|
||||
@@ -311,7 +322,7 @@ func execProjections(ctx context.Context, instances <-chan string, failedInstanc
|
||||
wg.Done()
|
||||
}
|
||||
|
||||
// returns the instance configured by flag
|
||||
// queryInstanceIDs returns the instance configured by flag
|
||||
// or all instances which are not removed
|
||||
func queryInstanceIDs(ctx context.Context, source *database.DB) []string {
|
||||
if len(instanceIDs) > 0 {
|
||||
|
@@ -46,6 +46,7 @@ func copySystem(ctx context.Context, config *Migration) {
|
||||
}
|
||||
|
||||
func copyAssets(ctx context.Context, source, dest *database.DB) {
|
||||
logging.Info("starting to copy assets")
|
||||
start := time.Now()
|
||||
|
||||
sourceConn, err := source.Conn(ctx)
|
||||
@@ -70,7 +71,7 @@ func copyAssets(ctx context.Context, source, dest *database.DB) {
|
||||
logging.OnError(err).Fatal("unable to acquire dest connection")
|
||||
defer destConn.Close()
|
||||
|
||||
var eventCount int64
|
||||
var assetCount int64
|
||||
err = destConn.Raw(func(driverConn interface{}) error {
|
||||
conn := driverConn.(*stdlib.Conn).Conn()
|
||||
|
||||
@@ -82,16 +83,17 @@ func copyAssets(ctx context.Context, source, dest *database.DB) {
|
||||
}
|
||||
|
||||
tag, err := conn.PgConn().CopyFrom(ctx, r, "COPY system.assets (instance_id, asset_type, resource_owner, name, content_type, data, updated_at) FROM stdin")
|
||||
eventCount = tag.RowsAffected()
|
||||
assetCount = tag.RowsAffected()
|
||||
|
||||
return err
|
||||
})
|
||||
logging.OnError(err).Fatal("unable to copy assets to destination")
|
||||
logging.OnError(<-errs).Fatal("unable to copy assets from source")
|
||||
logging.WithFields("took", time.Since(start), "count", eventCount).Info("assets migrated")
|
||||
logging.WithFields("took", time.Since(start), "count", assetCount).Info("assets migrated")
|
||||
}
|
||||
|
||||
func copyEncryptionKeys(ctx context.Context, source, dest *database.DB) {
|
||||
logging.Info("starting to copy encryption keys")
|
||||
start := time.Now()
|
||||
|
||||
sourceConn, err := source.Conn(ctx)
|
||||
@@ -116,7 +118,7 @@ func copyEncryptionKeys(ctx context.Context, source, dest *database.DB) {
|
||||
logging.OnError(err).Fatal("unable to acquire dest connection")
|
||||
defer destConn.Close()
|
||||
|
||||
var eventCount int64
|
||||
var keyCount int64
|
||||
err = destConn.Raw(func(driverConn interface{}) error {
|
||||
conn := driverConn.(*stdlib.Conn).Conn()
|
||||
|
||||
@@ -128,11 +130,11 @@ func copyEncryptionKeys(ctx context.Context, source, dest *database.DB) {
|
||||
}
|
||||
|
||||
tag, err := conn.PgConn().CopyFrom(ctx, r, "COPY system.encryption_keys FROM stdin")
|
||||
eventCount = tag.RowsAffected()
|
||||
keyCount = tag.RowsAffected()
|
||||
|
||||
return err
|
||||
})
|
||||
logging.OnError(err).Fatal("unable to copy encryption keys to destination")
|
||||
logging.OnError(<-errs).Fatal("unable to copy encryption keys from source")
|
||||
logging.WithFields("took", time.Since(start), "count", eventCount).Info("encryption keys migrated")
|
||||
logging.WithFields("took", time.Since(start), "count", keyCount).Info("encryption keys migrated")
|
||||
}
|
||||
|
@@ -15,7 +15,6 @@ var (
|
||||
|
||||
type BackChannelLogoutNotificationStart struct {
|
||||
dbClient *database.DB
|
||||
esClient *eventstore.Eventstore
|
||||
}
|
||||
|
||||
func (mig *BackChannelLogoutNotificationStart) Execute(ctx context.Context, e eventstore.Event) error {
|
||||
|
@@ -4,29 +4,24 @@ import (
|
||||
"context"
|
||||
_ "embed"
|
||||
|
||||
"github.com/zitadel/zitadel/backend/v3/storage/database/dialect/postgres"
|
||||
"github.com/zitadel/zitadel/internal/database"
|
||||
"github.com/zitadel/zitadel/internal/eventstore"
|
||||
)
|
||||
|
||||
type TransactionalTables struct {
|
||||
var (
|
||||
//go:embed 54.sql
|
||||
instancePositionIndex string
|
||||
)
|
||||
|
||||
type InstancePositionIndex struct {
|
||||
dbClient *database.DB
|
||||
}
|
||||
|
||||
func (mig *TransactionalTables) Execute(ctx context.Context, _ eventstore.Event) error {
|
||||
config := &postgres.Config{Pool: mig.dbClient.Pool}
|
||||
pool, err := config.Connect(ctx)
|
||||
if err != nil {
|
||||
func (mig *InstancePositionIndex) Execute(ctx context.Context, _ eventstore.Event) error {
|
||||
_, err := mig.dbClient.ExecContext(ctx, instancePositionIndex)
|
||||
return err
|
||||
}
|
||||
|
||||
return pool.Migrate(ctx)
|
||||
}
|
||||
|
||||
func (mig *TransactionalTables) String() string {
|
||||
return "54_repeatable_transactional_tables"
|
||||
}
|
||||
|
||||
func (mig *TransactionalTables) Check(lastRun map[string]interface{}) bool {
|
||||
return true
|
||||
func (mig *InstancePositionIndex) String() string {
|
||||
return "54_instance_position_index_again"
|
||||
}
|
||||
|
1
cmd/setup/54.sql
Normal file
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
|
||||
s52IDPTemplate6LDAP2 *IDPTemplate6LDAP2
|
||||
s53InitPermittedOrgsFunction *InitPermittedOrgsFunction53
|
||||
s54InstancePositionIndex *InstancePositionIndex
|
||||
s55ExecutionHandlerStart *ExecutionHandlerStart
|
||||
s56IDPTemplate6SAMLFederatedLogout *IDPTemplate6SAMLFederatedLogout
|
||||
s57CreateResourceCounts *CreateResourceCounts
|
||||
s58ReplaceLoginNames3View *ReplaceLoginNames3View
|
||||
}
|
||||
|
||||
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.s36FillV2Milestones = &FillV3Milestones{dbClient: dbClient, eventstore: eventstoreClient}
|
||||
steps.s37Apps7OIDConfigsBackChannelLogoutURI = &Apps7OIDConfigsBackChannelLogoutURI{dbClient: dbClient}
|
||||
steps.s38BackChannelLogoutNotificationStart = &BackChannelLogoutNotificationStart{dbClient: dbClient, esClient: eventstoreClient}
|
||||
steps.s38BackChannelLogoutNotificationStart = &BackChannelLogoutNotificationStart{dbClient: dbClient}
|
||||
steps.s40InitPushFunc = &InitPushFunc{dbClient: dbClient}
|
||||
steps.s42Apps7OIDCConfigsLoginVersion = &Apps7OIDCConfigsLoginVersion{dbClient: dbClient}
|
||||
steps.s43CreateFieldsDomainIndex = &CreateFieldsDomainIndex{dbClient: dbClient}
|
||||
@@ -212,6 +212,11 @@ func Setup(ctx context.Context, config *Config, steps *Steps, masterKey string)
|
||||
steps.s51IDPTemplate6RootCA = &IDPTemplate6RootCA{dbClient: dbClient}
|
||||
steps.s52IDPTemplate6LDAP2 = &IDPTemplate6LDAP2{dbClient: dbClient}
|
||||
steps.s53InitPermittedOrgsFunction = &InitPermittedOrgsFunction53{dbClient: dbClient}
|
||||
steps.s54InstancePositionIndex = &InstancePositionIndex{dbClient: dbClient}
|
||||
steps.s55ExecutionHandlerStart = &ExecutionHandlerStart{dbClient: dbClient}
|
||||
steps.s56IDPTemplate6SAMLFederatedLogout = &IDPTemplate6SAMLFederatedLogout{dbClient: dbClient}
|
||||
steps.s57CreateResourceCounts = &CreateResourceCounts{dbClient: dbClient}
|
||||
steps.s58ReplaceLoginNames3View = &ReplaceLoginNames3View{dbClient: dbClient}
|
||||
|
||||
err = projection.Create(ctx, dbClient, eventstoreClient, config.Projections, nil, nil, nil)
|
||||
logging.OnError(err).Fatal("unable to start projections")
|
||||
@@ -254,6 +259,11 @@ func Setup(ctx context.Context, config *Config, steps *Steps, masterKey string)
|
||||
steps.s51IDPTemplate6RootCA,
|
||||
steps.s52IDPTemplate6LDAP2,
|
||||
steps.s53InitPermittedOrgsFunction,
|
||||
steps.s54InstancePositionIndex,
|
||||
steps.s55ExecutionHandlerStart,
|
||||
steps.s56IDPTemplate6SAMLFederatedLogout,
|
||||
steps.s57CreateResourceCounts,
|
||||
steps.s58ReplaceLoginNames3View,
|
||||
} {
|
||||
setupErr = executeMigration(ctx, eventstoreClient, step, "migration failed")
|
||||
if setupErr != nil {
|
||||
@@ -293,6 +303,7 @@ func Setup(ctx context.Context, config *Config, steps *Steps, masterKey string)
|
||||
client: dbClient,
|
||||
},
|
||||
}
|
||||
repeatableSteps = append(repeatableSteps, triggerSteps(dbClient)...)
|
||||
|
||||
for _, repeatableStep := range repeatableSteps {
|
||||
setupErr = executeMigration(ctx, eventstoreClient, repeatableStep, "unable to migrate repeatable step")
|
||||
|
125
cmd/setup/trigger_steps.go
Normal file
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_v2beta "github.com/zitadel/zitadel/internal/api/grpc/feature/v2beta"
|
||||
idp_v2 "github.com/zitadel/zitadel/internal/api/grpc/idp/v2"
|
||||
instance "github.com/zitadel/zitadel/internal/api/grpc/instance/v2beta"
|
||||
"github.com/zitadel/zitadel/internal/api/grpc/management"
|
||||
oidc_v2 "github.com/zitadel/zitadel/internal/api/grpc/oidc/v2"
|
||||
oidc_v2beta "github.com/zitadel/zitadel/internal/api/grpc/oidc/v2beta"
|
||||
org_v2 "github.com/zitadel/zitadel/internal/api/grpc/org/v2"
|
||||
org_v2beta "github.com/zitadel/zitadel/internal/api/grpc/org/v2beta"
|
||||
project_v2beta "github.com/zitadel/zitadel/internal/api/grpc/project/v2beta"
|
||||
"github.com/zitadel/zitadel/internal/api/grpc/resources/debug_events/debug_events"
|
||||
user_v3_alpha "github.com/zitadel/zitadel/internal/api/grpc/resources/user/v3alpha"
|
||||
userschema_v3_alpha "github.com/zitadel/zitadel/internal/api/grpc/resources/userschema/v3alpha"
|
||||
@@ -72,12 +74,14 @@ import (
|
||||
"github.com/zitadel/zitadel/internal/authz"
|
||||
authz_repo "github.com/zitadel/zitadel/internal/authz/repository"
|
||||
authz_es "github.com/zitadel/zitadel/internal/authz/repository/eventsourcing/eventstore"
|
||||
"github.com/zitadel/zitadel/internal/cache"
|
||||
"github.com/zitadel/zitadel/internal/cache/connector"
|
||||
"github.com/zitadel/zitadel/internal/command"
|
||||
"github.com/zitadel/zitadel/internal/crypto"
|
||||
cryptoDB "github.com/zitadel/zitadel/internal/crypto/database"
|
||||
"github.com/zitadel/zitadel/internal/database"
|
||||
"github.com/zitadel/zitadel/internal/domain"
|
||||
"github.com/zitadel/zitadel/internal/domain/federatedlogout"
|
||||
"github.com/zitadel/zitadel/internal/eventstore"
|
||||
old_es "github.com/zitadel/zitadel/internal/eventstore/repository/sql"
|
||||
new_es "github.com/zitadel/zitadel/internal/eventstore/v3"
|
||||
@@ -304,7 +308,7 @@ func startZitadel(ctx context.Context, config *Config, masterKey string, server
|
||||
|
||||
execution.Register(
|
||||
ctx,
|
||||
config.Projections.Customizations["executions"],
|
||||
config.Projections.Customizations["execution_handler"],
|
||||
config.Executions,
|
||||
queries,
|
||||
eventstoreClient.EventTypes(),
|
||||
@@ -442,6 +446,9 @@ func startAPIs(
|
||||
if err := apis.RegisterServer(ctx, system.CreateServer(commands, queries, config.Database.DatabaseName(), config.DefaultInstance, config.ExternalDomain), tlsConfig); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := apis.RegisterService(ctx, instance.CreateServer(commands, queries, config.Database.DatabaseName(), config.DefaultInstance, config.ExternalDomain)); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := apis.RegisterServer(ctx, admin.CreateServer(config.Database.DatabaseName(), commands, queries, keys.User, config.AuditLogRetention), tlsConfig); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -454,7 +461,7 @@ func startAPIs(
|
||||
if err := apis.RegisterService(ctx, user_v2beta.CreateServer(commands, queries, keys.User, keys.IDPConfig, idp.CallbackURL(), idp.SAMLRootURL(), assets.AssetAPI(), permissionCheck)); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := apis.RegisterService(ctx, user_v2.CreateServer(commands, queries, keys.User, keys.IDPConfig, idp.CallbackURL(), idp.SAMLRootURL(), assets.AssetAPI(), permissionCheck)); err != nil {
|
||||
if err := apis.RegisterService(ctx, user_v2.CreateServer(config.SystemDefaults, commands, queries, keys.User, keys.IDPConfig, idp.CallbackURL(), idp.SAMLRootURL(), assets.AssetAPI(), permissionCheck)); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := apis.RegisterService(ctx, session_v2beta.CreateServer(commands, queries, permissionCheck)); err != nil {
|
||||
@@ -463,7 +470,7 @@ func startAPIs(
|
||||
if err := apis.RegisterService(ctx, settings_v2beta.CreateServer(commands, queries)); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := apis.RegisterService(ctx, org_v2beta.CreateServer(commands, queries, permissionCheck)); err != nil {
|
||||
if err := apis.RegisterService(ctx, org_v2beta.CreateServer(config.SystemDefaults, commands, queries, permissionCheck)); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := apis.RegisterService(ctx, feature_v2beta.CreateServer(commands, queries)); err != nil {
|
||||
@@ -487,6 +494,9 @@ func startAPIs(
|
||||
if err := apis.RegisterService(ctx, action_v2_beta.CreateServer(config.SystemDefaults, commands, queries, domain.AllActionFunctions, apis.ListGrpcMethods, apis.ListGrpcServices)); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := apis.RegisterService(ctx, project_v2beta.CreateServer(config.SystemDefaults, commands, queries, permissionCheck)); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := apis.RegisterService(ctx, userschema_v3_alpha.CreateServer(config.SystemDefaults, commands, queries)); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -503,7 +513,12 @@ func startAPIs(
|
||||
assetsCache := middleware.AssetsCacheInterceptor(config.AssetStorage.Cache.MaxAge, config.AssetStorage.Cache.SharedMaxAge)
|
||||
apis.RegisterHandlerOnPrefix(assets.HandlerPrefix, assets.NewHandler(commands, verifier, config.SystemAuthZ, config.InternalAuthZ, id.SonyFlakeGenerator(), store, queries, middleware.CallDurationHandler, instanceInterceptor.Handler, assetsCache.Handler, limitingAccessInterceptor.Handle))
|
||||
|
||||
apis.RegisterHandlerOnPrefix(idp.HandlerPrefix, idp.NewHandler(commands, queries, keys.IDPConfig, instanceInterceptor.Handler))
|
||||
federatedLogoutsCache, err := connector.StartCache[federatedlogout.Index, string, *federatedlogout.FederatedLogout](ctx, []federatedlogout.Index{federatedlogout.IndexRequestID}, cache.PurposeFederatedLogout, cacheConnectors.Config.FederatedLogouts, cacheConnectors)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
apis.RegisterHandlerOnPrefix(idp.HandlerPrefix, idp.NewHandler(commands, queries, keys.IDPConfig, instanceInterceptor.Handler, federatedLogoutsCache))
|
||||
|
||||
userAgentInterceptor, err := middleware.NewUserAgentHandler(config.UserAgentCookie, keys.UserAgentCookieKey, id.SonyFlakeGenerator(), config.ExternalSecure, login.EndpointResources, login.EndpointExternalLoginCallbackFormPost, login.EndpointSAMLACS)
|
||||
if err != nil {
|
||||
@@ -524,7 +539,25 @@ func startAPIs(
|
||||
}
|
||||
apis.RegisterHandlerOnPrefix(openapi.HandlerPrefix, openAPIHandler)
|
||||
|
||||
oidcServer, err := oidc.NewServer(ctx, config.OIDC, login.DefaultLoggedOutPath, config.ExternalSecure, commands, queries, authRepo, keys.OIDC, keys.OIDCKey, eventstore, dbClient, userAgentInterceptor, instanceInterceptor.Handler, limitingAccessInterceptor, config.Log.Slog(), config.SystemDefaults.SecretHasher)
|
||||
oidcServer, err := oidc.NewServer(
|
||||
ctx,
|
||||
config.OIDC,
|
||||
login.DefaultLoggedOutPath,
|
||||
config.ExternalSecure,
|
||||
commands,
|
||||
queries,
|
||||
authRepo,
|
||||
keys.OIDC,
|
||||
keys.OIDCKey,
|
||||
eventstore,
|
||||
dbClient,
|
||||
userAgentInterceptor,
|
||||
instanceInterceptor.Handler,
|
||||
limitingAccessInterceptor,
|
||||
config.Log.Slog(),
|
||||
config.SystemDefaults.SecretHasher,
|
||||
federatedLogoutsCache,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to start oidc provider: %w", err)
|
||||
}
|
||||
@@ -573,6 +606,7 @@ func startAPIs(
|
||||
keys.IDPConfig,
|
||||
keys.CSRFCookieKey,
|
||||
cacheConnectors,
|
||||
federatedLogoutsCache,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to start login: %w", err)
|
||||
|
@@ -1,6 +1,8 @@
|
||||
package start
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
"github.com/zitadel/logging"
|
||||
@@ -29,14 +31,19 @@ Requirements:
|
||||
masterKey, err := key.MasterKey(cmd)
|
||||
logging.OnError(err).Panic("No master key provided")
|
||||
|
||||
initialise.InitAll(cmd.Context(), initialise.MustNewConfig(viper.GetViper()))
|
||||
initCtx, cancel := context.WithCancel(cmd.Context())
|
||||
initialise.InitAll(initCtx, initialise.MustNewConfig(viper.GetViper()))
|
||||
cancel()
|
||||
|
||||
err = setup.BindInitProjections(cmd)
|
||||
logging.OnError(err).Fatal("unable to bind \"init-projections\" flag")
|
||||
|
||||
setupConfig := setup.MustNewConfig(viper.GetViper())
|
||||
setupSteps := setup.MustNewSteps(viper.New())
|
||||
setup.Setup(cmd.Context(), setupConfig, setupSteps, masterKey)
|
||||
|
||||
setupCtx, cancel := context.WithCancel(cmd.Context())
|
||||
setup.Setup(setupCtx, setupConfig, setupSteps, masterKey)
|
||||
cancel()
|
||||
|
||||
startConfig := MustNewConfig(viper.GetViper())
|
||||
|
||||
|
@@ -1,6 +1,8 @@
|
||||
package start
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
"github.com/zitadel/logging"
|
||||
@@ -34,7 +36,10 @@ Requirements:
|
||||
|
||||
setupConfig := setup.MustNewConfig(viper.GetViper())
|
||||
setupSteps := setup.MustNewSteps(viper.New())
|
||||
setup.Setup(cmd.Context(), setupConfig, setupSteps, masterKey)
|
||||
|
||||
setupCtx, cancel := context.WithCancel(cmd.Context())
|
||||
setup.Setup(setupCtx, setupConfig, setupSteps, masterKey)
|
||||
cancel()
|
||||
|
||||
startConfig := MustNewConfig(viper.GetViper())
|
||||
|
||||
|
@@ -31,8 +31,8 @@
|
||||
"@fortawesome/fontawesome-svg-core": "^6.7.2",
|
||||
"@fortawesome/free-brands-svg-icons": "^6.7.2",
|
||||
"@ngx-translate/core": "^15.0.0",
|
||||
"@zitadel/client": "^1.0.7",
|
||||
"@zitadel/proto": "1.0.5-sha-4118a9d",
|
||||
"@zitadel/client": "1.2.0",
|
||||
"@zitadel/proto": "1.2.0",
|
||||
"angular-oauth2-oidc": "^15.0.1",
|
||||
"angularx-qrcode": "^16.0.2",
|
||||
"buffer": "^6.0.3",
|
||||
@@ -82,6 +82,7 @@
|
||||
"jasmine-spec-reporter": "~7.0.0",
|
||||
"karma": "^6.4.4",
|
||||
"karma-chrome-launcher": "^3.2.0",
|
||||
"karma-coverage": "^2.2.1",
|
||||
"karma-coverage-istanbul-reporter": "^3.0.3",
|
||||
"karma-jasmine": "^5.1.0",
|
||||
"karma-jasmine-html-reporter": "^2.1.0",
|
||||
|
@@ -1,16 +1,16 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { QuickstartComponent } from './quickstart.component';
|
||||
import { OIDCConfigurationComponent } from './oidc-configuration.component';
|
||||
|
||||
describe('QuickstartComponent', () => {
|
||||
let component: QuickstartComponent;
|
||||
let fixture: ComponentFixture<QuickstartComponent>;
|
||||
let component: OIDCConfigurationComponent;
|
||||
let fixture: ComponentFixture<OIDCConfigurationComponent>;
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [QuickstartComponent],
|
||||
declarations: [OIDCConfigurationComponent],
|
||||
});
|
||||
fixture = TestBed.createComponent(QuickstartComponent);
|
||||
fixture = TestBed.createComponent(OIDCConfigurationComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
@@ -24,8 +24,8 @@
|
||||
<th mat-header-cell *matHeaderCellDef>{{ 'ACTIONSTWO.EXECUTION.TABLE.TARGET' | translate }}</th>
|
||||
<td mat-cell *cnslCellDef="let row; dataSource: dataSource">
|
||||
<div class="target-key">
|
||||
<cnsl-project-role-chip *ngFor="let target of row.mappedTargets; trackBy: trackTarget" [roleName]="target.name"
|
||||
>{{ target.name }}
|
||||
<cnsl-project-role-chip *ngFor="let target of row.mappedTargets; trackBy: trackTarget" [roleName]="target.name">
|
||||
{{ target.name }}
|
||||
</cnsl-project-role-chip>
|
||||
</div>
|
||||
</td>
|
||||
|
@@ -55,13 +55,9 @@ export class ActionsTwoActionsTableComponent {
|
||||
}
|
||||
|
||||
return executions.map((execution) => {
|
||||
const mappedTargets = execution.targets.map((target) => {
|
||||
const targetType = targetsMap.get(target.type.value);
|
||||
if (!targetType) {
|
||||
throw new Error(`Target with id ${target.type.value} not found`);
|
||||
}
|
||||
return targetType;
|
||||
});
|
||||
const mappedTargets = execution.targets
|
||||
.map((target) => targetsMap.get(target))
|
||||
.filter((target): target is NonNullable<typeof target> => !!target);
|
||||
return { execution, mappedTargets };
|
||||
});
|
||||
});
|
||||
|
@@ -1,4 +1,7 @@
|
||||
<h2>{{ 'ACTIONSTWO.EXECUTION.TITLE' | translate }}</h2>
|
||||
<cnsl-info-section [type]="InfoSectionType.ALERT">
|
||||
{{ 'ACTIONSTWO.BETA_NOTE' | translate }}
|
||||
</cnsl-info-section>
|
||||
<p class="cnsl-secondary-text">{{ 'ACTIONSTWO.EXECUTION.DESCRIPTION' | translate }}</p>
|
||||
|
||||
<cnsl-actions-two-actions-table
|
||||
|
@@ -15,6 +15,8 @@ import { MatDialog } from '@angular/material/dialog';
|
||||
import { MessageInitShape } from '@bufbuild/protobuf';
|
||||
import { SetExecutionRequestSchema } from '@zitadel/proto/zitadel/action/v2beta/action_service_pb';
|
||||
import { Target } from '@zitadel/proto/zitadel/action/v2beta/target_pb';
|
||||
import { InfoSectionType } from '../../info-section/info-section.component';
|
||||
import { ExecutionFieldName } from '@zitadel/proto/zitadel/action/v2beta/query_pb';
|
||||
|
||||
@Component({
|
||||
selector: 'cnsl-actions-two-actions',
|
||||
@@ -41,7 +43,7 @@ export class ActionsTwoActionsComponent {
|
||||
return this.refresh$.pipe(
|
||||
startWith(true),
|
||||
switchMap(() => {
|
||||
return this.actionService.listExecutions({});
|
||||
return this.actionService.listExecutions({ sortingColumn: ExecutionFieldName.ID, pagination: { asc: true } });
|
||||
}),
|
||||
map(({ result }) => result.map(correctlyTypeExecution)),
|
||||
catchError((err) => {
|
||||
@@ -110,4 +112,6 @@ export class ActionsTwoActionsComponent {
|
||||
this.toast.showError(error);
|
||||
}
|
||||
}
|
||||
|
||||
protected readonly InfoSectionType = InfoSectionType;
|
||||
}
|
||||
|
@@ -84,7 +84,7 @@
|
||||
<div class="execution-condition-text">
|
||||
<span>{{ 'ACTIONSTWO.EXECUTION.DIALOG.CONDITION.ALL.TITLE' | translate }}</span>
|
||||
<span class="description cnsl-secondary-text">{{
|
||||
'ACTIONSTWO.EXECUTION.DIALOG.CONDITION.ALL.DESCRIPTION' | translate
|
||||
'ACTIONSTWO.EXECUTION.DIALOG.CONDITION.ALL_EVENTS' | translate
|
||||
}}</span>
|
||||
</div>
|
||||
</mat-checkbox>
|
||||
|
@@ -10,12 +10,7 @@ import {
|
||||
} from './actions-two-add-action-condition/actions-two-add-action-condition.component';
|
||||
import { ActionsTwoAddActionTargetComponent } from './actions-two-add-action-target/actions-two-add-action-target.component';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import {
|
||||
Condition,
|
||||
Execution,
|
||||
ExecutionTargetType,
|
||||
ExecutionTargetTypeSchema,
|
||||
} from '@zitadel/proto/zitadel/action/v2beta/execution_pb';
|
||||
import { Condition, Execution } from '@zitadel/proto/zitadel/action/v2beta/execution_pb';
|
||||
import { Subject } from 'rxjs';
|
||||
import { SetExecutionRequestSchema } from '@zitadel/proto/zitadel/action/v2beta/action_service_pb';
|
||||
|
||||
@@ -27,11 +22,8 @@ enum Page {
|
||||
|
||||
export type CorrectlyTypedCondition = Condition & { conditionType: Extract<Condition['conditionType'], { case: string }> };
|
||||
|
||||
type CorrectlyTypedTargets = { type: Extract<ExecutionTargetType['type'], { case: 'target' }> };
|
||||
|
||||
export type CorrectlyTypedExecution = Omit<Execution, 'targets' | 'condition'> & {
|
||||
export type CorrectlyTypedExecution = Omit<Execution, 'condition'> & {
|
||||
condition: CorrectlyTypedCondition;
|
||||
targets: CorrectlyTypedTargets[];
|
||||
};
|
||||
|
||||
export const correctlyTypeExecution = (execution: Execution): CorrectlyTypedExecution => {
|
||||
@@ -48,9 +40,6 @@ export const correctlyTypeExecution = (execution: Execution): CorrectlyTypedExec
|
||||
return {
|
||||
...execution,
|
||||
condition,
|
||||
targets: execution.targets
|
||||
.map(({ type }) => ({ type }))
|
||||
.filter((target): target is CorrectlyTypedTargets => target.type.case === 'target'),
|
||||
};
|
||||
};
|
||||
|
||||
@@ -81,7 +70,7 @@ export class ActionTwoAddActionDialogComponent {
|
||||
|
||||
protected readonly typeSignal = signal<ConditionType>('request');
|
||||
protected readonly conditionSignal = signal<MessageInitShape<typeof SetExecutionRequestSchema>['condition']>(undefined);
|
||||
protected readonly targetsSignal = signal<MessageInitShape<typeof ExecutionTargetTypeSchema>[]>([]);
|
||||
protected readonly targetsSignal = signal<string[]>([]);
|
||||
|
||||
protected readonly continueSubject = new Subject<void>();
|
||||
|
||||
@@ -112,7 +101,7 @@ export class ActionTwoAddActionDialogComponent {
|
||||
this.targetsSignal.set(data.execution.targets);
|
||||
this.typeSignal.set(data.execution.condition.conditionType.case);
|
||||
this.conditionSignal.set(data.execution.condition);
|
||||
this.preselectedTargetIds = data.execution.targets.map((target) => target.type.value);
|
||||
this.preselectedTargetIds = data.execution.targets;
|
||||
|
||||
this.page.set(Page.Target); // Set the initial page based on the provided execution data
|
||||
}
|
||||
|
@@ -1,4 +1,4 @@
|
||||
<form class="form-grid" [formGroup]="form()" (ngSubmit)="submit()">
|
||||
<form *ngIf="form$ | async as form" class="form-grid" [formGroup]="form" (ngSubmit)="submit()">
|
||||
<p class="target-description">{{ 'ACTIONSTWO.EXECUTION.DIALOG.TARGET.DESCRIPTION' | translate }}</p>
|
||||
|
||||
<cnsl-form-field class="full-width">
|
||||
@@ -8,9 +8,9 @@
|
||||
#trigger="matAutocompleteTrigger"
|
||||
#input
|
||||
type="text"
|
||||
[formControl]="form().controls.autocomplete"
|
||||
[formControl]="form.controls.autocomplete"
|
||||
[matAutocomplete]="autoservice"
|
||||
(keydown.enter)="handleEnter($event); input.blur(); trigger.closePanel()"
|
||||
(keydown.enter)="handleEnter($event, form); input.blur(); trigger.closePanel()"
|
||||
/>
|
||||
<mat-autocomplete #autoservice="matAutocomplete">
|
||||
<mat-option *ngIf="targets().state === 'loading'" class="is-loading">
|
||||
@@ -19,7 +19,7 @@
|
||||
<mat-option
|
||||
*ngFor="let target of selectableTargets(); trackBy: trackTarget"
|
||||
#option
|
||||
(click)="addTarget(target); option.deselect()"
|
||||
(click)="addTarget(target, form); option.deselect()"
|
||||
[value]="target.name"
|
||||
>
|
||||
{{ target.name }}
|
||||
@@ -27,7 +27,7 @@
|
||||
</mat-autocomplete>
|
||||
</cnsl-form-field>
|
||||
|
||||
<table mat-table cdkDropList (cdkDropListDropped)="drop($event)" [dataSource]="dataSource" [trackBy]="trackTarget">
|
||||
<table mat-table cdkDropList (cdkDropListDropped)="drop($event, form)" [dataSource]="dataSource" [trackBy]="trackTarget">
|
||||
<ng-container matColumnDef="order">
|
||||
<th mat-header-cell *matHeaderCellDef>Reorder</th>
|
||||
<td mat-cell *cnslCellDef="let row; let i = index; dataSource: dataSource">
|
||||
@@ -48,7 +48,7 @@
|
||||
actions
|
||||
matTooltip="{{ 'ACTIONS.REMOVE' | translate }}"
|
||||
color="warn"
|
||||
(click)="removeTarget(i)"
|
||||
(click)="removeTarget(i, form)"
|
||||
mat-icon-button
|
||||
>
|
||||
<i class="las la-trash"></i>
|
||||
@@ -65,7 +65,7 @@
|
||||
{{ 'ACTIONS.BACK' | translate }}
|
||||
</button>
|
||||
<span class="fill-space"></span>
|
||||
<button color="primary" [disabled]="form().invalid" mat-raised-button type="submit">
|
||||
<button color="primary" [disabled]="form.invalid" mat-raised-button type="submit">
|
||||
{{ 'ACTIONS.CONTINUE' | translate }}
|
||||
</button>
|
||||
</div>
|
||||
|
@@ -14,7 +14,7 @@ import { RouterModule } from '@angular/router';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { FormBuilder, FormControl, FormsModule, ReactiveFormsModule } from '@angular/forms';
|
||||
import { ReplaySubject, switchMap } from 'rxjs';
|
||||
import { ObservedValueOf, ReplaySubject, shareReplay, switchMap } from 'rxjs';
|
||||
import { MatRadioModule } from '@angular/material/radio';
|
||||
import { ActionService } from 'src/app/services/action.service';
|
||||
import { ToastService } from 'src/app/services/toast.service';
|
||||
@@ -23,14 +23,13 @@ import { InputModule } from 'src/app/modules/input/input.module';
|
||||
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
|
||||
import { MessageInitShape } from '@bufbuild/protobuf';
|
||||
import { Target } from '@zitadel/proto/zitadel/action/v2beta/target_pb';
|
||||
import { ExecutionTargetTypeSchema } from '@zitadel/proto/zitadel/action/v2beta/execution_pb';
|
||||
import { MatSelectModule } from '@angular/material/select';
|
||||
import { ActionConditionPipeModule } from 'src/app/pipes/action-condition-pipe/action-condition-pipe.module';
|
||||
import { MatTableDataSource, MatTableModule } from '@angular/material/table';
|
||||
import { startWith } from 'rxjs/operators';
|
||||
import { map, startWith } from 'rxjs/operators';
|
||||
import { TypeSafeCellDefModule } from 'src/app/directives/type-safe-cell-def/type-safe-cell-def.module';
|
||||
import { CdkDrag, CdkDragDrop, CdkDropList, moveItemInArray } from '@angular/cdk/drag-drop';
|
||||
import { toObservable, toSignal } from '@angular/core/rxjs-interop';
|
||||
import { toSignal } from '@angular/core/rxjs-interop';
|
||||
import { minArrayLengthValidator } from '../../../form-field/validators/validators';
|
||||
import { ProjectRoleChipModule } from '../../../project-role-chip/project-role-chip.module';
|
||||
import { MatTooltipModule } from '@angular/material/tooltip';
|
||||
@@ -72,11 +71,12 @@ export class ActionsTwoAddActionTargetComponent {
|
||||
}
|
||||
|
||||
@Output() public readonly back = new EventEmitter<void>();
|
||||
@Output() public readonly continue = new EventEmitter<MessageInitShape<typeof ExecutionTargetTypeSchema>[]>();
|
||||
@Output() public readonly continue = new EventEmitter<string[]>();
|
||||
|
||||
private readonly preselectedTargetIds$ = new ReplaySubject<string[]>(1);
|
||||
|
||||
protected readonly form: ReturnType<typeof this.buildForm>;
|
||||
protected readonly form$: ReturnType<typeof this.buildForm>;
|
||||
|
||||
protected readonly targets: ReturnType<typeof this.listTargets>;
|
||||
private readonly selectedTargetIds: Signal<string[]>;
|
||||
protected readonly selectableTargets: Signal<Target[]>;
|
||||
@@ -87,26 +87,27 @@ export class ActionsTwoAddActionTargetComponent {
|
||||
private readonly actionService: ActionService,
|
||||
private readonly toast: ToastService,
|
||||
) {
|
||||
this.form = this.buildForm();
|
||||
this.form$ = this.buildForm().pipe(shareReplay({ refCount: true, bufferSize: 1 }));
|
||||
this.targets = this.listTargets();
|
||||
|
||||
this.selectedTargetIds = this.getSelectedTargetIds(this.form);
|
||||
this.selectableTargets = this.getSelectableTargets(this.targets, this.selectedTargetIds);
|
||||
this.selectedTargetIds = this.getSelectedTargetIds(this.form$);
|
||||
this.selectableTargets = this.getSelectableTargets(this.targets, this.selectedTargetIds, this.form$);
|
||||
this.dataSource = this.getDataSource(this.targets, this.selectedTargetIds);
|
||||
}
|
||||
|
||||
private buildForm() {
|
||||
const preselectedTargetIds = toSignal(this.preselectedTargetIds$, { initialValue: [] as string[] });
|
||||
|
||||
return computed(() => {
|
||||
return this.preselectedTargetIds$.pipe(
|
||||
startWith([] as string[]),
|
||||
map((preselectedTargetIds) => {
|
||||
return this.fb.group({
|
||||
autocomplete: new FormControl('', { nonNullable: true }),
|
||||
selectedTargetIds: new FormControl(preselectedTargetIds(), {
|
||||
selectedTargetIds: new FormControl(preselectedTargetIds, {
|
||||
nonNullable: true,
|
||||
validators: [minArrayLengthValidator(1)],
|
||||
}),
|
||||
});
|
||||
});
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
private listTargets() {
|
||||
@@ -129,25 +130,35 @@ export class ActionsTwoAddActionTargetComponent {
|
||||
return computed(targetsSignal);
|
||||
}
|
||||
|
||||
private getSelectedTargetIds(form: typeof this.form) {
|
||||
const selectedTargetIds$ = toObservable(form).pipe(
|
||||
startWith(form()),
|
||||
switchMap((form) => {
|
||||
const { selectedTargetIds } = form.controls;
|
||||
private getSelectedTargetIds(form$: typeof this.form$) {
|
||||
const selectedTargetIds$ = form$.pipe(
|
||||
switchMap(({ controls: { selectedTargetIds } }) => {
|
||||
return selectedTargetIds.valueChanges.pipe(startWith(selectedTargetIds.value));
|
||||
}),
|
||||
);
|
||||
return toSignal(selectedTargetIds$, { requireSync: true });
|
||||
}
|
||||
|
||||
private getSelectableTargets(targets: typeof this.targets, selectedTargetIds: Signal<string[]>) {
|
||||
return computed(() => {
|
||||
private getSelectableTargets(targets: typeof this.targets, selectedTargetIds: Signal<string[]>, form$: typeof this.form$) {
|
||||
const autocomplete$ = form$.pipe(
|
||||
switchMap(({ controls: { autocomplete } }) => {
|
||||
return autocomplete.valueChanges.pipe(startWith(autocomplete.value));
|
||||
}),
|
||||
);
|
||||
const autocompleteSignal = toSignal(autocomplete$, { requireSync: true });
|
||||
|
||||
const unselectedTargets = computed(() => {
|
||||
const targetsCopy = new Map(targets().targets);
|
||||
for (const selectedTargetId of selectedTargetIds()) {
|
||||
targetsCopy.delete(selectedTargetId);
|
||||
}
|
||||
return Array.from(targetsCopy.values());
|
||||
});
|
||||
|
||||
return computed(() => {
|
||||
const autocomplete = autocompleteSignal().toLowerCase();
|
||||
return unselectedTargets().filter(({ name }) => name.toLowerCase().includes(autocomplete));
|
||||
});
|
||||
}
|
||||
|
||||
private getDataSource(targetsSignal: typeof this.targets, selectedTargetIdsSignal: Signal<string[]>) {
|
||||
@@ -178,46 +189,39 @@ export class ActionsTwoAddActionTargetComponent {
|
||||
return dataSource;
|
||||
}
|
||||
|
||||
protected addTarget(target: Target) {
|
||||
const { selectedTargetIds } = this.form().controls;
|
||||
protected addTarget(target: Target, form: ObservedValueOf<typeof this.form$>) {
|
||||
const { selectedTargetIds } = form.controls;
|
||||
selectedTargetIds.setValue([target.id, ...selectedTargetIds.value]);
|
||||
this.form().controls.autocomplete.setValue('');
|
||||
form.controls.autocomplete.setValue('');
|
||||
}
|
||||
|
||||
protected removeTarget(index: number) {
|
||||
const { selectedTargetIds } = this.form().controls;
|
||||
protected removeTarget(index: number, form: ObservedValueOf<typeof this.form$>) {
|
||||
const { selectedTargetIds } = form.controls;
|
||||
const data = [...selectedTargetIds.value];
|
||||
data.splice(index, 1);
|
||||
selectedTargetIds.setValue(data);
|
||||
}
|
||||
|
||||
protected drop(event: CdkDragDrop<undefined>) {
|
||||
const { selectedTargetIds } = this.form().controls;
|
||||
protected drop(event: CdkDragDrop<undefined>, form: ObservedValueOf<typeof this.form$>) {
|
||||
const { selectedTargetIds } = form.controls;
|
||||
|
||||
const data = [...selectedTargetIds.value];
|
||||
moveItemInArray(data, event.previousIndex, event.currentIndex);
|
||||
selectedTargetIds.setValue(data);
|
||||
}
|
||||
|
||||
protected handleEnter(event: Event) {
|
||||
protected handleEnter(event: Event, form: ObservedValueOf<typeof this.form$>) {
|
||||
const selectableTargets = this.selectableTargets();
|
||||
if (selectableTargets.length !== 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
event.preventDefault();
|
||||
this.addTarget(selectableTargets[0]);
|
||||
this.addTarget(selectableTargets[0], form);
|
||||
}
|
||||
|
||||
protected submit() {
|
||||
const selectedTargets = this.selectedTargetIds().map((value) => ({
|
||||
type: {
|
||||
case: 'target' as const,
|
||||
value,
|
||||
},
|
||||
}));
|
||||
|
||||
this.continue.emit(selectedTargets);
|
||||
this.continue.emit(this.selectedTargetIds());
|
||||
}
|
||||
|
||||
protected trackTarget(_: number, target: Target) {
|
||||
|
@@ -26,6 +26,9 @@
|
||||
{{ 'ACTIONSTWO.TARGET.CREATE.TYPES.' + type | translate }}
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
<span class="name-hint cnsl-secondary-text types-description">
|
||||
{{ 'ACTIONSTWO.TARGET.CREATE.TYPES_DESCRIPTION' | translate }}
|
||||
</span>
|
||||
</cnsl-form-field>
|
||||
|
||||
<cnsl-form-field class="full-width">
|
||||
|
@@ -23,3 +23,7 @@
|
||||
.name-hint {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.types-description {
|
||||
white-space: pre-line;
|
||||
}
|
||||
|
@@ -1,4 +1,7 @@
|
||||
<h2>{{ 'ACTIONSTWO.TARGET.TITLE' | translate }}</h2>
|
||||
<cnsl-info-section [type]="InfoSectionType.ALERT">
|
||||
{{ 'ACTIONSTWO.BETA_NOTE' | translate }}
|
||||
</cnsl-info-section>
|
||||
<p class="cnsl-secondary-text">{{ 'ACTIONSTWO.TARGET.DESCRIPTION' | translate }}</p>
|
||||
|
||||
<cnsl-actions-two-targets-table
|
||||
|
@@ -12,6 +12,7 @@ import {
|
||||
CreateTargetRequestSchema,
|
||||
UpdateTargetRequestSchema,
|
||||
} from '@zitadel/proto/zitadel/action/v2beta/action_service_pb';
|
||||
import { InfoSectionType } from '../../info-section/info-section.component';
|
||||
|
||||
@Component({
|
||||
selector: 'cnsl-actions-two-targets',
|
||||
@@ -76,7 +77,8 @@ export class ActionsTwoTargetsComponent {
|
||||
if ('id' in request) {
|
||||
await this.actionService.updateTarget(request);
|
||||
} else {
|
||||
await this.actionService.createTarget(request);
|
||||
const resp = await this.actionService.createTarget(request);
|
||||
console.log(`Your singing key: ${resp.signingKey}`);
|
||||
}
|
||||
|
||||
await new Promise((res) => setTimeout(res, 1000));
|
||||
@@ -86,4 +88,6 @@ export class ActionsTwoTargetsComponent {
|
||||
this.toast.showError(error);
|
||||
}
|
||||
}
|
||||
|
||||
protected readonly InfoSectionType = InfoSectionType;
|
||||
}
|
||||
|
@@ -20,6 +20,7 @@ import { ProjectRoleChipModule } from '../project-role-chip/project-role-chip.mo
|
||||
import { ActionConditionPipeModule } from 'src/app/pipes/action-condition-pipe/action-condition-pipe.module';
|
||||
import { MatSelectModule } from '@angular/material/select';
|
||||
import { MatIconModule } from '@angular/material/icon';
|
||||
import { InfoSectionModule } from '../info-section/info-section.module';
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
@@ -47,6 +48,7 @@ import { MatIconModule } from '@angular/material/icon';
|
||||
TypeSafeCellDefModule,
|
||||
ProjectRoleChipModule,
|
||||
ActionConditionPipeModule,
|
||||
InfoSectionModule,
|
||||
],
|
||||
exports: [ActionsTwoActionsComponent, ActionsTwoTargetsComponent, ActionsTwoTargetsTableComponent],
|
||||
})
|
||||
|
@@ -1,19 +1,19 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { OrgDomainsComponent } from './org-domains.component';
|
||||
import { DomainsComponent } from './domains.component';
|
||||
|
||||
describe('OrgDomainsComponent', () => {
|
||||
let component: OrgDomainsComponent;
|
||||
let fixture: ComponentFixture<OrgDomainsComponent>;
|
||||
let component: DomainsComponent;
|
||||
let fixture: ComponentFixture<DomainsComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [OrgDomainsComponent],
|
||||
declarations: [DomainsComponent],
|
||||
}).compileComponents();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(OrgDomainsComponent);
|
||||
fixture = TestBed.createComponent(DomainsComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
@@ -69,4 +69,19 @@
|
||||
</cnsl-form-field>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="id-query">
|
||||
<mat-checkbox id="id" class="cb" [checked]="getSubFilter(SubQuery.ID)" (change)="changeCheckbox(SubQuery.ID, $event)"
|
||||
>{{ 'FILTER.ORGID' | translate }}
|
||||
</mat-checkbox>
|
||||
<div class="subquery" *ngIf="getSubFilter(SubQuery.ID) as idq">
|
||||
<span class="nomethod cnsl-secondary-text">
|
||||
{{ 'FILTER.METHODS.1' | translate }}
|
||||
</span>
|
||||
|
||||
<cnsl-form-field class="filter-input-value">
|
||||
<input cnslInput name="value" [value]="idq.getId()" (change)="setValue(SubQuery.ID, idq, $event)" />
|
||||
</cnsl-form-field>
|
||||
</div>
|
||||
</div>
|
||||
</cnsl-filter>
|
||||
|
@@ -3,7 +3,14 @@ import { MatCheckboxChange } from '@angular/material/checkbox';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { take } from 'rxjs';
|
||||
import { TextQueryMethod } from 'src/app/proto/generated/zitadel/object_pb';
|
||||
import { OrgDomainQuery, OrgNameQuery, OrgQuery, OrgState, OrgStateQuery } from 'src/app/proto/generated/zitadel/org_pb';
|
||||
import {
|
||||
OrgDomainQuery,
|
||||
OrgNameQuery,
|
||||
OrgQuery,
|
||||
OrgState,
|
||||
OrgStateQuery,
|
||||
OrgIDQuery,
|
||||
} from 'src/app/proto/generated/zitadel/org_pb';
|
||||
import { UserNameQuery } from 'src/app/proto/generated/zitadel/user_pb';
|
||||
|
||||
import { FilterComponent } from '../filter/filter.component';
|
||||
@@ -12,6 +19,7 @@ enum SubQuery {
|
||||
NAME,
|
||||
STATE,
|
||||
DOMAIN,
|
||||
ID,
|
||||
}
|
||||
|
||||
@Component({
|
||||
@@ -61,6 +69,12 @@ export class FilterOrgComponent extends FilterComponent implements OnInit {
|
||||
orgDomainQuery.setMethod(filter.domainQuery.method);
|
||||
orgQuery.setDomainQuery(orgDomainQuery);
|
||||
return orgQuery;
|
||||
} else if (filter.idQuery) {
|
||||
const orgQuery = new OrgQuery();
|
||||
const orgIdQuery = new OrgIDQuery();
|
||||
orgIdQuery.setId(filter.idQuery.id);
|
||||
orgQuery.setIdQuery(orgIdQuery);
|
||||
return orgQuery;
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
@@ -100,6 +114,13 @@ export class FilterOrgComponent extends FilterComponent implements OnInit {
|
||||
odq.setDomainQuery(dq);
|
||||
this.searchQueries.push(odq);
|
||||
break;
|
||||
case SubQuery.ID:
|
||||
const idq = new OrgIDQuery();
|
||||
idq.setId('');
|
||||
const oidq = new OrgQuery();
|
||||
oidq.setIdQuery(idq);
|
||||
this.searchQueries.push(oidq);
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
switch (subquery) {
|
||||
@@ -121,6 +142,12 @@ export class FilterOrgComponent extends FilterComponent implements OnInit {
|
||||
this.searchQueries.splice(index_pdn, 1);
|
||||
}
|
||||
break;
|
||||
case SubQuery.ID:
|
||||
const index_id = this.searchQueries.findIndex((q) => (q as OrgQuery).toObject().idQuery !== undefined);
|
||||
if (index_id > -1) {
|
||||
this.searchQueries.splice(index_id, 1);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -140,6 +167,10 @@ export class FilterOrgComponent extends FilterComponent implements OnInit {
|
||||
(query as OrgDomainQuery).setDomain(value);
|
||||
this.filterChanged.emit(this.searchQueries ? this.searchQueries : []);
|
||||
break;
|
||||
case SubQuery.ID:
|
||||
(query as OrgIDQuery).setId(value);
|
||||
this.filterChanged.emit(this.searchQueries ? this.searchQueries : []);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -166,6 +197,13 @@ export class FilterOrgComponent extends FilterComponent implements OnInit {
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
case SubQuery.ID:
|
||||
const id = this.searchQueries.find((q) => (q as OrgQuery).toObject().idQuery !== undefined);
|
||||
if (id) {
|
||||
return (id as OrgQuery).getIdQuery();
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -1,19 +1,19 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { FilterUserComponent } from './filter-user.component';
|
||||
import { FilterProjectComponent } from './filter-project.component';
|
||||
|
||||
describe('FilterUserComponent', () => {
|
||||
let component: FilterUserComponent;
|
||||
let fixture: ComponentFixture<FilterUserComponent>;
|
||||
let component: FilterProjectComponent;
|
||||
let fixture: ComponentFixture<FilterProjectComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [FilterUserComponent],
|
||||
declarations: [FilterProjectComponent],
|
||||
}).compileComponents();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(FilterUserComponent);
|
||||
fixture = TestBed.createComponent(FilterProjectComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
@@ -2,7 +2,7 @@
|
||||
<div class="info-wrapper">
|
||||
<p class="info-row-title">{{ 'USER.PAGES.STATE' | translate }}</p>
|
||||
<p
|
||||
*ngIf="user && user.state !== undefined"
|
||||
*ngIf="user?.state"
|
||||
class="state"
|
||||
[ngClass]="{
|
||||
active: user.state === UserState.USER_STATE_ACTIVE,
|
||||
@@ -53,7 +53,7 @@
|
||||
<div class="info-wrapper">
|
||||
<p class="info-row-title">{{ 'IAM.PAGES.STATE' | translate }}</p>
|
||||
<p
|
||||
*ngIf="instance && instance.state !== undefined"
|
||||
*ngIf="instance?.state"
|
||||
class="state"
|
||||
[ngClass]="{
|
||||
active: instance.state === State.INSTANCE_STATE_RUNNING,
|
||||
@@ -66,17 +66,17 @@
|
||||
|
||||
<div class="info-wrapper">
|
||||
<p class="info-row-title">{{ 'RESOURCEID' | translate }}</p>
|
||||
<p *ngIf="instance && instance.id" class="info-row-desc">{{ instance.id }}</p>
|
||||
<p *ngIf="instance?.id" class="info-row-desc">{{ instance.id }}</p>
|
||||
</div>
|
||||
|
||||
<div class="info-wrapper">
|
||||
<p class="info-row-title">{{ 'NAME' | translate }}</p>
|
||||
<p *ngIf="instance && instance.name" class="info-row-desc">{{ instance.name }}</p>
|
||||
<p *ngIf="instance?.name" class="info-row-desc">{{ instance.name }}</p>
|
||||
</div>
|
||||
|
||||
<div class="info-wrapper">
|
||||
<p class="info-row-title">{{ 'VERSION' | translate }}</p>
|
||||
<p *ngIf="instance && instance.version" class="info-row-desc">{{ instance.version }}</p>
|
||||
<p *ngIf="instance?.version" class="info-row-desc">{{ instance.version }}</p>
|
||||
</div>
|
||||
|
||||
<div class="info-wrapper width">
|
||||
@@ -96,15 +96,15 @@
|
||||
|
||||
<div class="info-wrapper">
|
||||
<p class="info-row-title">{{ 'ORG.PAGES.CREATIONDATE' | translate }}</p>
|
||||
<p *ngIf="instance && instance.details && instance.details.creationDate" class="info-row-desc">
|
||||
{{ instance.details.creationDate | timestampToDate | localizedDate: 'dd. MMMM YYYY, HH:mm' }}
|
||||
<p *ngIf="instance?.details?.creationDate as creationDate" class="info-row-desc">
|
||||
{{ creationDate | timestampToDate | localizedDate: 'dd. MMMM YYYY, HH:mm' }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="info-wrapper">
|
||||
<p class="info-row-title">{{ 'ORG.PAGES.DATECHANGED' | translate }}</p>
|
||||
<p *ngIf="instance && instance.details && instance.details.changeDate" class="info-row-desc">
|
||||
{{ instance.details.changeDate | timestampToDate | localizedDate: 'dd. MMMM YYYY, HH:mm' }}
|
||||
<p *ngIf="instance?.details?.changeDate as changeDate" class="info-row-desc">
|
||||
{{ changeDate | timestampToDate | localizedDate: 'dd. MMMM YYYY, HH:mm' }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -113,7 +113,7 @@
|
||||
<div class="info-wrapper">
|
||||
<p class="info-row-title">{{ 'ORG.PAGES.STATE' | translate }}</p>
|
||||
<p
|
||||
*ngIf="org && org.state !== undefined"
|
||||
*ngIf="org?.state"
|
||||
class="state"
|
||||
[ngClass]="{ active: org.state === OrgState.ORG_STATE_ACTIVE, inactive: org.state === OrgState.ORG_STATE_INACTIVE }"
|
||||
>
|
||||
@@ -123,7 +123,7 @@
|
||||
|
||||
<div class="info-wrapper">
|
||||
<p class="info-row-title">{{ 'RESOURCEID' | translate }}</p>
|
||||
<p *ngIf="org && org.id" class="info-row-desc">{{ org.id }}</p>
|
||||
<p *ngIf="org?.id" class="info-row-desc">{{ org.id }}</p>
|
||||
</div>
|
||||
|
||||
<div class="info-wrapper width">
|
||||
@@ -143,15 +143,15 @@
|
||||
|
||||
<div class="info-wrapper">
|
||||
<p class="info-row-title">{{ 'ORG.PAGES.CREATIONDATE' | translate }}</p>
|
||||
<p *ngIf="org && org.details && org.details.creationDate" class="info-row-desc">
|
||||
{{ org.details.creationDate | timestampToDate | localizedDate: 'dd. MMMM YYYY, HH:mm' }}
|
||||
<p *ngIf="org?.details?.creationDate as creationDate" class="info-row-desc">
|
||||
{{ creationDate | timestampToDate | localizedDate: 'dd. MMMM YYYY, HH:mm' }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="info-wrapper">
|
||||
<p class="info-row-title">{{ 'ORG.PAGES.DATECHANGED' | translate }}</p>
|
||||
<p *ngIf="org && org.details && org.details.changeDate" class="info-row-desc">
|
||||
{{ org.details.changeDate | timestampToDate | localizedDate: 'dd. MMMM YYYY, HH:mm' }}
|
||||
<p *ngIf="org?.details?.changeDate as changeDate" class="info-row-desc">
|
||||
{{ changeDate | timestampToDate | localizedDate: 'dd. MMMM YYYY, HH:mm' }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -160,7 +160,7 @@
|
||||
<div class="info-wrapper">
|
||||
<p class="info-row-title">{{ 'PROJECT.STATE.TITLE' | translate }}</p>
|
||||
<p
|
||||
*ngIf="project && project.state !== undefined"
|
||||
*ngIf="project?.state"
|
||||
class="state"
|
||||
[ngClass]="{
|
||||
active: project.state === ProjectState.PROJECT_STATE_ACTIVE,
|
||||
@@ -173,20 +173,20 @@
|
||||
|
||||
<div class="info-wrapper">
|
||||
<p class="info-row-title">{{ 'RESOURCEID' | translate }}</p>
|
||||
<p *ngIf="project && project.id" class="info-row-desc">{{ project.id }}</p>
|
||||
<p *ngIf="project?.id" class="info-row-desc">{{ project.id }}</p>
|
||||
</div>
|
||||
|
||||
<div class="info-wrapper">
|
||||
<p class="info-row-title">{{ 'PROJECT.PAGES.CREATEDON' | translate }}</p>
|
||||
<p *ngIf="project && project.details && project.details.creationDate" class="info-row-desc">
|
||||
{{ project.details.creationDate | timestampToDate | localizedDate: 'dd. MMMM YYYY, HH:mm' }}
|
||||
<p *ngIf="project?.details?.creationDate as creationDate" class="info-row-desc">
|
||||
{{ creationDate | timestampToDate | localizedDate: 'dd. MMMM YYYY, HH:mm' }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="info-wrapper">
|
||||
<p class="info-row-title">{{ 'PROJECT.PAGES.LASTMODIFIED' | translate }}</p>
|
||||
<p *ngIf="project && project.details && project.details.changeDate" class="info-row-desc">
|
||||
{{ project.details.changeDate | timestampToDate | localizedDate: 'dd. MMMM YYYY, HH:mm' }}
|
||||
<p *ngIf="project?.details?.changeDate as changeDate" class="info-row-desc">
|
||||
{{ changeDate | timestampToDate | localizedDate: 'dd. MMMM YYYY, HH:mm' }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -195,7 +195,7 @@
|
||||
<div class="info-wrapper">
|
||||
<p class="info-row-title">{{ 'PROJECT.STATE.TITLE' | translate }}</p>
|
||||
<p
|
||||
*ngIf="grantedProject && grantedProject.state !== undefined"
|
||||
*ngIf="grantedProject?.state"
|
||||
class="state"
|
||||
[ngClass]="{
|
||||
active: grantedProject.state === ProjectGrantState.PROJECT_GRANT_STATE_ACTIVE,
|
||||
@@ -208,25 +208,25 @@
|
||||
|
||||
<div class="info-wrapper">
|
||||
<p class="info-row-title">{{ 'RESOURCEID' | translate }}</p>
|
||||
<p *ngIf="grantedProject && grantedProject.projectId" class="info-row-desc">{{ grantedProject.projectId }}</p>
|
||||
<p *ngIf="grantedProject?.projectId" class="info-row-desc">{{ grantedProject.projectId }}</p>
|
||||
</div>
|
||||
|
||||
<div class="info-wrapper">
|
||||
<p class="info-row-title">{{ 'PROJECT.GRANT.GRANTID' | translate }}</p>
|
||||
<p *ngIf="grantedProject && grantedProject.grantId" class="info-row-desc">{{ grantedProject.grantId }}</p>
|
||||
<p *ngIf="grantedProject?.grantId" class="info-row-desc">{{ grantedProject.grantId }}</p>
|
||||
</div>
|
||||
|
||||
<div class="info-wrapper">
|
||||
<p class="info-row-title">{{ 'PROJECT.PAGES.CREATEDON' | translate }}</p>
|
||||
<p *ngIf="grantedProject && grantedProject.details && grantedProject.details.creationDate" class="info-row-desc">
|
||||
{{ grantedProject.details.creationDate | timestampToDate | localizedDate: 'dd. MMMM YYYY, HH:mm' }}
|
||||
<p *ngIf="grantedProject?.details?.creationDate as creationDate" class="info-row-desc">
|
||||
{{ creationDate | timestampToDate | localizedDate: 'dd. MMMM YYYY, HH:mm' }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="info-wrapper">
|
||||
<p class="info-row-title">{{ 'PROJECT.PAGES.LASTMODIFIED' | translate }}</p>
|
||||
<p *ngIf="grantedProject && grantedProject.details && grantedProject.details.changeDate" class="info-row-desc">
|
||||
{{ grantedProject.details.changeDate | timestampToDate | localizedDate: 'dd. MMMM YYYY, HH:mm' }}
|
||||
<p *ngIf="grantedProject?.details?.changeDate as changeDate" class="info-row-desc">
|
||||
{{ changeDate | timestampToDate | localizedDate: 'dd. MMMM YYYY, HH:mm' }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -236,30 +236,43 @@
|
||||
<div class="info-wrapper">
|
||||
<p class="info-row-title">{{ 'APP.PAGES.STATE' | translate }}</p>
|
||||
<p
|
||||
*ngIf="app && app.state !== undefined"
|
||||
*ngIf="app?.state"
|
||||
class="state"
|
||||
[ngClass]="{ active: app.state === AppState.APP_STATE_ACTIVE, inactive: app.state === AppState.APP_STATE_INACTIVE }"
|
||||
>
|
||||
{{ 'APP.PAGES.DETAIL.STATE.' + app.state | translate }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="info-wrapper" *ngIf="app?.apiConfig?.authMethodType as authMethodType">
|
||||
<p class="info-row-title">{{ 'APP.AUTHMETHOD' | translate }}</p>
|
||||
<p class="info-row-desc">
|
||||
{{ 'APP.API.AUTHMETHOD.' + authMethodType | translate }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="info-wrapper" *ngIf="app?.oidcConfig?.authMethodType as authMethodType">
|
||||
<p class="info-row-title">{{ 'APP.AUTHMETHOD' | translate }}</p>
|
||||
<p class="info-row-desc">
|
||||
{{ 'APP.OIDC.AUTHMETHOD.' + authMethodType | translate }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="info-wrapper">
|
||||
<p class="info-row-title">{{ 'APP.PAGES.ID' | translate }}</p>
|
||||
<p *ngIf="app && app.id" class="info-row-desc">{{ app.id }}</p>
|
||||
<p *ngIf="app?.id" class="info-row-desc">{{ app.id }}</p>
|
||||
</div>
|
||||
|
||||
<div class="info-wrapper">
|
||||
<p class="info-row-title">{{ 'APP.PAGES.DATECREATED' | translate }}</p>
|
||||
<p *ngIf="app && app.details && app.details.creationDate" class="info-row-desc">
|
||||
{{ app.details.creationDate | timestampToDate | localizedDate: 'dd. MMMM YYYY, HH:mm' }}
|
||||
<p *ngIf="app?.details?.creationDate as creationDate" class="info-row-desc">
|
||||
{{ creationDate | timestampToDate | localizedDate: 'dd. MMMM YYYY, HH:mm' }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="info-wrapper">
|
||||
<p class="info-row-title">{{ 'APP.PAGES.DATECHANGED' | translate }}</p>
|
||||
<p *ngIf="app && app.details && app.details.changeDate" class="info-row-desc">
|
||||
{{ app.details.changeDate | timestampToDate | localizedDate: 'dd. MMMM YYYY, HH:mm' }}
|
||||
<p *ngIf="app?.details?.changeDate as changeDate" class="info-row-desc">
|
||||
{{ changeDate | timestampToDate | localizedDate: 'dd. MMMM YYYY, HH:mm' }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -267,27 +280,27 @@
|
||||
<p class="info-row-title">{{ 'APP.OIDC.INFO.CLIENTID' | translate }}</p>
|
||||
<div class="copy-row" *ngIf="app.oidcConfig?.clientId">
|
||||
<button
|
||||
*ngIf="app.oidcConfig && app.oidcConfig?.clientId"
|
||||
[disabled]="copied === app.oidcConfig.clientId"
|
||||
[matTooltip]="(copied !== app.oidcConfig.clientId ? 'ACTIONS.COPY' : 'ACTIONS.COPIED') | translate"
|
||||
*ngIf="app.oidcConfig?.clientId as clientId"
|
||||
[disabled]="copied === clientId"
|
||||
[matTooltip]="(copied !== clientId ? 'ACTIONS.COPY' : 'ACTIONS.COPIED') | translate"
|
||||
cnslCopyToClipboard
|
||||
[valueToCopy]="app.oidcConfig.clientId"
|
||||
[valueToCopy]="clientId"
|
||||
(copiedValue)="copied = $event"
|
||||
>
|
||||
{{ app.oidcConfig.clientId }}
|
||||
{{ clientId }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="copy-row" *ngIf="app.apiConfig?.clientId">
|
||||
<button
|
||||
*ngIf="app && app.apiConfig && app.apiConfig.clientId"
|
||||
[disabled]="copied === app.apiConfig.clientId"
|
||||
[matTooltip]="(copied !== app.apiConfig.clientId ? 'ACTIONS.COPY' : 'ACTIONS.COPIED') | translate"
|
||||
*ngIf="app.apiConfig?.clientId as clientId"
|
||||
[disabled]="copied === clientId"
|
||||
[matTooltip]="(copied !== clientId ? 'ACTIONS.COPY' : 'ACTIONS.COPIED') | translate"
|
||||
cnslCopyToClipboard
|
||||
[valueToCopy]="app.apiConfig.clientId"
|
||||
[valueToCopy]="clientId"
|
||||
(copiedValue)="copied = $event"
|
||||
>
|
||||
{{ app.apiConfig.clientId }}
|
||||
{{ clientId }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -304,22 +317,22 @@
|
||||
|
||||
<div class="info-wrapper">
|
||||
<p class="info-row-title">{{ 'IDP.DETAIL.DATECREATED' | translate }}</p>
|
||||
<p class="info-row-desc" *ngIf="idp && idp.details && idp.details.creationDate">
|
||||
{{ idp.details.creationDate | timestampToDate | localizedDate: 'dd. MMMM YYYY, HH:mm' }}
|
||||
<p class="info-row-desc" *ngIf="idp?.details?.creationDate as creationDate">
|
||||
{{ creationDate | timestampToDate | localizedDate: 'dd. MMMM YYYY, HH:mm' }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="info-wrapper">
|
||||
<p class="info-row-title">{{ 'IDP.DETAIL.DATECHANGED' | translate }}</p>
|
||||
<p class="info-row-desc" *ngIf="idp && idp.details && idp.details.changeDate">
|
||||
{{ idp.details.changeDate | timestampToDate | localizedDate: 'dd. MMMM YYYY, HH:mm' }}
|
||||
<p class="info-row-desc" *ngIf="idp?.details?.changeDate as changeDate">
|
||||
{{ changeDate | timestampToDate | localizedDate: 'dd. MMMM YYYY, HH:mm' }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="info-wrapper">
|
||||
<p class="info-row-title">{{ 'IDP.STATE' | translate }}</p>
|
||||
<p
|
||||
*ngIf="idp && idp.state !== undefined"
|
||||
*ngIf="idp?.state"
|
||||
class="state"
|
||||
[ngClass]="{ active: idp.state === IDPState.IDP_STATE_ACTIVE, inactive: idp.state === IDPState.IDP_STATE_INACTIVE }"
|
||||
>
|
||||
|
@@ -1,8 +1,49 @@
|
||||
import { Component, ElementRef, NgZone } from '@angular/core';
|
||||
import { TestBed, ComponentFixture } from '@angular/core/testing';
|
||||
import { InputDirective } from './input.directive';
|
||||
import { Platform } from '@angular/cdk/platform';
|
||||
import { NgControl, NgForm, FormGroupDirective } from '@angular/forms';
|
||||
import { ErrorStateMatcher } from '@angular/material/core';
|
||||
import { AutofillMonitor } from '@angular/cdk/text-field';
|
||||
import { MatFormField } from '@angular/material/form-field';
|
||||
import { MAT_INPUT_VALUE_ACCESSOR } from '@angular/material/input';
|
||||
import { of } from 'rxjs';
|
||||
import { By } from '@angular/platform-browser';
|
||||
|
||||
@Component({
|
||||
template: `<input appInputDirective />`,
|
||||
})
|
||||
class TestHostComponent {}
|
||||
|
||||
describe('InputDirective', () => {
|
||||
let fixture: ComponentFixture<TestHostComponent>;
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [InputDirective, TestHostComponent],
|
||||
providers: [
|
||||
{ provide: ElementRef, useValue: new ElementRef(document.createElement('input')) },
|
||||
Platform,
|
||||
{ provide: NgControl, useValue: null },
|
||||
{ provide: NgForm, useValue: null },
|
||||
{ provide: FormGroupDirective, useValue: null },
|
||||
ErrorStateMatcher,
|
||||
{ provide: MAT_INPUT_VALUE_ACCESSOR, useValue: null },
|
||||
{
|
||||
provide: AutofillMonitor,
|
||||
useValue: { monitor: () => of(), stopMonitoring: () => {} },
|
||||
},
|
||||
NgZone,
|
||||
{ provide: MatFormField, useValue: null },
|
||||
],
|
||||
}).compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(TestHostComponent);
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create an instance', () => {
|
||||
const directive = new InputDirective();
|
||||
expect(directive).toBeTruthy();
|
||||
const directiveEl = fixture.debugElement.query(By.directive(InputDirective));
|
||||
expect(directiveEl).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
@@ -1,19 +1,19 @@
|
||||
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
|
||||
|
||||
import { AvatarComponent } from './avatar.component';
|
||||
import { LabelComponent } from './label.component';
|
||||
|
||||
describe('AvatarComponent', () => {
|
||||
let component: AvatarComponent;
|
||||
let fixture: ComponentFixture<AvatarComponent>;
|
||||
let component: LabelComponent;
|
||||
let fixture: ComponentFixture<LabelComponent>;
|
||||
|
||||
beforeEach(waitForAsync(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [AvatarComponent],
|
||||
declarations: [LabelComponent],
|
||||
}).compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(AvatarComponent);
|
||||
fixture = TestBed.createComponent(LabelComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
@@ -4,7 +4,6 @@ import { Timestamp } from 'google-protobuf/google/protobuf/timestamp_pb';
|
||||
import { ToastService } from 'src/app/services/toast.service';
|
||||
import { Metadata as MetadataV2 } from '@zitadel/proto/zitadel/metadata_pb';
|
||||
import { Metadata } from 'src/app/proto/generated/zitadel/metadata_pb';
|
||||
import { Buffer } from 'buffer';
|
||||
|
||||
export type MetadataDialogData = {
|
||||
metadata: (Metadata.AsObject | MetadataV2)[];
|
||||
@@ -26,9 +25,10 @@ export class MetadataDialogComponent {
|
||||
public dialogRef: MatDialogRef<MetadataDialogComponent>,
|
||||
@Inject(MAT_DIALOG_DATA) public data: MetadataDialogData,
|
||||
) {
|
||||
const decoder = new TextDecoder();
|
||||
this.metadata = data.metadata.map(({ key, value }) => ({
|
||||
key,
|
||||
value: typeof value === 'string' ? value : Buffer.from(value as unknown as string, 'base64').toString('utf8'),
|
||||
value: typeof value === 'string' ? value : decoder.decode(value),
|
||||
}));
|
||||
}
|
||||
|
||||
|
@@ -5,7 +5,6 @@ import { Observable, ReplaySubject } from 'rxjs';
|
||||
import { Metadata as MetadataV2 } from '@zitadel/proto/zitadel/metadata_pb';
|
||||
import { map, startWith } from 'rxjs/operators';
|
||||
import { Metadata } from 'src/app/proto/generated/zitadel/metadata_pb';
|
||||
import { Buffer } from 'buffer';
|
||||
|
||||
type StringMetadata = {
|
||||
key: string;
|
||||
@@ -37,12 +36,13 @@ export class MetadataComponent implements OnInit {
|
||||
|
||||
ngOnInit() {
|
||||
this.dataSource$ = this.metadata$.pipe(
|
||||
map((metadata) =>
|
||||
metadata.map(({ key, value }) => ({
|
||||
map((metadata) => {
|
||||
const decoder = new TextDecoder();
|
||||
return metadata.map(({ key, value }) => ({
|
||||
key,
|
||||
value: Buffer.from(value as any as string, 'base64').toString('utf-8'),
|
||||
})),
|
||||
),
|
||||
value: typeof value === 'string' ? value : decoder.decode(value),
|
||||
}));
|
||||
}),
|
||||
startWith([] as StringMetadata[]),
|
||||
map((metadata) => new MatTableDataSource(metadata)),
|
||||
);
|
||||
|
@@ -2,14 +2,12 @@ import { Component, Injector, Input, OnDestroy, OnInit, Type } from '@angular/co
|
||||
import { AbstractControl, UntypedFormBuilder, UntypedFormGroup } from '@angular/forms';
|
||||
import { MatDialog } from '@angular/material/dialog';
|
||||
import { Duration } from 'google-protobuf/google/protobuf/duration_pb';
|
||||
import { firstValueFrom, forkJoin, from, Observable, of, Subject, take } from 'rxjs';
|
||||
import { forkJoin, from, of, Subject, take } from 'rxjs';
|
||||
import {
|
||||
GetLoginPolicyResponse as AdminGetLoginPolicyResponse,
|
||||
UpdateLoginPolicyRequest,
|
||||
UpdateLoginPolicyResponse,
|
||||
} from 'src/app/proto/generated/zitadel/admin_pb';
|
||||
import {
|
||||
AddCustomLoginPolicyRequest,
|
||||
GetLoginPolicyResponse as MgmtGetLoginPolicyResponse,
|
||||
UpdateCustomLoginPolicyRequest,
|
||||
} from 'src/app/proto/generated/zitadel/management_pb';
|
||||
@@ -24,8 +22,7 @@ import { InfoSectionType } from '../../info-section/info-section.component';
|
||||
import { WarnDialogComponent } from '../../warn-dialog/warn-dialog.component';
|
||||
import { PolicyComponentServiceType } from '../policy-component-types.enum';
|
||||
import { LoginMethodComponentType } from './factor-table/factor-table.component';
|
||||
import { catchError, map, takeUntil } from 'rxjs/operators';
|
||||
import { error } from 'console';
|
||||
import { map, takeUntil } from 'rxjs/operators';
|
||||
import { LoginPolicyService } from '../../../services/login-policy.service';
|
||||
|
||||
const minValueValidator = (minValue: number) => (control: AbstractControl) => {
|
||||
|
@@ -1,19 +1,19 @@
|
||||
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
|
||||
|
||||
import { LoginPolicyComponent } from './login-policy.component';
|
||||
import { MessageTextsComponent } from './message-texts.component';
|
||||
|
||||
describe('LoginPolicyComponent', () => {
|
||||
let component: LoginPolicyComponent;
|
||||
let fixture: ComponentFixture<LoginPolicyComponent>;
|
||||
let component: MessageTextsComponent;
|
||||
let fixture: ComponentFixture<MessageTextsComponent>;
|
||||
|
||||
beforeEach(waitForAsync(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [LoginPolicyComponent],
|
||||
declarations: [MessageTextsComponent],
|
||||
}).compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(LoginPolicyComponent);
|
||||
fixture = TestBed.createComponent(MessageTextsComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
@@ -1,19 +1,19 @@
|
||||
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
|
||||
|
||||
import { PasswordComplexityPolicyComponent } from './password-complexity-policy.component';
|
||||
import { NotificationPolicyComponent } from './notification-policy.component';
|
||||
|
||||
describe('PasswordComplexityPolicyComponent', () => {
|
||||
let component: PasswordComplexityPolicyComponent;
|
||||
let fixture: ComponentFixture<PasswordComplexityPolicyComponent>;
|
||||
let component: NotificationPolicyComponent;
|
||||
let fixture: ComponentFixture<NotificationPolicyComponent>;
|
||||
|
||||
beforeEach(waitForAsync(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [PasswordComplexityPolicyComponent],
|
||||
declarations: [NotificationPolicyComponent],
|
||||
}).compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(PasswordComplexityPolicyComponent);
|
||||
fixture = TestBed.createComponent(NotificationPolicyComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
@@ -1,19 +1,19 @@
|
||||
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
|
||||
|
||||
import { PasswordDialogComponent } from './password-dialog-sms-provider.component';
|
||||
import { PasswordDialogSMSProviderComponent } from './password-dialog-sms-provider.component';
|
||||
|
||||
describe('PasswordDialogComponent', () => {
|
||||
let component: PasswordDialogComponent;
|
||||
let fixture: ComponentFixture<PasswordDialogComponent>;
|
||||
let component: PasswordDialogSMSProviderComponent;
|
||||
let fixture: ComponentFixture<PasswordDialogSMSProviderComponent>;
|
||||
|
||||
beforeEach(waitForAsync(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [PasswordDialogComponent],
|
||||
declarations: [PasswordDialogSMSProviderComponent],
|
||||
}).compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(PasswordDialogComponent);
|
||||
fixture = TestBed.createComponent(PasswordDialogSMSProviderComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
@@ -1,19 +1,19 @@
|
||||
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
|
||||
|
||||
import { ProviderOAuthComponent } from './provider-oauth.component';
|
||||
import { ProviderGithubESComponent } from './provider-github-es.component';
|
||||
|
||||
describe('ProviderOAuthComponent', () => {
|
||||
let component: ProviderOAuthComponent;
|
||||
let fixture: ComponentFixture<ProviderOAuthComponent>;
|
||||
let component: ProviderGithubESComponent;
|
||||
let fixture: ComponentFixture<ProviderGithubESComponent>;
|
||||
|
||||
beforeEach(waitForAsync(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ProviderOAuthComponent],
|
||||
declarations: [ProviderGithubESComponent],
|
||||
}).compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(ProviderOAuthComponent);
|
||||
fixture = TestBed.createComponent(ProviderGithubESComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
@@ -1,19 +1,19 @@
|
||||
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
|
||||
|
||||
import { ProviderGoogleComponent } from './provider-google.component';
|
||||
import { ProviderGitlabSelfHostedComponent } from './provider-gitlab-self-hosted.component';
|
||||
|
||||
describe('ProviderGoogleComponent', () => {
|
||||
let component: ProviderGoogleComponent;
|
||||
let fixture: ComponentFixture<ProviderGoogleComponent>;
|
||||
let component: ProviderGitlabSelfHostedComponent;
|
||||
let fixture: ComponentFixture<ProviderGitlabSelfHostedComponent>;
|
||||
|
||||
beforeEach(waitForAsync(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ProviderGoogleComponent],
|
||||
declarations: [ProviderGitlabSelfHostedComponent],
|
||||
}).compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(ProviderGoogleComponent);
|
||||
fixture = TestBed.createComponent(ProviderGitlabSelfHostedComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
@@ -1,19 +1,19 @@
|
||||
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
|
||||
|
||||
import { ProviderGoogleComponent } from './provider-google.component';
|
||||
import { ProviderGitlabComponent } from './provider-gitlab.component';
|
||||
|
||||
describe('ProviderGoogleComponent', () => {
|
||||
let component: ProviderGoogleComponent;
|
||||
let fixture: ComponentFixture<ProviderGoogleComponent>;
|
||||
let component: ProviderGitlabComponent;
|
||||
let fixture: ComponentFixture<ProviderGitlabComponent>;
|
||||
|
||||
beforeEach(waitForAsync(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ProviderGoogleComponent],
|
||||
declarations: [ProviderGitlabComponent],
|
||||
}).compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(ProviderGoogleComponent);
|
||||
fixture = TestBed.createComponent(ProviderGitlabComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
@@ -98,14 +98,13 @@
|
||||
<p class="checkbox-desc">{{ 'IDP.ISIDTOKENMAPPING_DESC' | translate }}</p>
|
||||
<mat-checkbox formControlName="isIdTokenMapping">{{ 'IDP.ISIDTOKENMAPPING' | translate }}</mat-checkbox>
|
||||
</div>
|
||||
</cnsl-info-section>
|
||||
|
||||
<cnsl-info-section>
|
||||
<div>
|
||||
<p class="checkbox-desc">{{ 'IDP.USEPKCE_DESC' | translate }}</p>
|
||||
<mat-checkbox formControlName="usePkce">{{ 'IDP.USEPKCE' | translate }}</mat-checkbox>
|
||||
</div>
|
||||
</cnsl-info-section>
|
||||
</cnsl-info-section>
|
||||
</div>
|
||||
|
||||
<cnsl-provider-options
|
||||
|
@@ -82,7 +82,7 @@
|
||||
|
||||
<cnsl-info-section>
|
||||
<div>
|
||||
<p class="transient-info-desc">{{ 'IDP.SAML.TRANSIENTMAPPINGATTRIBUTENAME_DESC' | translate }}</p>
|
||||
<p class="option-desc">{{ 'IDP.SAML.TRANSIENTMAPPINGATTRIBUTENAME_DESC' | translate }}</p>
|
||||
</div>
|
||||
|
||||
<cnsl-form-field class="formfield">
|
||||
@@ -90,6 +90,15 @@
|
||||
<input cnslInput formControlName="transientMappingAttributeName" />
|
||||
</cnsl-form-field>
|
||||
</cnsl-info-section>
|
||||
|
||||
<cnsl-info-section>
|
||||
<div>
|
||||
<p class="option-desc">{{ 'IDP.FEDERATEDLOGOUTENABLED_DESC' | translate }}</p>
|
||||
<mat-checkbox formControlName="federatedLogoutEnabled">{{
|
||||
'IDP.FEDERATEDLOGOUTENABLED' | translate
|
||||
}}</mat-checkbox>
|
||||
</div>
|
||||
</cnsl-info-section>
|
||||
</div>
|
||||
|
||||
<cnsl-provider-options
|
||||
|
@@ -4,9 +4,9 @@
|
||||
|
||||
.transient-info {
|
||||
max-width: 500px;
|
||||
}
|
||||
|
||||
.transient-info-desc {
|
||||
.option-desc {
|
||||
margin-top: 0;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
}
|
||||
|
@@ -127,6 +127,7 @@ export class ProviderSamlSpComponent {
|
||||
withSignedRequest: new UntypedFormControl(true, [requiredValidator]),
|
||||
nameIdFormat: new UntypedFormControl(SAMLNameIDFormat.SAML_NAME_ID_FORMAT_PERSISTENT, []),
|
||||
transientMappingAttributeName: new UntypedFormControl('', []),
|
||||
federatedLogoutEnabled: new UntypedFormControl(false, []),
|
||||
},
|
||||
atLeastOneIsFilled('metadataXml', 'metadataUrl'),
|
||||
);
|
||||
@@ -210,6 +211,7 @@ export class ProviderSamlSpComponent {
|
||||
// @ts-ignore
|
||||
req.setNameIdFormat(SAMLNameIDFormat[this.nameIDFormat?.value]);
|
||||
req.setTransientMappingAttributeName(this.transientMapping?.value);
|
||||
req.setFederatedLogoutEnabled(this.federatedLogoutEnabled?.value);
|
||||
req.setProviderOptions(this.options);
|
||||
|
||||
this.loading = true;
|
||||
@@ -250,6 +252,7 @@ export class ProviderSamlSpComponent {
|
||||
req.setNameIdFormat(SAMLNameIDFormat[this.nameIDFormat.value]);
|
||||
}
|
||||
req.setTransientMappingAttributeName(this.transientMapping?.value);
|
||||
req.setFederatedLogoutEnabled(this.federatedLogoutEnabled?.value);
|
||||
this.loading = true;
|
||||
this.service
|
||||
.addSAMLProvider(req)
|
||||
@@ -335,4 +338,8 @@ export class ProviderSamlSpComponent {
|
||||
private get transientMapping(): AbstractControl | null {
|
||||
return this.form.get('transientMappingAttributeName');
|
||||
}
|
||||
|
||||
private get federatedLogoutEnabled(): AbstractControl | null {
|
||||
return this.form.get('federatedLogoutEnabled');
|
||||
}
|
||||
}
|
||||
|
@@ -228,9 +228,9 @@ export const ACTIONS: SidenavSetting = {
|
||||
i18nKey: 'SETTINGS.LIST.ACTIONS',
|
||||
groupI18nKey: 'SETTINGS.GROUPS.ACTIONS',
|
||||
requiredRoles: {
|
||||
// todo: figure out roles
|
||||
[PolicyComponentServiceType.ADMIN]: ['iam.policy.read'],
|
||||
[PolicyComponentServiceType.ADMIN]: ['action.execution.write', 'action.target.write'],
|
||||
},
|
||||
beta: true,
|
||||
};
|
||||
|
||||
export const ACTIONS_TARGETS: SidenavSetting = {
|
||||
@@ -238,7 +238,7 @@ export const ACTIONS_TARGETS: SidenavSetting = {
|
||||
i18nKey: 'SETTINGS.LIST.TARGETS',
|
||||
groupI18nKey: 'SETTINGS.GROUPS.ACTIONS',
|
||||
requiredRoles: {
|
||||
// todo: figure out roles
|
||||
[PolicyComponentServiceType.ADMIN]: ['iam.policy.read'],
|
||||
[PolicyComponentServiceType.ADMIN]: ['action.execution.write', 'action.target.write'],
|
||||
},
|
||||
beta: true,
|
||||
};
|
||||
|
@@ -1,19 +1,19 @@
|
||||
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
|
||||
|
||||
import { ShowKeyDialogComponent } from './show-key-dialog.component';
|
||||
import { ShowTokenDialogComponent } from './show-token-dialog.component';
|
||||
|
||||
describe('ShowKeyDialogComponent', () => {
|
||||
let component: ShowKeyDialogComponent;
|
||||
let fixture: ComponentFixture<ShowKeyDialogComponent>;
|
||||
let component: ShowTokenDialogComponent;
|
||||
let fixture: ComponentFixture<ShowTokenDialogComponent>;
|
||||
|
||||
beforeEach(waitForAsync(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ShowKeyDialogComponent],
|
||||
declarations: [ShowTokenDialogComponent],
|
||||
}).compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(ShowKeyDialogComponent);
|
||||
fixture = TestBed.createComponent(ShowTokenDialogComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
@@ -28,6 +28,7 @@
|
||||
[attr.data-e2e]="'sidenav-element-' + setting.id"
|
||||
>
|
||||
<span>{{ setting.i18nKey | translate }}</span>
|
||||
<span class="state" *ngIf="setting?.beta">{{ 'SETTINGS.BETA' | translate }}</span>
|
||||
<mat-icon *ngIf="setting.showWarn" class="warn-icon" svgIcon="mdi_shield_alert"></mat-icon>
|
||||
</button>
|
||||
</ng-container>
|
||||
|
@@ -90,6 +90,10 @@
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.state {
|
||||
margin-left: 0.5rem;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
span {
|
||||
opacity: 1;
|
||||
|
@@ -11,6 +11,7 @@ export interface SidenavSetting {
|
||||
[PolicyComponentServiceType.ADMIN]?: string[];
|
||||
};
|
||||
showWarn?: boolean;
|
||||
beta?: boolean;
|
||||
}
|
||||
|
||||
@Component({
|
||||
|
@@ -1,19 +1,19 @@
|
||||
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
|
||||
|
||||
import { IdpTableComponent } from './smtp-table.component';
|
||||
import { SMTPTableComponent } from './smtp-table.component';
|
||||
|
||||
describe('UserTableComponent', () => {
|
||||
let component: IdpTableComponent;
|
||||
let fixture: ComponentFixture<IdpTableComponent>;
|
||||
let component: SMTPTableComponent;
|
||||
let fixture: ComponentFixture<SMTPTableComponent>;
|
||||
|
||||
beforeEach(waitForAsync(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [IdpTableComponent],
|
||||
declarations: [SMTPTableComponent],
|
||||
}).compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(IdpTableComponent);
|
||||
fixture = TestBed.createComponent(SMTPTableComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user