mirror of
https://github.com/zitadel/zitadel.git
synced 2025-08-11 20:17:32 +00:00
Merge branch 'main' into clean-transactional-propsal
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
|
||||
|
||||
|
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 {
|
||||
|
27
cmd/setup/54.go
Normal file
27
cmd/setup/54.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 54.sql
|
||||
instancePositionIndex string
|
||||
)
|
||||
|
||||
type InstancePositionIndex struct {
|
||||
dbClient *database.DB
|
||||
}
|
||||
|
||||
func (mig *InstancePositionIndex) Execute(ctx context.Context, _ eventstore.Event) error {
|
||||
_, err := mig.dbClient.ExecContext(ctx, instancePositionIndex)
|
||||
return err
|
||||
}
|
||||
|
||||
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)
|
@@ -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)
|
||||
dbClient: 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();
|
||||
});
|
||||
|
@@ -6,6 +6,9 @@
|
||||
<mat-icon class="icon">info_outline</mat-icon>
|
||||
</a>
|
||||
</div>
|
||||
<cnsl-info-section [type]="InfoSectionType.ALERT">
|
||||
{{ 'DESCRIPTIONS.ACTIONS.ACTIONSTWO_NOTE' | translate }}
|
||||
</cnsl-info-section>
|
||||
<p class="desc cnsl-secondary-text">{{ 'DESCRIPTIONS.ACTIONS.DESCRIPTION' | translate }}</p>
|
||||
|
||||
<cnsl-info-section class="max-actions" *ngIf="maxActions"
|
||||
|
@@ -1,8 +1,7 @@
|
||||
import { CdkDragDrop, moveItemInArray } from '@angular/cdk/drag-drop';
|
||||
import { Component, OnDestroy } from '@angular/core';
|
||||
import { Component, DestroyRef } from '@angular/core';
|
||||
import { UntypedFormControl } from '@angular/forms';
|
||||
import { MatDialog } from '@angular/material/dialog';
|
||||
import { Subject, takeUntil } from 'rxjs';
|
||||
import { ActionKeysType } from 'src/app/modules/action-keys/action-keys.component';
|
||||
import { InfoSectionType } from 'src/app/modules/info-section/info-section.component';
|
||||
import { WarnDialogComponent } from 'src/app/modules/warn-dialog/warn-dialog.component';
|
||||
@@ -13,31 +12,32 @@ import { ManagementService } from 'src/app/services/mgmt.service';
|
||||
import { ToastService } from 'src/app/services/toast.service';
|
||||
|
||||
import { AddFlowDialogComponent } from './add-flow-dialog/add-flow-dialog.component';
|
||||
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
||||
|
||||
@Component({
|
||||
selector: 'cnsl-actions',
|
||||
templateUrl: './actions.component.html',
|
||||
styleUrls: ['./actions.component.scss'],
|
||||
})
|
||||
export class ActionsComponent implements OnDestroy {
|
||||
public flow!: Flow.AsObject;
|
||||
export class ActionsComponent {
|
||||
protected flow!: Flow.AsObject;
|
||||
|
||||
public typeControl: UntypedFormControl = new UntypedFormControl();
|
||||
protected typeControl: UntypedFormControl = new UntypedFormControl();
|
||||
|
||||
public typesForSelection: FlowType.AsObject[] = [];
|
||||
protected typesForSelection: FlowType.AsObject[] = [];
|
||||
|
||||
public selection: Action.AsObject[] = [];
|
||||
public InfoSectionType: any = InfoSectionType;
|
||||
public ActionKeysType: any = ActionKeysType;
|
||||
protected selection: Action.AsObject[] = [];
|
||||
protected InfoSectionType = InfoSectionType;
|
||||
protected ActionKeysType = ActionKeysType;
|
||||
|
||||
public maxActions: number | null = null;
|
||||
public ActionState: any = ActionState;
|
||||
private destroy$: Subject<void> = new Subject();
|
||||
protected maxActions: number | null = null;
|
||||
protected ActionState = ActionState;
|
||||
constructor(
|
||||
private mgmtService: ManagementService,
|
||||
breadcrumbService: BreadcrumbService,
|
||||
private dialog: MatDialog,
|
||||
private toast: ToastService,
|
||||
destroyRef: DestroyRef,
|
||||
) {
|
||||
const bread: Breadcrumb = {
|
||||
type: BreadcrumbType.ORG,
|
||||
@@ -45,31 +45,24 @@ export class ActionsComponent implements OnDestroy {
|
||||
};
|
||||
breadcrumbService.setBreadcrumb([bread]);
|
||||
|
||||
this.getFlowTypes();
|
||||
this.getFlowTypes().then();
|
||||
|
||||
this.typeControl.valueChanges.pipe(takeUntil(this.destroy$)).subscribe((value) => {
|
||||
this.typeControl.valueChanges.pipe(takeUntilDestroyed(destroyRef)).subscribe((value) => {
|
||||
this.loadFlow((value as FlowType.AsObject).id);
|
||||
});
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.destroy$.next();
|
||||
this.destroy$.complete();
|
||||
}
|
||||
|
||||
private getFlowTypes(): Promise<void> {
|
||||
return this.mgmtService
|
||||
.listFlowTypes()
|
||||
.then((resp) => {
|
||||
private async getFlowTypes(): Promise<void> {
|
||||
try {
|
||||
let resp = await this.mgmtService.listFlowTypes();
|
||||
this.typesForSelection = resp.resultList;
|
||||
if (!this.flow && resp.resultList[0]) {
|
||||
const type = resp.resultList[0];
|
||||
this.typeControl.setValue(type);
|
||||
}
|
||||
})
|
||||
.catch((error: any) => {
|
||||
} catch (error) {
|
||||
this.toast.showError(error);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private loadFlow(id: string) {
|
||||
@@ -106,7 +99,7 @@ export class ActionsComponent implements OnDestroy {
|
||||
});
|
||||
}
|
||||
|
||||
public openAddTrigger(flow: FlowType.AsObject, trigger?: TriggerType.AsObject): void {
|
||||
protected openAddTrigger(flow: FlowType.AsObject, trigger?: TriggerType.AsObject): void {
|
||||
const dialogRef = this.dialog.open(AddFlowDialogComponent, {
|
||||
data: {
|
||||
flowType: flow,
|
||||
@@ -119,7 +112,7 @@ export class ActionsComponent implements OnDestroy {
|
||||
if (req) {
|
||||
this.mgmtService
|
||||
.setTriggerActions(req.getActionIdsList(), req.getFlowType(), req.getTriggerType())
|
||||
.then((resp) => {
|
||||
.then(() => {
|
||||
this.toast.showInfo('FLOWS.FLOWCHANGED', true);
|
||||
this.loadFlow(flow.id);
|
||||
})
|
||||
@@ -157,7 +150,7 @@ export class ActionsComponent implements OnDestroy {
|
||||
}
|
||||
}
|
||||
|
||||
public removeTriggerActionsList(index: number) {
|
||||
protected removeTriggerActionsList(index: number) {
|
||||
if (this.flow.type && this.flow.triggerActionsList && this.flow.triggerActionsList[index]) {
|
||||
const dialogRef = this.dialog.open(WarnDialogComponent, {
|
||||
data: {
|
||||
|
@@ -1,19 +1,19 @@
|
||||
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
|
||||
|
||||
import { AddKeyDialogComponent } from './add-key-dialog.component';
|
||||
import { AddActionDialogComponent } from './add-action-dialog.component';
|
||||
|
||||
describe('AddKeyDialogComponent', () => {
|
||||
let component: AddKeyDialogComponent;
|
||||
let fixture: ComponentFixture<AddKeyDialogComponent>;
|
||||
let component: AddActionDialogComponent;
|
||||
let fixture: ComponentFixture<AddActionDialogComponent>;
|
||||
|
||||
beforeEach(waitForAsync(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [AddKeyDialogComponent],
|
||||
declarations: [AddActionDialogComponent],
|
||||
}).compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(AddKeyDialogComponent);
|
||||
fixture = TestBed.createComponent(AddActionDialogComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
@@ -1,19 +1,19 @@
|
||||
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
|
||||
|
||||
import { AddKeyDialogComponent } from './add-key-dialog.component';
|
||||
import { AddFlowDialogComponent } from './add-flow-dialog.component';
|
||||
|
||||
describe('AddKeyDialogComponent', () => {
|
||||
let component: AddKeyDialogComponent;
|
||||
let fixture: ComponentFixture<AddKeyDialogComponent>;
|
||||
let component: AddFlowDialogComponent;
|
||||
let fixture: ComponentFixture<AddFlowDialogComponent>;
|
||||
|
||||
beforeEach(waitForAsync(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [AddKeyDialogComponent],
|
||||
declarations: [AddFlowDialogComponent],
|
||||
}).compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(AddKeyDialogComponent);
|
||||
fixture = TestBed.createComponent(AddFlowDialogComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
@@ -42,8 +42,6 @@ import { SidenavSetting } from 'src/app/modules/sidenav/sidenav.component';
|
||||
import { GrpcAuthService } from 'src/app/services/grpc-auth.service';
|
||||
import { EnvironmentService } from 'src/app/services/environment.service';
|
||||
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
||||
import { NewFeatureService } from '../../services/new-feature.service';
|
||||
import { withLatestFromSynchronousFix } from '../../utils/withLatestFromSynchronousFix';
|
||||
@Component({
|
||||
selector: 'cnsl-instance',
|
||||
templateUrl: './instance.component.html',
|
||||
@@ -106,7 +104,6 @@ export class InstanceComponent {
|
||||
private readonly envService: EnvironmentService,
|
||||
activatedRoute: ActivatedRoute,
|
||||
private readonly destroyRef: DestroyRef,
|
||||
private readonly featureService: NewFeatureService,
|
||||
) {
|
||||
this.loadMembers();
|
||||
|
||||
@@ -139,32 +136,7 @@ export class InstanceComponent {
|
||||
}
|
||||
|
||||
private getSettingsList(): Observable<SidenavSetting[]> {
|
||||
const features$ = this.getFeatures().pipe(shareReplay({ refCount: true, bufferSize: 1 }));
|
||||
|
||||
const actionsEnabled$ = features$.pipe(map((features) => features?.actions?.enabled));
|
||||
|
||||
return this.authService
|
||||
.isAllowedMapper(this.defaultSettingsList, (setting) => setting.requiredRoles.admin || [])
|
||||
.pipe(
|
||||
withLatestFromSynchronousFix(actionsEnabled$),
|
||||
map(([settings, actionsEnabled]) =>
|
||||
settings
|
||||
.filter((setting) => actionsEnabled || setting.id !== ACTIONS.id)
|
||||
.filter((setting) => actionsEnabled || setting.id !== ACTIONS_TARGETS.id),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
private getFeatures() {
|
||||
return defer(() => this.featureService.getInstanceFeatures()).pipe(
|
||||
timeout(1000),
|
||||
catchError((error) => {
|
||||
if (!(error instanceof TimeoutError)) {
|
||||
this.toast.showError(error);
|
||||
}
|
||||
return of(undefined);
|
||||
}),
|
||||
);
|
||||
return this.authService.isAllowedMapper(this.defaultSettingsList, (setting) => setting.requiredRoles.admin || []);
|
||||
}
|
||||
|
||||
public loadMembers(): void {
|
||||
|
@@ -1,7 +1,6 @@
|
||||
import { Component, OnDestroy, OnInit } from '@angular/core';
|
||||
import { MatDialog } from '@angular/material/dialog';
|
||||
import { Router } from '@angular/router';
|
||||
import { Buffer } from 'buffer';
|
||||
import { BehaviorSubject, from, Observable, of, Subject, takeUntil } from 'rxjs';
|
||||
import { catchError, finalize, map } from 'rxjs/operators';
|
||||
import { CreationType, MemberCreateDialogComponent } from 'src/app/modules/add-member-dialog/member-create-dialog.component';
|
||||
@@ -266,10 +265,11 @@ export class OrgDetailComponent implements OnInit, OnDestroy {
|
||||
.listOrgMetadata()
|
||||
.then((resp) => {
|
||||
this.loadingMetadata = false;
|
||||
this.metadata = resp.resultList.map((md) => {
|
||||
const decoder = new TextDecoder();
|
||||
this.metadata = resp.resultList.map(({ key, value }) => {
|
||||
return {
|
||||
key: md.key,
|
||||
value: Buffer.from(md.value as string, 'base64').toString('utf-8'),
|
||||
key,
|
||||
value: atob(typeof value === 'string' ? value : decoder.decode(value)),
|
||||
};
|
||||
});
|
||||
})
|
||||
|
@@ -32,6 +32,7 @@ import { withLatestFromSynchronousFix } from 'src/app/utils/withLatestFromSynchr
|
||||
import { PasswordComplexityValidatorFactoryService } from 'src/app/services/password-complexity-validator-factory.service';
|
||||
import { NewFeatureService } from 'src/app/services/new-feature.service';
|
||||
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
||||
import { GrpcAuthService } from 'src/app/services/grpc-auth.service';
|
||||
|
||||
type PwdForm = ReturnType<UserCreateV2Component['buildPwdForm']>;
|
||||
type AuthenticationFactor =
|
||||
@@ -65,6 +66,7 @@ export class UserCreateV2Component implements OnInit {
|
||||
private readonly destroyRef: DestroyRef,
|
||||
private readonly route: ActivatedRoute,
|
||||
protected readonly location: Location,
|
||||
private readonly authService: GrpcAuthService,
|
||||
) {
|
||||
this.userForm = this.buildUserForm();
|
||||
|
||||
@@ -180,9 +182,12 @@ export class UserCreateV2Component implements OnInit {
|
||||
private async createUserV2Try(authenticationFactor: AuthenticationFactor) {
|
||||
this.loading.set(true);
|
||||
|
||||
const org = await this.authService.getActiveOrg();
|
||||
|
||||
const userValues = this.userForm.getRawValue();
|
||||
|
||||
const humanReq: MessageInitShape<typeof AddHumanUserRequestSchema> = {
|
||||
organization: { org: { case: 'orgId', value: org.id } },
|
||||
username: userValues.username,
|
||||
profile: {
|
||||
givenName: userValues.givenName,
|
||||
|
@@ -1,5 +1,5 @@
|
||||
<h1 mat-dialog-title>
|
||||
<span class="title">{{ 'USER.MFA.DIALOG.ADD_MFA_TITLE' | translate }} {{ data?.number }}</span>
|
||||
<span class="title">{{ 'USER.MFA.DIALOG.ADD_MFA_TITLE' | translate }}</span>
|
||||
</h1>
|
||||
<div mat-dialog-content>
|
||||
<ng-container *ngIf="selectedType === undefined">
|
||||
@@ -7,6 +7,7 @@
|
||||
|
||||
<div class="type-selection">
|
||||
<button
|
||||
*ngIf="data.otp$ | async"
|
||||
mat-stroked-button
|
||||
[disabled]="data.otpDisabled$ | async"
|
||||
(click)="selectType(AuthFactorType.OTP)"
|
||||
@@ -56,7 +57,7 @@
|
||||
<span>{{ 'USER.MFA.OTP' | translate }}</span>
|
||||
</div>
|
||||
</button>
|
||||
<button mat-stroked-button (click)="selectType(AuthFactorType.U2F)">
|
||||
<button *ngIf="data.u2f$ | async" mat-stroked-button (click)="selectType(AuthFactorType.U2F)">
|
||||
<div class="u2f-btn">
|
||||
<div class="icon-row">
|
||||
<svg
|
||||
@@ -78,6 +79,7 @@
|
||||
</div>
|
||||
</button>
|
||||
<button
|
||||
*ngIf="data.otpSms$ | async"
|
||||
[disabled]="!data.phoneVerified || (data.otpSmsDisabled$ | async)"
|
||||
mat-stroked-button
|
||||
(click)="selectType(AuthFactorType.OTPSMS)"
|
||||
@@ -110,7 +112,12 @@
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
<button [disabled]="data.otpEmailDisabled$ | async" mat-stroked-button (click)="selectType(AuthFactorType.OTPEMAIL)">
|
||||
<button
|
||||
*ngIf="data.otpEmail$ | async"
|
||||
[disabled]="data.otpEmailDisabled$ | async"
|
||||
mat-stroked-button
|
||||
(click)="selectType(AuthFactorType.OTPEMAIL)"
|
||||
>
|
||||
<div class="otp-btn">
|
||||
<div class="icon-row">
|
||||
<svg
|
||||
|
@@ -2,6 +2,7 @@ import { Component, Inject } from '@angular/core';
|
||||
import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { take } from 'rxjs/operators';
|
||||
import { Observable } from 'rxjs';
|
||||
import { GrpcAuthService } from 'src/app/services/grpc-auth.service';
|
||||
import { ToastService } from 'src/app/services/toast.service';
|
||||
|
||||
@@ -16,6 +17,17 @@ export enum AuthFactorType {
|
||||
OTPEMAIL,
|
||||
}
|
||||
|
||||
export type AddAuthFactorDialogData = {
|
||||
otp$: Observable<boolean>;
|
||||
u2f$: Observable<boolean>;
|
||||
otpSms$: Observable<boolean>;
|
||||
otpEmail$: Observable<boolean>;
|
||||
otpDisabled$: Observable<boolean>;
|
||||
otpSmsDisabled$: Observable<boolean>;
|
||||
otpEmailDisabled$: Observable<boolean>;
|
||||
phoneVerified: boolean;
|
||||
};
|
||||
|
||||
@Component({
|
||||
selector: 'cnsl-auth-factor-dialog',
|
||||
templateUrl: './auth-factor-dialog.component.html',
|
||||
@@ -44,7 +56,7 @@ export class AuthFactorDialogComponent {
|
||||
private toast: ToastService,
|
||||
private translate: TranslateService,
|
||||
public dialogRef: MatDialogRef<AuthFactorDialogComponent>,
|
||||
@Inject(MAT_DIALOG_DATA) public data: any,
|
||||
@Inject(MAT_DIALOG_DATA) public data: AddAuthFactorDialogData,
|
||||
) {}
|
||||
|
||||
closeDialog(code: string = ''): void {
|
||||
|
@@ -1,24 +1,147 @@
|
||||
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
|
||||
|
||||
import { of } from 'rxjs';
|
||||
import { AuthUserMfaComponent } from './auth-user-mfa.component';
|
||||
import { ToastService } from 'src/app/services/toast.service';
|
||||
import { MatDialog } from '@angular/material/dialog';
|
||||
import { NewAuthService } from 'src/app/services/new-auth.service';
|
||||
import { SecondFactorType } from 'src/app/proto/generated/zitadel/policy_pb';
|
||||
import { CardComponent } from 'src/app/modules/card/card.component';
|
||||
import { RefreshTableComponent } from 'src/app/modules/refresh-table/refresh-table.component';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { MatIconModule } from '@angular/material/icon';
|
||||
import { MatTableModule } from '@angular/material/table';
|
||||
import { MatTooltipModule } from '@angular/material/tooltip';
|
||||
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
|
||||
import { AuthFactor, AuthFactorState } from '@zitadel/proto/zitadel/user_pb';
|
||||
|
||||
describe('AuthUserMfaComponent', () => {
|
||||
let component: AuthUserMfaComponent;
|
||||
let fixture: ComponentFixture<AuthUserMfaComponent>;
|
||||
// Create a test host component that extends the original component
|
||||
class TestHostComponent extends AuthUserMfaComponent {
|
||||
// Expose protected properties for testing
|
||||
public getOtpEmailDisabled$() {
|
||||
return this.otpEmailDisabled$;
|
||||
}
|
||||
|
||||
public getOtpDisabled$() {
|
||||
return this.otpDisabled$;
|
||||
}
|
||||
|
||||
public getOtpSmsDisabled$() {
|
||||
return this.otpSmsDisabled$;
|
||||
}
|
||||
}
|
||||
|
||||
let component: TestHostComponent;
|
||||
let fixture: ComponentFixture<TestHostComponent>;
|
||||
let serviceStub: Partial<NewAuthService>;
|
||||
let toastStub: Partial<ToastService>;
|
||||
let dialogStub: Partial<MatDialog>;
|
||||
|
||||
beforeEach(waitForAsync(() => {
|
||||
// Create stubs for required services
|
||||
serviceStub = {
|
||||
listMyMultiFactors: jasmine.createSpy('listMyMultiFactors').and.returnValue(
|
||||
Promise.resolve({
|
||||
result: [
|
||||
{ type: { case: 'otp' }, state: AuthFactorState.READY, $typeName: 'zitadel.user.v1.AuthFactor' } as AuthFactor,
|
||||
{
|
||||
type: { case: 'otpSms' },
|
||||
state: AuthFactorState.READY,
|
||||
$typeName: 'zitadel.user.v1.AuthFactor',
|
||||
} as AuthFactor,
|
||||
{
|
||||
type: { case: 'otpEmail' },
|
||||
state: AuthFactorState.READY,
|
||||
$typeName: 'zitadel.user.v1.AuthFactor',
|
||||
} as AuthFactor,
|
||||
],
|
||||
}),
|
||||
),
|
||||
getMyLoginPolicy: jasmine.createSpy('getMyLoginPolicy').and.returnValue(
|
||||
Promise.resolve({
|
||||
policy: {
|
||||
secondFactorsList: [
|
||||
SecondFactorType.SECOND_FACTOR_TYPE_OTP,
|
||||
SecondFactorType.SECOND_FACTOR_TYPE_U2F,
|
||||
SecondFactorType.SECOND_FACTOR_TYPE_OTP_EMAIL,
|
||||
SecondFactorType.SECOND_FACTOR_TYPE_OTP_SMS,
|
||||
],
|
||||
},
|
||||
}),
|
||||
),
|
||||
removeMyMultiFactorOTP: jasmine.createSpy('removeMyMultiFactorOTP').and.returnValue(Promise.resolve()),
|
||||
removeMyMultiFactorU2F: jasmine.createSpy('removeMyMultiFactorU2F').and.returnValue(Promise.resolve()),
|
||||
removeMyAuthFactorOTPEmail: jasmine.createSpy('removeMyAuthFactorOTPEmail').and.returnValue(Promise.resolve()),
|
||||
removeMyAuthFactorOTPSMS: jasmine.createSpy('removeMyAuthFactorOTPSMS').and.returnValue(Promise.resolve()),
|
||||
};
|
||||
|
||||
toastStub = {
|
||||
showInfo: jasmine.createSpy('showInfo'),
|
||||
showError: jasmine.createSpy('showError'),
|
||||
};
|
||||
|
||||
dialogStub = {
|
||||
// Opened dialog returns a truthy value after closing
|
||||
open: jasmine.createSpy('open').and.returnValue({
|
||||
afterClosed: () => of(true),
|
||||
}),
|
||||
};
|
||||
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [AuthUserMfaComponent],
|
||||
declarations: [TestHostComponent, CardComponent, RefreshTableComponent], // Use TestHostComponent instead
|
||||
imports: [MatIconModule, TranslateModule.forRoot(), MatTooltipModule, MatTableModule, BrowserAnimationsModule],
|
||||
providers: [
|
||||
{ provide: NewAuthService, useValue: serviceStub },
|
||||
{ provide: ToastService, useValue: toastStub },
|
||||
{ provide: MatDialog, useValue: dialogStub },
|
||||
],
|
||||
}).compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(AuthUserMfaComponent);
|
||||
fixture = TestBed.createComponent(TestHostComponent); // Use TestHostComponent
|
||||
component = fixture.componentInstance;
|
||||
// Optionally set the phoneVerified input if needed by your tests
|
||||
component.phoneVerified = true;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should call getMFAs and update dataSource and disable flags', async () => {
|
||||
// Call the method and wait for the Promise resolution
|
||||
await component.getMFAs();
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(serviceStub.listMyMultiFactors).toHaveBeenCalled();
|
||||
// Our stub returns 3 items
|
||||
expect(component.dataSource.data.length).toBe(3);
|
||||
|
||||
// Use the public getter methods to access protected properties
|
||||
component.getOtpDisabled$().subscribe((value) => {
|
||||
expect(value).toBeTrue();
|
||||
});
|
||||
component.getOtpSmsDisabled$().subscribe((value) => {
|
||||
expect(value).toBeTrue();
|
||||
});
|
||||
component.getOtpEmailDisabled$().subscribe((value) => {
|
||||
expect(value).toBeTrue();
|
||||
});
|
||||
});
|
||||
|
||||
it('should call deleteMFA and remove OTP factor', async () => {
|
||||
// OTP is set
|
||||
const factor = {
|
||||
type: { case: 'otp' },
|
||||
state: AuthFactorState.READY,
|
||||
$typeName: 'zitadel.user.v1.AuthFactor',
|
||||
} as AuthFactor;
|
||||
await component.deleteMFA(factor);
|
||||
|
||||
// Verify that the service method for OTP removal was called
|
||||
expect(serviceStub.removeMyMultiFactorOTP).toHaveBeenCalled();
|
||||
expect(serviceStub.listMyMultiFactors).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
@@ -4,12 +4,12 @@ import { MatSort } from '@angular/material/sort';
|
||||
import { MatTable, MatTableDataSource } from '@angular/material/table';
|
||||
import { BehaviorSubject, Observable } from 'rxjs';
|
||||
import { WarnDialogComponent } from 'src/app/modules/warn-dialog/warn-dialog.component';
|
||||
import { AuthFactor, AuthFactorState } from 'src/app/proto/generated/zitadel/user_pb';
|
||||
import { GrpcAuthService } from 'src/app/services/grpc-auth.service';
|
||||
import { AuthFactorState } from 'src/app/proto/generated/zitadel/user_pb';
|
||||
import { NewAuthService } from 'src/app/services/new-auth.service';
|
||||
import { ToastService } from 'src/app/services/toast.service';
|
||||
|
||||
import { AuthFactorDialogComponent } from '../auth-factor-dialog/auth-factor-dialog.component';
|
||||
|
||||
import { AddAuthFactorDialogData, AuthFactorDialogComponent } from '../auth-factor-dialog/auth-factor-dialog.component';
|
||||
import { AuthFactor } from '@zitadel/proto/zitadel/user_pb';
|
||||
import { SecondFactorType } from '@zitadel/proto/zitadel/policy_pb';
|
||||
export interface WebAuthNOptions {
|
||||
challenge: string;
|
||||
rp: { name: string; id: string };
|
||||
@@ -30,26 +30,31 @@ export class AuthUserMfaComponent implements OnInit, OnDestroy {
|
||||
private loadingSubject: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(false);
|
||||
public loading$: Observable<boolean> = this.loadingSubject.asObservable();
|
||||
|
||||
@ViewChild(MatTable) public table!: MatTable<AuthFactor.AsObject>;
|
||||
@ViewChild(MatTable) public table!: MatTable<AuthFactor>;
|
||||
@ViewChild(MatSort) public sort!: MatSort;
|
||||
@Input() public phoneVerified: boolean = false;
|
||||
public dataSource: MatTableDataSource<AuthFactor.AsObject> = new MatTableDataSource<AuthFactor.AsObject>([]);
|
||||
|
||||
public AuthFactorState: any = AuthFactorState;
|
||||
public dataSource: MatTableDataSource<AuthFactor> = new MatTableDataSource<AuthFactor>([]);
|
||||
|
||||
public error: string = '';
|
||||
public otpDisabled$ = new BehaviorSubject<boolean>(true);
|
||||
public otpSmsDisabled$ = new BehaviorSubject<boolean>(true);
|
||||
public otpEmailDisabled$ = new BehaviorSubject<boolean>(true);
|
||||
protected error: string = '';
|
||||
|
||||
protected otpAvailable$ = new BehaviorSubject<boolean>(false);
|
||||
protected u2fAvailable$ = new BehaviorSubject<boolean>(false);
|
||||
protected otpSmsAvailable$ = new BehaviorSubject<boolean>(false);
|
||||
protected otpEmailAvailable$ = new BehaviorSubject<boolean>(false);
|
||||
protected otpDisabled$ = new BehaviorSubject<boolean>(true);
|
||||
protected otpSmsDisabled$ = new BehaviorSubject<boolean>(true);
|
||||
protected otpEmailDisabled$ = new BehaviorSubject<boolean>(true);
|
||||
|
||||
constructor(
|
||||
private service: GrpcAuthService,
|
||||
private toast: ToastService,
|
||||
private dialog: MatDialog,
|
||||
private readonly service: NewAuthService,
|
||||
private readonly toast: ToastService,
|
||||
private readonly dialog: MatDialog,
|
||||
) {}
|
||||
|
||||
public ngOnInit(): void {
|
||||
this.getMFAs();
|
||||
this.applyOrgPolicy();
|
||||
}
|
||||
|
||||
public ngOnDestroy(): void {
|
||||
@@ -57,13 +62,19 @@ export class AuthUserMfaComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
|
||||
public addAuthFactor(): void {
|
||||
const dialogRef = this.dialog.open(AuthFactorDialogComponent, {
|
||||
data: {
|
||||
const data: AddAuthFactorDialogData = {
|
||||
otp$: this.otpAvailable$,
|
||||
u2f$: this.u2fAvailable$,
|
||||
otpSms$: this.otpSmsAvailable$,
|
||||
otpEmail$: this.otpEmailAvailable$,
|
||||
otpDisabled$: this.otpDisabled$,
|
||||
otpSmsDisabled$: this.otpSmsDisabled$,
|
||||
otpEmailDisabled$: this.otpEmailDisabled$,
|
||||
phoneVerified: this.phoneVerified,
|
||||
},
|
||||
} as const;
|
||||
|
||||
const dialogRef = this.dialog.open(AuthFactorDialogComponent, {
|
||||
data: data,
|
||||
});
|
||||
|
||||
dialogRef.afterClosed().subscribe(() => {
|
||||
@@ -75,48 +86,32 @@ export class AuthUserMfaComponent implements OnInit, OnDestroy {
|
||||
this.service
|
||||
.listMyMultiFactors()
|
||||
.then((mfas) => {
|
||||
const list = mfas.resultList;
|
||||
const list: AuthFactor[] = mfas.result;
|
||||
this.dataSource = new MatTableDataSource(list);
|
||||
this.dataSource.sort = this.sort;
|
||||
|
||||
const index = list.findIndex((mfa) => mfa.otp);
|
||||
if (index === -1) {
|
||||
this.otpDisabled$.next(false);
|
||||
}
|
||||
|
||||
const sms = list.findIndex((mfa) => mfa.otpSms);
|
||||
if (sms === -1) {
|
||||
this.otpSmsDisabled$.next(false);
|
||||
}
|
||||
|
||||
const email = list.findIndex((mfa) => mfa.otpEmail);
|
||||
if (email === -1) {
|
||||
this.otpEmailDisabled$.next(false);
|
||||
}
|
||||
this.disableAuthFactor(list, 'otp', this.otpDisabled$);
|
||||
this.disableAuthFactor(list, 'otpSms', this.otpSmsDisabled$);
|
||||
this.disableAuthFactor(list, 'otpEmail', this.otpEmailDisabled$);
|
||||
})
|
||||
.catch((error) => {
|
||||
this.error = error.message;
|
||||
});
|
||||
}
|
||||
|
||||
private cleanupList(): void {
|
||||
const totp = this.dataSource.data.findIndex((mfa) => !!mfa.otp);
|
||||
if (totp > -1) {
|
||||
this.dataSource.data.splice(totp, 1);
|
||||
public applyOrgPolicy(): void {
|
||||
this.service.getMyLoginPolicy().then((resp) => {
|
||||
if (resp && resp.policy) {
|
||||
const secondFactors = resp.policy?.secondFactors;
|
||||
this.displayAuthFactorBasedOnPolicy(secondFactors, SecondFactorType.OTP, this.otpAvailable$);
|
||||
this.displayAuthFactorBasedOnPolicy(secondFactors, SecondFactorType.U2F, this.u2fAvailable$);
|
||||
this.displayAuthFactorBasedOnPolicy(secondFactors, SecondFactorType.OTP_EMAIL, this.otpEmailAvailable$);
|
||||
this.displayAuthFactorBasedOnPolicy(secondFactors, SecondFactorType.OTP_SMS, this.otpSmsAvailable$);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const sms = this.dataSource.data.findIndex((mfa) => !!mfa.otpSms);
|
||||
if (sms > -1) {
|
||||
this.dataSource.data.splice(sms, 1);
|
||||
}
|
||||
|
||||
const email = this.dataSource.data.findIndex((mfa) => !!mfa.otpEmail);
|
||||
if (email > -1) {
|
||||
this.dataSource.data.splice(email, 1);
|
||||
}
|
||||
}
|
||||
|
||||
public deleteMFA(factor: AuthFactor.AsObject): void {
|
||||
public deleteMFA(factor: AuthFactor): void {
|
||||
const dialogRef = this.dialog.open(WarnDialogComponent, {
|
||||
data: {
|
||||
confirmKey: 'ACTIONS.DELETE',
|
||||
@@ -129,7 +124,7 @@ export class AuthUserMfaComponent implements OnInit, OnDestroy {
|
||||
|
||||
dialogRef.afterClosed().subscribe((resp) => {
|
||||
if (resp) {
|
||||
if (factor.otp) {
|
||||
if (factor.type.case === 'otp') {
|
||||
this.service
|
||||
.removeMyMultiFactorOTP()
|
||||
.then(() => {
|
||||
@@ -141,9 +136,9 @@ export class AuthUserMfaComponent implements OnInit, OnDestroy {
|
||||
.catch((error) => {
|
||||
this.toast.showError(error);
|
||||
});
|
||||
} else if (factor.u2f) {
|
||||
} else if (factor.type.case === 'u2f') {
|
||||
this.service
|
||||
.removeMyMultiFactorU2F(factor.u2f.id)
|
||||
.removeMyMultiFactorU2F(factor.type.value.id)
|
||||
.then(() => {
|
||||
this.toast.showInfo('USER.TOAST.U2FREMOVED', true);
|
||||
|
||||
@@ -153,7 +148,7 @@ export class AuthUserMfaComponent implements OnInit, OnDestroy {
|
||||
.catch((error) => {
|
||||
this.toast.showError(error);
|
||||
});
|
||||
} else if (factor.otpEmail) {
|
||||
} else if (factor.type.case === 'otpEmail') {
|
||||
this.service
|
||||
.removeMyAuthFactorOTPEmail()
|
||||
.then(() => {
|
||||
@@ -165,7 +160,7 @@ export class AuthUserMfaComponent implements OnInit, OnDestroy {
|
||||
.catch((error) => {
|
||||
this.toast.showError(error);
|
||||
});
|
||||
} else if (factor.otpSms) {
|
||||
} else if (factor.type.case === 'otpSms') {
|
||||
this.service
|
||||
.removeMyAuthFactorOTPSMS()
|
||||
.then(() => {
|
||||
@@ -181,4 +176,22 @@ export class AuthUserMfaComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private cleanupList(): void {
|
||||
this.dataSource.data = this.dataSource.data.filter((mfa: AuthFactor) => {
|
||||
return mfa.type.case;
|
||||
});
|
||||
}
|
||||
|
||||
private disableAuthFactor(mfas: AuthFactor[], key: string, subject: BehaviorSubject<boolean>): void {
|
||||
subject.next(mfas.some((mfa) => mfa.type.case === key));
|
||||
}
|
||||
|
||||
private displayAuthFactorBasedOnPolicy(
|
||||
factors: SecondFactorType[],
|
||||
factor: SecondFactorType,
|
||||
subject: BehaviorSubject<boolean>,
|
||||
): void {
|
||||
subject.next(factors.some((f) => f === factor));
|
||||
}
|
||||
}
|
||||
|
@@ -2,7 +2,7 @@ import { Component, EventEmitter, Input, Output } from '@angular/core';
|
||||
import { MatDialog } from '@angular/material/dialog';
|
||||
import { WarnDialogComponent } from 'src/app/modules/warn-dialog/warn-dialog.component';
|
||||
|
||||
import { GrpcAuthService } from 'src/app/services/grpc-auth.service';
|
||||
import { NewAuthService } from 'src/app/services/new-auth.service';
|
||||
import { CodeDialogComponent } from '../auth-user-detail/code-dialog/code-dialog.component';
|
||||
import { EditDialogType } from '../auth-user-detail/edit-dialog/edit-dialog.component';
|
||||
import { HumanUser, UserState } from '@zitadel/proto/zitadel/user/v2/user_pb';
|
||||
@@ -28,12 +28,12 @@ export class ContactComponent {
|
||||
public EditDialogType: any = EditDialogType;
|
||||
constructor(
|
||||
private dialog: MatDialog,
|
||||
private authService: GrpcAuthService,
|
||||
private authService: NewAuthService,
|
||||
) {}
|
||||
|
||||
async emitDeletePhone(): Promise<void> {
|
||||
const { resultList } = await this.authService.listMyMultiFactors();
|
||||
const hasSMSOTP = !!resultList.find((mfa) => mfa.otpSms);
|
||||
const { result } = await this.authService.listMyMultiFactors();
|
||||
const hasSMSOTP = !!result.some((mfa) => mfa.type.case === 'otpSms');
|
||||
|
||||
const dialogRef = this.dialog.open(WarnDialogComponent, {
|
||||
data: {
|
||||
|
@@ -1,19 +1,19 @@
|
||||
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
|
||||
|
||||
import { DetailFormComponent } from './detail-form.component';
|
||||
import { DetailFormMachineComponent } from './detail-form-machine.component';
|
||||
|
||||
describe('DetailFormComponent', () => {
|
||||
let component: DetailFormComponent;
|
||||
let fixture: ComponentFixture<DetailFormComponent>;
|
||||
let component: DetailFormMachineComponent;
|
||||
let fixture: ComponentFixture<DetailFormMachineComponent>;
|
||||
|
||||
beforeEach(waitForAsync(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [DetailFormComponent],
|
||||
declarations: [DetailFormMachineComponent],
|
||||
}).compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(DetailFormComponent);
|
||||
fixture = TestBed.createComponent(DetailFormMachineComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
@@ -1,19 +1,19 @@
|
||||
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
|
||||
|
||||
import { AuthPasswordlessComponent } from './auth-passwordless.component';
|
||||
import { PasswordlessComponent } from './passwordless.component';
|
||||
|
||||
describe('AuthPasswordlessComponent', () => {
|
||||
let component: AuthPasswordlessComponent;
|
||||
let fixture: ComponentFixture<AuthPasswordlessComponent>;
|
||||
let component: PasswordlessComponent;
|
||||
let fixture: ComponentFixture<PasswordlessComponent>;
|
||||
|
||||
beforeEach(waitForAsync(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [AuthPasswordlessComponent],
|
||||
declarations: [PasswordlessComponent],
|
||||
}).compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(AuthPasswordlessComponent);
|
||||
fixture = TestBed.createComponent(PasswordlessComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
@@ -4,7 +4,6 @@ import { Validators } from '@angular/forms';
|
||||
import { MatDialog } from '@angular/material/dialog';
|
||||
import { ActivatedRoute, Params, Router } from '@angular/router';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { Buffer } from 'buffer';
|
||||
import { catchError, filter, map, startWith, take } from 'rxjs/operators';
|
||||
import { ChangeType } from 'src/app/modules/changes/changes.component';
|
||||
import { phoneValidator, requiredValidator } from 'src/app/modules/form-field/validators/validators';
|
||||
@@ -582,7 +581,7 @@ export class UserDetailComponent implements OnInit {
|
||||
const setFcn = (key: string, value: string) =>
|
||||
this.newMgmtService.setUserMetadata({
|
||||
key,
|
||||
value: Buffer.from(value),
|
||||
value: new TextEncoder().encode(value),
|
||||
id: user.userId,
|
||||
});
|
||||
const removeFcn = (key: string): Promise<any> => this.newMgmtService.removeUserMetadata({ key, id: user.userId });
|
||||
|
@@ -1,7 +1,18 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { SortDirection } from '@angular/material/sort';
|
||||
import { OAuthService } from 'angular-oauth2-oidc';
|
||||
import { BehaviorSubject, combineLatestWith, EMPTY, mergeWith, NEVER, Observable, of, shareReplay, Subject } from 'rxjs';
|
||||
import {
|
||||
BehaviorSubject,
|
||||
combineLatestWith,
|
||||
EMPTY,
|
||||
identity,
|
||||
mergeWith,
|
||||
NEVER,
|
||||
Observable,
|
||||
of,
|
||||
shareReplay,
|
||||
Subject,
|
||||
} from 'rxjs';
|
||||
import { catchError, distinctUntilChanged, filter, finalize, map, startWith, switchMap, tap, timeout } from 'rxjs/operators';
|
||||
|
||||
import {
|
||||
@@ -20,8 +31,6 @@ import {
|
||||
GetMyEmailRequest,
|
||||
GetMyEmailResponse,
|
||||
GetMyLabelPolicyRequest,
|
||||
GetMyLoginPolicyRequest,
|
||||
GetMyLoginPolicyResponse,
|
||||
GetMyPasswordComplexityPolicyRequest,
|
||||
GetMyPasswordComplexityPolicyResponse,
|
||||
GetMyPhoneRequest,
|
||||
@@ -31,8 +40,6 @@ import {
|
||||
GetMyProfileResponse,
|
||||
GetMyUserRequest,
|
||||
GetMyUserResponse,
|
||||
ListMyAuthFactorsRequest,
|
||||
ListMyAuthFactorsResponse,
|
||||
ListMyLinkedIDPsRequest,
|
||||
ListMyLinkedIDPsResponse,
|
||||
ListMyMembershipsRequest,
|
||||
@@ -51,14 +58,6 @@ import {
|
||||
ListMyUserSessionsResponse,
|
||||
ListMyZitadelPermissionsRequest,
|
||||
ListMyZitadelPermissionsResponse,
|
||||
RemoveMyAuthFactorOTPEmailRequest,
|
||||
RemoveMyAuthFactorOTPEmailResponse,
|
||||
RemoveMyAuthFactorOTPRequest,
|
||||
RemoveMyAuthFactorOTPResponse,
|
||||
RemoveMyAuthFactorOTPSMSRequest,
|
||||
RemoveMyAuthFactorOTPSMSResponse,
|
||||
RemoveMyAuthFactorU2FRequest,
|
||||
RemoveMyAuthFactorU2FResponse,
|
||||
RemoveMyAvatarRequest,
|
||||
RemoveMyAvatarResponse,
|
||||
RemoveMyLinkedIDPRequest,
|
||||
@@ -326,7 +325,7 @@ export class GrpcAuthService {
|
||||
return new RegExp(reqRegexp).test(role);
|
||||
});
|
||||
|
||||
const allCheck = requestedRoles.map(test).every((x) => !!x);
|
||||
const allCheck = requestedRoles.map(test).every(identity);
|
||||
const oneCheck = requestedRoles.some(test);
|
||||
|
||||
return requiresAll ? allCheck : oneCheck;
|
||||
@@ -346,10 +345,6 @@ export class GrpcAuthService {
|
||||
return this.grpcService.auth.getMyUser(new GetMyUserRequest(), null).then((resp) => resp.toObject());
|
||||
}
|
||||
|
||||
public listMyMultiFactors(): Promise<ListMyAuthFactorsResponse.AsObject> {
|
||||
return this.grpcService.auth.listMyAuthFactors(new ListMyAuthFactorsRequest(), null).then((resp) => resp.toObject());
|
||||
}
|
||||
|
||||
public async revalidateOrgs() {
|
||||
const orgs = (await this.listMyProjectOrgs(ORG_LIMIT, 0)).resultList;
|
||||
this.cachedOrgs.next(orgs);
|
||||
@@ -477,11 +472,6 @@ export class GrpcAuthService {
|
||||
return this.grpcService.auth.resendMyEmailVerification(req, null).then((resp) => resp.toObject());
|
||||
}
|
||||
|
||||
public getMyLoginPolicy(): Promise<GetMyLoginPolicyResponse.AsObject> {
|
||||
const req = new GetMyLoginPolicyRequest();
|
||||
return this.grpcService.auth.getMyLoginPolicy(req, null).then((resp) => resp.toObject());
|
||||
}
|
||||
|
||||
public removeMyPhone(): Promise<RemoveMyPhoneResponse.AsObject> {
|
||||
return this.grpcService.auth.removeMyPhone(new RemoveMyPhoneRequest(), null).then((resp) => resp.toObject());
|
||||
}
|
||||
@@ -565,12 +555,6 @@ export class GrpcAuthService {
|
||||
return this.grpcService.auth.addMyAuthFactorU2F(new AddMyAuthFactorU2FRequest(), null).then((resp) => resp.toObject());
|
||||
}
|
||||
|
||||
public removeMyMultiFactorU2F(tokenId: string): Promise<RemoveMyAuthFactorU2FResponse.AsObject> {
|
||||
const req = new RemoveMyAuthFactorU2FRequest();
|
||||
req.setTokenId(tokenId);
|
||||
return this.grpcService.auth.removeMyAuthFactorU2F(req, null).then((resp) => resp.toObject());
|
||||
}
|
||||
|
||||
public verifyMyMultiFactorU2F(credential: string, tokenname: string): Promise<VerifyMyAuthFactorU2FResponse.AsObject> {
|
||||
const req = new VerifyMyAuthFactorU2FRequest();
|
||||
const verification = new WebAuthNVerification();
|
||||
@@ -615,24 +599,6 @@ export class GrpcAuthService {
|
||||
return this.grpcService.auth.addMyPasswordlessLink(req, null).then((resp) => resp.toObject());
|
||||
}
|
||||
|
||||
public removeMyMultiFactorOTP(): Promise<RemoveMyAuthFactorOTPResponse.AsObject> {
|
||||
return this.grpcService.auth
|
||||
.removeMyAuthFactorOTP(new RemoveMyAuthFactorOTPRequest(), null)
|
||||
.then((resp) => resp.toObject());
|
||||
}
|
||||
|
||||
public removeMyAuthFactorOTPSMS(): Promise<RemoveMyAuthFactorOTPSMSResponse.AsObject> {
|
||||
return this.grpcService.auth
|
||||
.removeMyAuthFactorOTPSMS(new RemoveMyAuthFactorOTPSMSRequest(), null)
|
||||
.then((resp) => resp.toObject());
|
||||
}
|
||||
|
||||
public removeMyAuthFactorOTPEmail(): Promise<RemoveMyAuthFactorOTPEmailResponse.AsObject> {
|
||||
return this.grpcService.auth
|
||||
.removeMyAuthFactorOTPEmail(new RemoveMyAuthFactorOTPEmailRequest(), null)
|
||||
.then((resp) => resp.toObject());
|
||||
}
|
||||
|
||||
public verifyMyMultiFactorOTP(code: string): Promise<VerifyMyAuthFactorOTPResponse.AsObject> {
|
||||
const req = new VerifyMyAuthFactorOTPRequest();
|
||||
req.setCode(code);
|
||||
|
@@ -15,7 +15,6 @@ import { AuthInterceptor, AuthInterceptorProvider, NewConnectWebAuthInterceptor
|
||||
import { ExhaustedGrpcInterceptor } from './interceptors/exhausted.grpc.interceptor';
|
||||
import { I18nInterceptor } from './interceptors/i18n.interceptor';
|
||||
import { NewConnectWebOrgInterceptor, OrgInterceptor, OrgInterceptorProvider } from './interceptors/org.interceptor';
|
||||
import { StorageService } from './storage.service';
|
||||
import { UserServiceClient } from '../proto/generated/zitadel/user/v2/User_serviceServiceClientPb';
|
||||
//@ts-ignore
|
||||
import { createFeatureServiceClient, createUserServiceClient, createSessionServiceClient } from '@zitadel/client/v2';
|
||||
@@ -24,14 +23,10 @@ import { createAuthServiceClient, createManagementServiceClient } from '@zitadel
|
||||
import { createGrpcWebTransport } from '@connectrpc/connect-web';
|
||||
// @ts-ignore
|
||||
import { createClientFor } from '@zitadel/client';
|
||||
import { Client, Transport } from '@connectrpc/connect';
|
||||
|
||||
import { WebKeyService } from '@zitadel/proto/zitadel/webkey/v2beta/webkey_service_pb';
|
||||
import { ActionService } from '@zitadel/proto/zitadel/action/v2beta/action_service_pb';
|
||||
|
||||
// @ts-ignore
|
||||
import { createClientFor } from '@zitadel/client';
|
||||
|
||||
const createWebKeyServiceClient = createClientFor(WebKeyService);
|
||||
const createActionServiceClient = createClientFor(ActionService);
|
||||
|
||||
|
@@ -1,9 +1,22 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { GrpcService } from './grpc.service';
|
||||
import { create } from '@bufbuild/protobuf';
|
||||
import {
|
||||
AddMyAuthFactorOTPSMSResponse,
|
||||
GetMyLoginPolicyResponse,
|
||||
GetMyLoginPolicyRequestSchema,
|
||||
GetMyPasswordComplexityPolicyResponse,
|
||||
GetMyUserResponse,
|
||||
ListMyAuthFactorsRequestSchema,
|
||||
ListMyAuthFactorsResponse,
|
||||
RemoveMyAuthFactorOTPEmailRequestSchema,
|
||||
RemoveMyAuthFactorOTPEmailResponse,
|
||||
RemoveMyAuthFactorOTPRequestSchema,
|
||||
RemoveMyAuthFactorOTPResponse,
|
||||
RemoveMyAuthFactorU2FRequestSchema,
|
||||
RemoveMyAuthFactorU2FResponse,
|
||||
RemoveMyAuthFactorOTPSMSRequestSchema,
|
||||
RemoveMyAuthFactorOTPSMSResponse,
|
||||
ListMyMetadataResponse,
|
||||
VerifyMyPhoneResponse,
|
||||
} from '@zitadel/proto/zitadel/auth_pb';
|
||||
@@ -30,6 +43,30 @@ export class NewAuthService {
|
||||
return this.grpcService.authNew.listMyMetadata({});
|
||||
}
|
||||
|
||||
public listMyMultiFactors(): Promise<ListMyAuthFactorsResponse> {
|
||||
return this.grpcService.authNew.listMyAuthFactors(create(ListMyAuthFactorsRequestSchema), null);
|
||||
}
|
||||
|
||||
public removeMyAuthFactorOTPSMS(): Promise<RemoveMyAuthFactorOTPSMSResponse> {
|
||||
return this.grpcService.authNew.removeMyAuthFactorOTPSMS(create(RemoveMyAuthFactorOTPSMSRequestSchema), null);
|
||||
}
|
||||
|
||||
public getMyLoginPolicy(): Promise<GetMyLoginPolicyResponse> {
|
||||
return this.grpcService.authNew.getMyLoginPolicy(create(GetMyLoginPolicyRequestSchema), null);
|
||||
}
|
||||
|
||||
public removeMyMultiFactorOTP(): Promise<RemoveMyAuthFactorOTPResponse> {
|
||||
return this.grpcService.authNew.removeMyAuthFactorOTP(create(RemoveMyAuthFactorOTPRequestSchema), null);
|
||||
}
|
||||
|
||||
public removeMyMultiFactorU2F(tokenId: string): Promise<RemoveMyAuthFactorU2FResponse> {
|
||||
return this.grpcService.authNew.removeMyAuthFactorU2F(create(RemoveMyAuthFactorU2FRequestSchema, { tokenId }), null);
|
||||
}
|
||||
|
||||
public removeMyAuthFactorOTPEmail(): Promise<RemoveMyAuthFactorOTPEmailResponse> {
|
||||
return this.grpcService.authNew.removeMyAuthFactorOTPEmail(create(RemoveMyAuthFactorOTPEmailRequestSchema), null);
|
||||
}
|
||||
|
||||
public getMyPasswordComplexityPolicy(): Promise<GetMyPasswordComplexityPolicyResponse> {
|
||||
return this.grpcService.authNew.getMyPasswordComplexityPolicy({});
|
||||
}
|
||||
|
@@ -26,8 +26,16 @@ export class PosthogService implements OnDestroy {
|
||||
maskAllInputs: true,
|
||||
maskTextSelector: '*',
|
||||
},
|
||||
disable_session_recording: true,
|
||||
enable_heatmaps: true,
|
||||
persistence: 'memory',
|
||||
loaded: (posthog) => {
|
||||
posthog.onFeatureFlags((flags) => {
|
||||
if (posthog.isFeatureEnabled('session_recording')) {
|
||||
posthog.startSessionRecording();
|
||||
}
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@@ -75,7 +75,8 @@
|
||||
"FLOWS": {
|
||||
"TITLE": "Потоци",
|
||||
"DESCRIPTION": "Изберете поток за удостоверяване и активирайте вашето действие при конкретно събитие в този поток."
|
||||
}
|
||||
},
|
||||
"ACTIONSTWO_NOTE": "Actions V2, нова и подобрена версия на Actions, вече е налична. Настоящата версия все още е достъпна, но бъдещото развитие ще бъде фокусирано върху новата, която в крайна сметка ще замени текущата версия."
|
||||
},
|
||||
"SETTINGS": {
|
||||
"INSTANCE": {
|
||||
@@ -528,13 +529,14 @@
|
||||
"APPLY": "Прилагам"
|
||||
},
|
||||
"ACTIONSTWO": {
|
||||
"BETA_NOTE": "В момента използвате новата версия Actions V2, която е в бета фаза. Предишната версия 1 все още е достъпна, но ще бъде спряна в бъдеще. Моля, съобщавайте за всякакви проблеми или изпратете обратна връзка.",
|
||||
"EXECUTION": {
|
||||
"TITLE": "Действия",
|
||||
"DESCRIPTION": "Действията ви позволяват да изпълнявате персонализиран код в отговор на API заявки, събития или специфични функции. Използвайте ги, за да разширите Zitadel, да автоматизирате работни процеси и да се интегрирате с други системи.",
|
||||
"TYPES": {
|
||||
"request": "Заявка",
|
||||
"response": "Отговор",
|
||||
"events": "Събития",
|
||||
"event": "Събития",
|
||||
"function": "Функция"
|
||||
},
|
||||
"DIALOG": {
|
||||
@@ -565,6 +567,7 @@
|
||||
"TITLE": "Всички",
|
||||
"DESCRIPTION": "Изберете това, ако искате да изпълните действието си при всяка заявка"
|
||||
},
|
||||
"ALL_EVENTS": "Изберете това, ако искате действието да се изпълнява при всяко събитие",
|
||||
"SELECT_SERVICE": {
|
||||
"TITLE": "Избор на услуга",
|
||||
"DESCRIPTION": "Изберете услуга на Zitadel за вашето действие."
|
||||
@@ -618,6 +621,7 @@
|
||||
"restCall": "REST извикване",
|
||||
"restAsync": "REST асинхронно"
|
||||
},
|
||||
"TYPES_DESCRIPTION": "Webhook, обаждането обработва кода на състоянието, но отговорът е без значение\nCall, обаждането обработва кода на състоянието и отговора\nAsync, обаждането не обработва нито кода на състоянието, нито отговора, но може да бъде извикано паралелно с други цели",
|
||||
"ENDPOINT": "Крайна точка",
|
||||
"ENDPOINT_DESCRIPTION": "Въведете крайната точка, където се хоства вашият код. Уверете се, че е достъпна за нас!",
|
||||
"TIMEOUT": "Време за изчакване",
|
||||
@@ -688,6 +692,7 @@
|
||||
"EMAIL": "електронна поща",
|
||||
"USERNAME": "Потребителско име",
|
||||
"ORGNAME": "Наименование на организацията",
|
||||
"ORGID": "Идентификатор на организацията",
|
||||
"PRIMARYDOMAIN": "Основен домейн",
|
||||
"PROJECTNAME": "Име на проекта",
|
||||
"RESOURCEOWNER": "Собственик на ресурс",
|
||||
@@ -1507,7 +1512,8 @@
|
||||
"APPEARANCE": "Външен вид",
|
||||
"OTHER": "други",
|
||||
"STORAGE": "Съхранение"
|
||||
}
|
||||
},
|
||||
"BETA": "БЕТА"
|
||||
},
|
||||
"SETTING": {
|
||||
"LANGUAGES": {
|
||||
@@ -2145,6 +2151,7 @@
|
||||
"ACTIVATE": "Активиране на проекта",
|
||||
"DELETE": "Изтриване на проекта",
|
||||
"ORGNAME": "Наименование на организацията",
|
||||
"ORGID": "Идентификатор на организацията",
|
||||
"ORGDOMAIN": "Домейн на организацията",
|
||||
"STATE": "Статус",
|
||||
"TYPE": "Тип",
|
||||
@@ -2333,7 +2340,8 @@
|
||||
"REMOVE_WARN_DESCRIPTION": "На път сте да премахнете доставчик на самоличност. Това ще премахне избора на наличен IDP за вашите потребители и вече регистрираните потребители няма да могат да влязат отново. Сигурни ли сте, че ще продължите?",
|
||||
"DELETE_SELECTION_TITLE": "Изтриване на IDP",
|
||||
"DELETE_SELECTION_DESCRIPTION": "На път сте да изтриете доставчик на самоличност. ",
|
||||
"EMPTY": "Няма наличен IDP",
|
||||
"FEDERATEDLOGOUTENABLED": "Федерирано изписване разрешено",
|
||||
"FEDERATEDLOGOUTENABLED_DESC": "Ако е разрешено, потребителят ще бъде изписан и от IdP, ако прекрати сесията си в ZITADEL.",
|
||||
"OIDC": {
|
||||
"GENERAL": "Главна информация",
|
||||
"TITLE": "Конфигурация на OIDC",
|
||||
|
@@ -75,7 +75,8 @@
|
||||
"FLOWS": {
|
||||
"TITLE": "Flows",
|
||||
"DESCRIPTION": "Vyberte proces autentizace a spusťte vaši akci na konkrétní události v rámci tohoto procesu."
|
||||
}
|
||||
},
|
||||
"ACTIONSTWO_NOTE": "Actions V2, nová a vylepšená verze Actions, je nyní k dispozici. Aktuální verze je stále přístupná, ale budoucí vývoj se zaměří na novou verzi, která nakonec nahradí tu současnou."
|
||||
},
|
||||
"SETTINGS": {
|
||||
"INSTANCE": {
|
||||
@@ -529,13 +530,14 @@
|
||||
"APPLY": "Platit"
|
||||
},
|
||||
"ACTIONSTWO": {
|
||||
"BETA_NOTE": "Aktuálně používáte novou verzi Actions V2, která je v beta verzi. Předchozí verze 1 je stále k dispozici, ale v budoucnu bude ukončena. Prosím, hlaste jakékoliv problémy nebo zpětnou vazbu.",
|
||||
"EXECUTION": {
|
||||
"TITLE": "Akce",
|
||||
"DESCRIPTION": "Akce vám umožňují spouštět vlastní kód v reakci na požadavky API, události nebo specifické funkce. Použijte je k rozšíření Zitadel, automatizaci pracovních postupů a integraci s dalšími systémy.",
|
||||
"TYPES": {
|
||||
"request": "Požadavek",
|
||||
"response": "Odpověď",
|
||||
"events": "Události",
|
||||
"event": "Události",
|
||||
"function": "Funkce"
|
||||
},
|
||||
"DIALOG": {
|
||||
@@ -566,6 +568,7 @@
|
||||
"TITLE": "Všechny",
|
||||
"DESCRIPTION": "Vyberte tuto možnost, pokud chcete spustit akci pro každý požadavek"
|
||||
},
|
||||
"ALL_EVENTS": "Vyberte toto, pokud chcete spustit akci při každé události",
|
||||
"SELECT_SERVICE": {
|
||||
"TITLE": "Vybrat službu",
|
||||
"DESCRIPTION": "Vyberte službu Zitadel pro svou akci."
|
||||
@@ -619,6 +622,7 @@
|
||||
"restCall": "REST Volání",
|
||||
"restAsync": "REST Asynchronní"
|
||||
},
|
||||
"TYPES_DESCRIPTION": "Webhook, volání zpracovává stavový kód, ale odpověď je irelevantní\nCall, volání zpracovává stavový kód a odpověď\nAsync, volání nezpracovává ani stavový kód, ani odpověď, ale může být spuštěno paralelně s jinými cíli",
|
||||
"ENDPOINT": "Koncový bod",
|
||||
"ENDPOINT_DESCRIPTION": "Zadejte koncový bod, kde je hostován váš kód. Ujistěte se, že je pro nás přístupný!",
|
||||
"TIMEOUT": "Časový limit",
|
||||
@@ -689,6 +693,7 @@
|
||||
"EMAIL": "Email",
|
||||
"USERNAME": "Uživatelské jméno",
|
||||
"ORGNAME": "Název organizace",
|
||||
"ORGID": "ID organizace",
|
||||
"PRIMARYDOMAIN": "Primární doména",
|
||||
"PROJECTNAME": "Název projektu",
|
||||
"RESOURCEOWNER": "Vlastník zdroje",
|
||||
@@ -1508,7 +1513,8 @@
|
||||
"APPEARANCE": "Vzhled",
|
||||
"OTHER": "Ostatní",
|
||||
"STORAGE": "Data"
|
||||
}
|
||||
},
|
||||
"BETA": "BETA"
|
||||
},
|
||||
"SETTING": {
|
||||
"LANGUAGES": {
|
||||
@@ -2146,6 +2152,7 @@
|
||||
"ACTIVATE": "Aktivovat projekt",
|
||||
"DELETE": "Smazat projekt",
|
||||
"ORGNAME": "Název organizace",
|
||||
"ORGID": "ID organizace",
|
||||
"ORGDOMAIN": "Doména organizace",
|
||||
"STATE": "Stav",
|
||||
"TYPE": "Typ",
|
||||
@@ -2338,6 +2345,8 @@
|
||||
"REMOVE_WARN_DESCRIPTION": "Chystáte se odebrat poskytovatele identity. To odstraní výběr dostupného IDP pro vaše uživatele a již registrovaní uživatelé se nebudou moci znovu přihlásit. Jste si jisti, že chcete pokračovat?",
|
||||
"DELETE_SELECTION_TITLE": "Odstranit IDP",
|
||||
"DELETE_SELECTION_DESCRIPTION": "Chystáte se odstranit poskytovatele identity. Výsledné změny jsou nevratné. Opravdu to chcete udělat?",
|
||||
"FEDERATEDLOGOUTENABLED": "Federované odhlášení povoleno",
|
||||
"FEDERATEDLOGOUTENABLED_DESC": "Je-li povoleno, uživatel bude odhlášen i z IdP, pokud ukončí relaci v ZITADEL.",
|
||||
"EMPTY": "Žádný IDP není dostupný",
|
||||
"OIDC": {
|
||||
"GENERAL": "Obecné informace",
|
||||
|
@@ -75,7 +75,8 @@
|
||||
"FLOWS": {
|
||||
"TITLE": "Flows",
|
||||
"DESCRIPTION": "Wähle einen Authentifizierungsflow und löse deine Aktionen bei einem spezifischen Ereignis innerhalb dieses Flows aus."
|
||||
}
|
||||
},
|
||||
"ACTIONSTWO_NOTE": "Actions V2, eine neue und verbesserte Version von Actions, ist jetzt verfügbar. Die aktuelle Version ist weiterhin zugänglich, aber unsere zukünftige Entwicklung wird sich auf die neue Version konzentrieren, die schließlich die aktuelle ersetzen wird."
|
||||
},
|
||||
"SETTINGS": {
|
||||
"INSTANCE": {
|
||||
@@ -529,13 +530,14 @@
|
||||
"APPLY": "Anwenden"
|
||||
},
|
||||
"ACTIONSTWO": {
|
||||
"BETA_NOTE": "Sie verwenden derzeit die neuen Actions V2, die sich in der Beta-Phase befinden. Version 1 ist weiterhin verfügbar, wird jedoch in Zukunft eingestellt. Bitte melden Sie Probleme oder Feedback.",
|
||||
"EXECUTION": {
|
||||
"TITLE": "Aktionen",
|
||||
"DESCRIPTION": "Aktionen ermöglichen es Ihnen, benutzerdefinierten Code als Reaktion auf API-Anfragen, Ereignisse oder bestimmte Funktionen auszuführen. Verwenden Sie sie, um Zitadel zu erweitern, Arbeitsabläufe zu automatisieren und sich in andere Systeme zu integrieren.",
|
||||
"TYPES": {
|
||||
"request": "Anfrage",
|
||||
"response": "Antwort",
|
||||
"events": "Ereignisse",
|
||||
"event": "Ereignisse",
|
||||
"function": "Funktion"
|
||||
},
|
||||
"DIALOG": {
|
||||
@@ -566,6 +568,7 @@
|
||||
"TITLE": "Alle",
|
||||
"DESCRIPTION": "Wählen Sie dies aus, wenn Sie Ihre Aktion bei jeder Anfrage ausführen möchten"
|
||||
},
|
||||
"ALL_EVENTS": "Wähle dies aus, wenn du deine Aktion bei jedem Ereignis ausführen möchtest",
|
||||
"SELECT_SERVICE": {
|
||||
"TITLE": "Dienst auswählen",
|
||||
"DESCRIPTION": "Wählen Sie einen Zitadel-Dienst für Ihre Aktion aus."
|
||||
@@ -619,6 +622,7 @@
|
||||
"restCall": "REST Aufruf",
|
||||
"restAsync": "REST Asynchron"
|
||||
},
|
||||
"TYPES_DESCRIPTION": "Webhook, der Aufruf verarbeitet den Statuscode, aber die Antwort ist irrelevant\nCall, der Aufruf verarbeitet den Statuscode und die Antwort\nAsync, der Aufruf verarbeitet weder Statuscode noch Antwort, kann aber parallel zu anderen Zielen aufgerufen werden",
|
||||
"ENDPOINT": "Endpunkt",
|
||||
"ENDPOINT_DESCRIPTION": "Geben Sie den Endpunkt ein, an dem Ihr Code gehostet wird. Stellen Sie sicher, dass er für uns zugänglich ist!",
|
||||
"TIMEOUT": "Timeout",
|
||||
@@ -689,6 +693,7 @@
|
||||
"EMAIL": "Email",
|
||||
"USERNAME": "Nutzername",
|
||||
"ORGNAME": "Organisationsname",
|
||||
"ORGID": "Organisations ID",
|
||||
"PRIMARYDOMAIN": "Primäre Domäne",
|
||||
"PROJECTNAME": "Projektname",
|
||||
"RESOURCEOWNER": "Ressourcenbesitzer",
|
||||
@@ -1508,7 +1513,8 @@
|
||||
"APPEARANCE": "Erscheinungsbild",
|
||||
"OTHER": "Anderes",
|
||||
"STORAGE": "Speicher"
|
||||
}
|
||||
},
|
||||
"BETA": "BETA"
|
||||
},
|
||||
"SETTING": {
|
||||
"LANGUAGES": {
|
||||
@@ -2145,6 +2151,7 @@
|
||||
"ACTIVATE": "Projekt aktivieren",
|
||||
"DELETE": "Projekt löschen",
|
||||
"ORGNAME": "Name der Organisation",
|
||||
"ORGID": "Organisations ID",
|
||||
"ORGDOMAIN": "Domain der Organisation",
|
||||
"STATE": "Status",
|
||||
"TYPE": "Typ",
|
||||
@@ -2334,6 +2341,8 @@
|
||||
"REMOVE_WARN_DESCRIPTION": "Sie sind dabei, einen Identitätsanbieter zu entfernen. Dadurch wird die Auswahl des verfügbaren IDP für Ihre Benutzer entfernt und bereits registrierte Benutzer können sich nicht erneut anmelden. Wollen Sie wirklich fortfahren?",
|
||||
"DELETE_SELECTION_TITLE": "Identitätsanbieter löschen",
|
||||
"DELETE_SELECTION_DESCRIPTION": "Sie sind im Begriff mehrere Identitätsanbieter zu löschen. Die dadurch hervorgerufenen Änderungen sind unwiderruflich. Wollen Sie dies wirklich tun?",
|
||||
"FEDERATEDLOGOUTENABLED": "Federated Logout aktiviert",
|
||||
"FEDERATEDLOGOUTENABLED_DESC": "Wenn aktiviert, wird der Benutzer auch vom IdP abgemeldet, wenn der Benutzer die Sitzung in ZITADEL beendet.",
|
||||
"EMPTY": "Kein IDP vorhanden",
|
||||
"OIDC": {
|
||||
"TITLE": "OIDC Konfiguration",
|
||||
|
@@ -75,7 +75,8 @@
|
||||
"FLOWS": {
|
||||
"TITLE": "Flows",
|
||||
"DESCRIPTION": "Choose an authentication flow and trigger your action on a specific event within this flow."
|
||||
}
|
||||
},
|
||||
"ACTIONSTWO_NOTE": "Actions V2 a new, improved version of Actions is now available. The current version is still accessible, but our future development will focus on the new one, which will eventually replace the current version."
|
||||
},
|
||||
"SETTINGS": {
|
||||
"INSTANCE": {
|
||||
@@ -529,13 +530,14 @@
|
||||
"APPLY": "Apply"
|
||||
},
|
||||
"ACTIONSTWO": {
|
||||
"BETA_NOTE": "You are currently using the new Actions V2, which is in beta. The previous Version 1 is still available but will be discontinued in the future. Please report any issues or feedback.",
|
||||
"EXECUTION": {
|
||||
"TITLE": "Actions",
|
||||
"DESCRIPTION": "Actions let you run custom code in response to API requests, events or specific functions. Use them to extend Zitadel, automate workflows, and itegrate with other systems.",
|
||||
"TYPES": {
|
||||
"request": "Request",
|
||||
"response": "Response",
|
||||
"events": "Events",
|
||||
"event": "Events",
|
||||
"function": "Function"
|
||||
},
|
||||
"DIALOG": {
|
||||
@@ -566,6 +568,7 @@
|
||||
"TITLE": "All",
|
||||
"DESCRIPTION": "Select this if you want to run your action on every request"
|
||||
},
|
||||
"ALL_EVENTS": "Select this if you want to run your action on every event",
|
||||
"SELECT_SERVICE": {
|
||||
"TITLE": "Select Service",
|
||||
"DESCRIPTION": "Choose a Zitadel Service for you action."
|
||||
@@ -619,6 +622,7 @@
|
||||
"restCall": "REST Call",
|
||||
"restAsync": "REST Async"
|
||||
},
|
||||
"TYPES_DESCRIPTION": "Webhook, the call handles the status code but response is irrelevant\nCall, the call handles the status code and response\nAsync, the call handles neither status code nor response, but can be called in parallel with other Targets",
|
||||
"ENDPOINT": "Endpoint",
|
||||
"ENDPOINT_DESCRIPTION": "Enter the endpoint where your code is hosted. Make sure it is accessible to us!",
|
||||
"TIMEOUT": "Timeout",
|
||||
@@ -684,12 +688,13 @@
|
||||
},
|
||||
"FILTER": {
|
||||
"TITLE": "Filter",
|
||||
"STATE": "Status",
|
||||
"ORGNAME": "Organization Name",
|
||||
"ORGID": "Organization ID",
|
||||
"STATE": "State",
|
||||
"PRIMARYDOMAIN": "Primary Domain",
|
||||
"DISPLAYNAME": "User Display Name",
|
||||
"EMAIL": "Email",
|
||||
"USERNAME": "User Name",
|
||||
"ORGNAME": "Organization Name",
|
||||
"PRIMARYDOMAIN": "Primary Domain",
|
||||
"PROJECTNAME": "Project Name",
|
||||
"RESOURCEOWNER": "Resource Owner",
|
||||
"METHODS": {
|
||||
@@ -1511,7 +1516,8 @@
|
||||
"OTHER": "Other",
|
||||
"STORAGE": "Storage",
|
||||
"ACTIONS": "Actions"
|
||||
}
|
||||
},
|
||||
"BETA": "BETA"
|
||||
},
|
||||
"SETTING": {
|
||||
"LANGUAGES": {
|
||||
@@ -2148,6 +2154,7 @@
|
||||
"ACTIVATE": "Activate Project",
|
||||
"DELETE": "Delete Project",
|
||||
"ORGNAME": "Organization Name",
|
||||
"ORGID": "Organization ID",
|
||||
"ORGDOMAIN": "Organization Domain",
|
||||
"STATE": "Status",
|
||||
"TYPE": "Type",
|
||||
@@ -2344,6 +2351,8 @@
|
||||
"REMOVE_WARN_DESCRIPTION": "You are about to remove an identity provider. This will remove the selection of the available IdP for your users and already registered users won't be able to login again. Are you sure to continue?",
|
||||
"DELETE_SELECTION_TITLE": "Delete IdP",
|
||||
"DELETE_SELECTION_DESCRIPTION": "You are about to delete an identity provider. The resulting changes are irrevocable. Do you really want to do this?",
|
||||
"FEDERATEDLOGOUTENABLED": "Federated Logout Enabled",
|
||||
"FEDERATEDLOGOUTENABLED_DESC": "If enabled, the user will be logged out from the IdP as well if the user terminates the session in ZITADEL.",
|
||||
"EMPTY": "No IdP available",
|
||||
"OIDC": {
|
||||
"GENERAL": "General Information",
|
||||
|
@@ -75,7 +75,8 @@
|
||||
"FLOWS": {
|
||||
"TITLE": "Flujos",
|
||||
"DESCRIPTION": "Elige un flujo de autenticación y activa tu acción en un evento específico dentro de este flujo."
|
||||
}
|
||||
},
|
||||
"ACTIONSTWO_NOTE": "Actions V2, una nueva y mejorada versión de Actions, ya está disponible. La versión actual sigue siendo accesible, pero nuestro desarrollo futuro se centrará en la nueva, que acabará reemplazando la versión actual."
|
||||
},
|
||||
"SETTINGS": {
|
||||
"INSTANCE": {
|
||||
@@ -529,13 +530,14 @@
|
||||
"APPLY": "Aplicar"
|
||||
},
|
||||
"ACTIONSTWO": {
|
||||
"BETA_NOTE": "Actualmente estás usando la nueva versión Actions V2, que está en fase beta. La versión anterior 1 todavía está disponible, pero será descontinuada en el futuro. Por favor, informa de cualquier problema o comentario.",
|
||||
"EXECUTION": {
|
||||
"TITLE": "Acciones",
|
||||
"DESCRIPTION": "Las acciones te permiten ejecutar código personalizado en respuesta a solicitudes de API, eventos o funciones específicas. Úsalas para extender Zitadel, automatizar flujos de trabajo e integrarte con otros sistemas.",
|
||||
"TYPES": {
|
||||
"request": "Solicitud",
|
||||
"response": "Respuesta",
|
||||
"events": "Eventos",
|
||||
"event": "Eventos",
|
||||
"function": "Función"
|
||||
},
|
||||
"DIALOG": {
|
||||
@@ -566,6 +568,7 @@
|
||||
"TITLE": "Todas",
|
||||
"DESCRIPTION": "Selecciona esto si quieres ejecutar tu acción en cada solicitud"
|
||||
},
|
||||
"ALL_EVENTS": "Selecciona esto si quieres ejecutar tu acción en cada evento",
|
||||
"SELECT_SERVICE": {
|
||||
"TITLE": "Seleccionar servicio",
|
||||
"DESCRIPTION": "Elige un servicio de Zitadel para tu acción."
|
||||
@@ -619,6 +622,7 @@
|
||||
"restCall": "Llamada REST",
|
||||
"restAsync": "REST Asíncrono"
|
||||
},
|
||||
"TYPES_DESCRIPTION": "Webhook, la llamada maneja el código de estado pero la respuesta es irrelevante\nCall, la llamada maneja el código de estado y la respuesta\nAsync, la llamada no maneja ni el código de estado ni la respuesta, pero puede ser llamada en paralelo con otros objetivos",
|
||||
"ENDPOINT": "Punto de conexión",
|
||||
"ENDPOINT_DESCRIPTION": "Introduce el punto de conexión donde se aloja tu código. ¡Asegúrate de que sea accesible para nosotros!",
|
||||
"TIMEOUT": "Tiempo de espera",
|
||||
@@ -689,6 +693,7 @@
|
||||
"EMAIL": "Email",
|
||||
"USERNAME": "Nombre de usuario",
|
||||
"ORGNAME": "Nombre de organización",
|
||||
"ORGID": "ID de organización",
|
||||
"PRIMARYDOMAIN": "Dominio primario",
|
||||
"PROJECTNAME": "Nombre de proyecto",
|
||||
"RESOURCEOWNER": "Propietario del recurso",
|
||||
@@ -1509,7 +1514,8 @@
|
||||
"APPEARANCE": "Apariencia",
|
||||
"OTHER": "Otros",
|
||||
"STORAGE": "Datos"
|
||||
}
|
||||
},
|
||||
"BETA": "BETA"
|
||||
},
|
||||
"SETTING": {
|
||||
"LANGUAGES": {
|
||||
@@ -2146,6 +2152,7 @@
|
||||
"ACTIVATE": "Activar proyecto",
|
||||
"DELETE": "Borrar proyecto",
|
||||
"ORGNAME": "Nombre de organización",
|
||||
"ORGID": "ID de organización",
|
||||
"ORGDOMAIN": "Dominio de organización",
|
||||
"STATE": "Estado",
|
||||
"TYPE": "Tipo",
|
||||
@@ -2334,6 +2341,8 @@
|
||||
"REMOVE_WARN_DESCRIPTION": "Está a punto de eliminar un proveedor de identidad. Esto eliminará la selección del IDP disponible para sus usuarios y los usuarios ya registrados no podrán volver a iniciar sesión. ¿Estás seguro de continuar?",
|
||||
"DELETE_SELECTION_TITLE": "Borrar IDP",
|
||||
"DELETE_SELECTION_DESCRIPTION": "Estás a punto de borrar un proveedor de identidad. Los cambios resultantes son irrevocables. ¿Estás seguro de que quieres hacer esto?",
|
||||
"FEDERATEDLOGOUTENABLED": "Cierre de sesión federado habilitado",
|
||||
"FEDERATEDLOGOUTENABLED_DESC": "Si está habilitado, el usuario también será desconectado del IdP si finaliza la sesión en ZITADEL.",
|
||||
"EMPTY": "No hay IDP disponible",
|
||||
"OIDC": {
|
||||
"GENERAL": "Información general",
|
||||
|
@@ -75,7 +75,8 @@
|
||||
"FLOWS": {
|
||||
"TITLE": "Flux",
|
||||
"DESCRIPTION": "Choisissez un flux d'authentification et déclenchez votre action sur un événement spécifique dans ce flux."
|
||||
}
|
||||
},
|
||||
"ACTIONSTWO_NOTE": "Actions V2, une nouvelle version améliorée de Actions, est désormais disponible. La version actuelle reste accessible, mais notre développement futur se concentrera sur la nouvelle, qui finira par remplacer la version actuelle."
|
||||
},
|
||||
"SETTINGS": {
|
||||
"INSTANCE": {
|
||||
@@ -529,13 +530,14 @@
|
||||
"APPLY": "Appliquer"
|
||||
},
|
||||
"ACTIONSTWO": {
|
||||
"BETA_NOTE": "Vous utilisez actuellement la nouvelle version Actions V2, qui est en phase bêta. L'ancienne version 1 est toujours disponible mais sera arrêtée à l'avenir. Veuillez signaler tout problème ou commentaire.",
|
||||
"EXECUTION": {
|
||||
"TITLE": "Actions",
|
||||
"DESCRIPTION": "Les actions vous permettent d'exécuter du code personnalisé en réponse à des requêtes API, des événements ou des fonctions spécifiques. Utilisez-les pour étendre Zitadel, automatiser les flux de travail et vous intégrer à d'autres systèmes.",
|
||||
"TYPES": {
|
||||
"request": "Requête",
|
||||
"response": "Réponse",
|
||||
"events": "Événements",
|
||||
"event": "Événements",
|
||||
"function": "Fonction"
|
||||
},
|
||||
"DIALOG": {
|
||||
@@ -566,6 +568,7 @@
|
||||
"TITLE": "Tous",
|
||||
"DESCRIPTION": "Sélectionnez ceci si vous souhaitez exécuter votre action sur chaque requête"
|
||||
},
|
||||
"ALL_EVENTS": "Sélectionnez ceci si vous souhaitez exécuter votre action à chaque événement",
|
||||
"SELECT_SERVICE": {
|
||||
"TITLE": "Sélectionner un service",
|
||||
"DESCRIPTION": "Choisissez un service Zitadel pour votre action."
|
||||
@@ -619,6 +622,7 @@
|
||||
"restCall": "Appel REST",
|
||||
"restAsync": "REST Asynchrone"
|
||||
},
|
||||
"TYPES_DESCRIPTION": "Webhook, l'appel gère le code d'état mais la réponse est sans importance\nCall, l'appel gère le code d'état et la réponse\nAsync, l'appel ne gère ni le code d'état ni la réponse, mais peut être appelé en parallèle avec d'autres cibles",
|
||||
"ENDPOINT": "Point de terminaison",
|
||||
"ENDPOINT_DESCRIPTION": "Entrez le point de terminaison où votre code est hébergé. Assurez-vous qu'il nous est accessible !",
|
||||
"TIMEOUT": "Délai d'attente",
|
||||
@@ -689,6 +693,7 @@
|
||||
"EMAIL": "Courriel",
|
||||
"USERNAME": "Nom de l'utilisateur",
|
||||
"ORGNAME": "Nom de l'organisation",
|
||||
"ORGID": "ID de l'organisation",
|
||||
"PRIMARYDOMAIN": "Domaine principal",
|
||||
"PROJECTNAME": "Nom du projet",
|
||||
"RESOURCEOWNER": "Propriétaire des ressources",
|
||||
@@ -1508,7 +1513,8 @@
|
||||
"APPEARANCE": "Apparence",
|
||||
"OTHER": "Autres",
|
||||
"STORAGE": "Stockage"
|
||||
}
|
||||
},
|
||||
"BETA": "BÊTA"
|
||||
},
|
||||
"SETTING": {
|
||||
"LANGUAGES": {
|
||||
@@ -2145,6 +2151,7 @@
|
||||
"ACTIVATE": "Activer le projet",
|
||||
"DELETE": "Supprimer le projet",
|
||||
"ORGNAME": "Nom de l'organisation",
|
||||
"ORGID": "ID de l'organisation",
|
||||
"ORGDOMAIN": "Domaine de l'organisation",
|
||||
"STATE": "Statut",
|
||||
"TYPE": "Type",
|
||||
@@ -2338,6 +2345,8 @@
|
||||
"REMOVE_WARN_DESCRIPTION": "Vous êtes sur le point de supprimer un fournisseur d'identité. Cela supprimera la sélection de l'IDP disponible pour vos utilisateurs et les utilisateurs déjà enregistrés ne pourront plus se reconnecter. Êtes-vous sûr de continuer ?",
|
||||
"DELETE_SELECTION_TITLE": "Supprimer Idp",
|
||||
"DELETE_SELECTION_DESCRIPTION": "Vous êtes sur le point de supprimer un fournisseur d'identité. Les changements qui en résultent sont irrévocables. Voulez-vous vraiment le faire ?",
|
||||
"FEDERATEDLOGOUTENABLED": "Déconnexion fédérée activée",
|
||||
"FEDERATEDLOGOUTENABLED_DESC": "Si activé, l'utilisateur sera également déconnecté de l'IdP s'il termine la session dans ZITADEL.",
|
||||
"EMPTY": "Aucun IDP disponible",
|
||||
"OIDC": {
|
||||
"GENERAL": "Informations générales",
|
||||
|
@@ -75,7 +75,8 @@
|
||||
"FLOWS": {
|
||||
"TITLE": "Folyamatok",
|
||||
"DESCRIPTION": "Válassz egy hitelesítési folyamatot, és váltasd ki a műveletedet egy adott esemény bekövetkezésekor ebben a folyamatban."
|
||||
}
|
||||
},
|
||||
"ACTIONSTWO_NOTE": "Az Actions V2, az Actions új, továbbfejlesztett verziója mostantól elérhető. A jelenlegi verzió továbbra is elérhető, de a jövőbeli fejlesztéseink az új verzióra összpontosítanak, amely végül felváltja a jelenlegi verziót."
|
||||
},
|
||||
"SETTINGS": {
|
||||
"INSTANCE": {
|
||||
@@ -529,13 +530,14 @@
|
||||
"APPLY": "Alkalmaz"
|
||||
},
|
||||
"ACTIONSTWO": {
|
||||
"BETA_NOTE": "Jelenleg az új Actions V2-t használja, amely béta verzióban van. Az előző 1-es verzió továbbra is elérhető, de a jövőben megszűnik. Kérjük, jelezze az esetleges problémákat vagy visszajelzéseit.",
|
||||
"EXECUTION": {
|
||||
"TITLE": "Műveletek",
|
||||
"DESCRIPTION": "A műveletek lehetővé teszik egyedi kód futtatását API-kérésekre, eseményekre vagy konkrét függvényekre válaszul. Használja őket a Zitadel kiterjesztéséhez, a munkafolyamatok automatizálásához és más rendszerekkel való integrációhoz.",
|
||||
"TYPES": {
|
||||
"request": "Kérés",
|
||||
"response": "Válasz",
|
||||
"events": "Események",
|
||||
"event": "Események",
|
||||
"function": "Függvény"
|
||||
},
|
||||
"DIALOG": {
|
||||
@@ -566,6 +568,7 @@
|
||||
"TITLE": "Összes",
|
||||
"DESCRIPTION": "Válassza ezt, ha minden kérésnél futtatni szeretné a műveletet"
|
||||
},
|
||||
"ALL_EVENTS": "Válaszd ezt, ha minden eseménynél futtatni szeretnéd a műveletet",
|
||||
"SELECT_SERVICE": {
|
||||
"TITLE": "Szolgáltatás kiválasztása",
|
||||
"DESCRIPTION": "Válasszon egy Zitadel szolgáltatást a művelethez."
|
||||
@@ -619,6 +622,7 @@
|
||||
"restCall": "REST Hívás",
|
||||
"restAsync": "REST Aszinkron"
|
||||
},
|
||||
"TYPES_DESCRIPTION": "Webhook, a hívás kezeli az állapotkódot, de a válasz lényegtelen\nCall, a hívás kezeli az állapotkódot és a választ\nAsync, a hívás sem az állapotkódot, sem a választ nem kezeli, de párhuzamosan hívható más célokkal",
|
||||
"ENDPOINT": "Végpont",
|
||||
"ENDPOINT_DESCRIPTION": "Adja meg azt a végpontot, ahol a kódja található. Győződjön meg arról, hogy elérhető számunkra!",
|
||||
"TIMEOUT": "Időtúllépés",
|
||||
@@ -689,6 +693,7 @@
|
||||
"EMAIL": "E-mail",
|
||||
"USERNAME": "Felhasználói Név",
|
||||
"ORGNAME": "Szervezet Neve",
|
||||
"ORGID": "Szervezet ID",
|
||||
"PRIMARYDOMAIN": "Elsődleges Domain",
|
||||
"PROJECTNAME": "Projekt Neve",
|
||||
"RESOURCEOWNER": "Erőforrás Tulajdonos",
|
||||
@@ -1508,7 +1513,8 @@
|
||||
"APPEARANCE": "Megjelenés",
|
||||
"OTHER": "Egyéb",
|
||||
"STORAGE": "Tárolás"
|
||||
}
|
||||
},
|
||||
"BETA": "BÉTA"
|
||||
},
|
||||
"SETTING": {
|
||||
"LANGUAGES": {
|
||||
@@ -2143,6 +2149,7 @@
|
||||
"ACTIVATE": "Projekt aktiválása",
|
||||
"DELETE": "Projekt törlése",
|
||||
"ORGNAME": "Szervezet neve",
|
||||
"ORGID": "Szervezet ID",
|
||||
"ORGDOMAIN": "Szervezet domainje",
|
||||
"STATE": "Státusz",
|
||||
"TYPE": "Típus",
|
||||
@@ -2339,6 +2346,8 @@
|
||||
"REMOVE_WARN_DESCRIPTION": "Egy identity providert készülsz eltávolítani. Ez eltávolítja a felhasználóid számára elérhető IDP kiválasztását, és a már regisztrált felhasználók nem tudnak újra bejelentkezni. Biztosan folytatod?",
|
||||
"DELETE_SELECTION_TITLE": "IDP törlése",
|
||||
"DELETE_SELECTION_DESCRIPTION": "Egy identity providert készülsz törölni. A változások visszafordíthatatlanok. Biztosan folytatni akarod?",
|
||||
"FEDERATEDLOGOUTENABLED": "Szövetséges kijelentkezés engedélyezve",
|
||||
"FEDERATEDLOGOUTENABLED_DESC": "Ha engedélyezve van, a felhasználó ki lesz jelentkezve az IdP-ből is, ha a felhasználó megszakítja a munkamenetet a ZITADEL-ben.",
|
||||
"EMPTY": "Nincs elérhető IDP",
|
||||
"OIDC": {
|
||||
"GENERAL": "Általános információk",
|
||||
|
@@ -69,7 +69,8 @@
|
||||
"FLOWS": {
|
||||
"TITLE": "Mengalir",
|
||||
"DESCRIPTION": "Pilih alur autentikasi dan picu tindakan Anda pada peristiwa tertentu dalam alur ini."
|
||||
}
|
||||
},
|
||||
"ACTIONSTWO_NOTE": "Actions V2, versi baru dan lebih baik dari Actions, sekarang tersedia. Versi saat ini masih dapat diakses, tetapi pengembangan di masa depan akan difokuskan pada versi baru ini yang pada akhirnya akan menggantikan versi saat ini."
|
||||
},
|
||||
"SETTINGS": {
|
||||
"INSTANCE": {
|
||||
@@ -496,13 +497,14 @@
|
||||
"APPLY": "Menerapkan"
|
||||
},
|
||||
"ACTIONSTWO": {
|
||||
"BETA_NOTE": "Anda saat ini menggunakan Actions V2 baru, yang masih dalam versi beta. Versi sebelumnya, Versi 1, masih tersedia tetapi akan dihentikan di masa depan. Silakan laporkan masalah atau berikan masukan.",
|
||||
"EXECUTION": {
|
||||
"TITLE": "Tindakan",
|
||||
"DESCRIPTION": "Tindakan memungkinkan Anda menjalankan kode khusus sebagai respons terhadap permintaan API, peristiwa, atau fungsi tertentu. Gunakan ini untuk memperluas Zitadel, mengotomatiskan alur kerja, dan berintegrasi dengan sistem lain.",
|
||||
"TYPES": {
|
||||
"request": "Permintaan",
|
||||
"response": "Respons",
|
||||
"events": "Peristiwa",
|
||||
"event": "Peristiwa",
|
||||
"function": "Fungsi"
|
||||
},
|
||||
"DIALOG": {
|
||||
@@ -533,6 +535,7 @@
|
||||
"TITLE": "Semua",
|
||||
"DESCRIPTION": "Pilih ini jika Anda ingin menjalankan tindakan Anda pada setiap permintaan"
|
||||
},
|
||||
"ALL_EVENTS": "Pilih ini jika Anda ingin menjalankan aksi Anda pada setiap peristiwa",
|
||||
"SELECT_SERVICE": {
|
||||
"TITLE": "Pilih Layanan",
|
||||
"DESCRIPTION": "Pilih Layanan Zitadel untuk tindakan Anda."
|
||||
@@ -586,6 +589,7 @@
|
||||
"restCall": "Panggilan REST",
|
||||
"restAsync": "REST Asinkron"
|
||||
},
|
||||
"TYPES_DESCRIPTION": "Webhook, panggilan menangani kode status tetapi respons tidak relevan\nCall, panggilan menangani kode status dan respons\nAsync, panggilan tidak menangani kode status maupun respons, tetapi dapat dipanggil secara paralel dengan Target lain",
|
||||
"ENDPOINT": "Titik Akhir",
|
||||
"ENDPOINT_DESCRIPTION": "Masukkan titik akhir tempat kode Anda dihosting. Pastikan dapat diakses oleh kami!",
|
||||
"TIMEOUT": "Batas Waktu",
|
||||
@@ -648,6 +652,7 @@
|
||||
"EMAIL": "E-mail",
|
||||
"USERNAME": "Nama belakang",
|
||||
"ORGNAME": "Nama Organisasi",
|
||||
"ORGID": "ID Organisasi",
|
||||
"PRIMARYDOMAIN": "Domain Utama",
|
||||
"PROJECTNAME": "Nama Proyek",
|
||||
"RESOURCEOWNER": "Pemilik Sumber Daya",
|
||||
@@ -1386,7 +1391,8 @@
|
||||
"APPEARANCE": "Penampilan",
|
||||
"OTHER": "Lainnya",
|
||||
"STORAGE": "Penyimpanan"
|
||||
}
|
||||
},
|
||||
"BETA": "BETA"
|
||||
},
|
||||
"SETTING": {
|
||||
"LANGUAGES": {
|
||||
@@ -1975,6 +1981,7 @@
|
||||
"ACTIVATE": "Aktifkan Proyek",
|
||||
"DELETE": "Hapus Proyek",
|
||||
"ORGNAME": "Nama Organisasi",
|
||||
"ORGID": "ID Organisasi",
|
||||
"ORGDOMAIN": "Domain Organisasi",
|
||||
"STATE": "Status",
|
||||
"TYPE": "Jenis",
|
||||
@@ -2121,6 +2128,8 @@
|
||||
"REMOVE_WARN_DESCRIPTION": "Anda akan menghapus penyedia identitas. Ini akan menghapus pilihan IDP yang tersedia untuk pengguna Anda dan pengguna yang sudah terdaftar tidak akan bisa masuk lagi. Apakah Anda yakin untuk melanjutkan?",
|
||||
"DELETE_SELECTION_TITLE": "Hapus IDP",
|
||||
"DELETE_SELECTION_DESCRIPTION": "Anda akan menghapus penyedia identitas. Perubahan yang dihasilkan tidak dapat dibatalkan. Apakah Anda benar-benar ingin melakukan ini?",
|
||||
"FEDERATEDLOGOUTENABLED": "Logout Federasi Diaktifkan",
|
||||
"FEDERATEDLOGOUTENABLED_DESC": "Jika diaktifkan, pengguna juga akan keluar dari IdP jika pengguna mengakhiri sesi di ZITADEL.",
|
||||
"EMPTY": "Tidak ada pengungsi yang tersedia",
|
||||
"OIDC": {
|
||||
"GENERAL": "Informasi Umum",
|
||||
|
@@ -75,7 +75,8 @@
|
||||
"FLOWS": {
|
||||
"TITLE": "Flussi",
|
||||
"DESCRIPTION": "Scegli un flusso di autenticazione e attiva la tua azione su un evento specifico all'interno di questo flusso."
|
||||
}
|
||||
},
|
||||
"ACTIONSTWO_NOTE": "Actions V2, una nuova versione migliorata di Actions, è ora disponibile. La versione attuale è ancora accessibile, ma i futuri sviluppi si concentreranno su quella nuova, che alla fine sostituirà la versione corrente."
|
||||
},
|
||||
"SETTINGS": {
|
||||
"INSTANCE": {
|
||||
@@ -528,13 +529,14 @@
|
||||
"APPLY": "Applicare"
|
||||
},
|
||||
"ACTIONSTWO": {
|
||||
"BETA_NOTE": "Stai attualmente utilizzando la nuova versione Actions V2, che è in beta. La precedente Versione 1 è ancora disponibile, ma sarà dismessa in futuro. Ti preghiamo di segnalare eventuali problemi o feedback.",
|
||||
"EXECUTION": {
|
||||
"TITLE": "Azioni",
|
||||
"DESCRIPTION": "Le azioni consentono di eseguire codice personalizzato in risposta a richieste API, eventi o funzioni specifiche. Usale per estendere Zitadel, automatizzare i flussi di lavoro e integrarti con altri sistemi.",
|
||||
"TYPES": {
|
||||
"request": "Richiesta",
|
||||
"response": "Risposta",
|
||||
"events": "Eventi",
|
||||
"event": "Eventi",
|
||||
"function": "Funzione"
|
||||
},
|
||||
"DIALOG": {
|
||||
@@ -565,6 +567,7 @@
|
||||
"TITLE": "Tutte",
|
||||
"DESCRIPTION": "Seleziona questa opzione se vuoi eseguire la tua azione su ogni richiesta"
|
||||
},
|
||||
"ALL_EVENTS": "Seleziona questo se vuoi eseguire la tua azione a ogni evento",
|
||||
"SELECT_SERVICE": {
|
||||
"TITLE": "Seleziona servizio",
|
||||
"DESCRIPTION": "Scegli un servizio Zitadel per la tua azione."
|
||||
@@ -618,6 +621,7 @@
|
||||
"restCall": "Chiamata REST",
|
||||
"restAsync": "REST Asincrono"
|
||||
},
|
||||
"TYPES_DESCRIPTION": "Webhook, la chiamata gestisce il codice di stato ma la risposta è irrilevante\nCall, la chiamata gestisce il codice di stato e la risposta\nAsync, la chiamata non gestisce né il codice di stato né la risposta, ma può essere eseguita in parallelo con altri obiettivi",
|
||||
"ENDPOINT": "Endpoint",
|
||||
"ENDPOINT_DESCRIPTION": "Inserisci l'endpoint in cui è ospitato il tuo codice. Assicurati che sia accessibile per noi!",
|
||||
"TIMEOUT": "Timeout",
|
||||
@@ -688,6 +692,7 @@
|
||||
"EMAIL": "Email",
|
||||
"USERNAME": "User Name",
|
||||
"ORGNAME": "Nome organizzazione",
|
||||
"ORGID": "ID organizzazione",
|
||||
"PRIMARYDOMAIN": "Dominio primario",
|
||||
"PROJECTNAME": "Nome del progetto",
|
||||
"RESOURCEOWNER": "Resource Owner",
|
||||
@@ -1508,7 +1513,8 @@
|
||||
"APPEARANCE": "Aspetto",
|
||||
"OTHER": "Altro",
|
||||
"STORAGE": "Dati"
|
||||
}
|
||||
},
|
||||
"BETA": "BETA"
|
||||
},
|
||||
"SETTING": {
|
||||
"LANGUAGES": {
|
||||
@@ -2145,6 +2151,7 @@
|
||||
"ACTIVATE": "Attiva progetto",
|
||||
"DELETE": "Rimuovi progetto",
|
||||
"ORGNAME": "Nome dell'organizzazione",
|
||||
"ORGID": "ID organizzazione",
|
||||
"ORGDOMAIN": "Dominio",
|
||||
"STATE": "Stato",
|
||||
"TYPE": "Tipo",
|
||||
@@ -2338,6 +2345,8 @@
|
||||
"REMOVE_WARN_DESCRIPTION": "Stai per rimuovere un provider di identità. Questo rimuoverà la selezione dell'IDP disponibile per i tuoi utenti e gli utenti già registrati non potranno accedere nuovamente. Sei sicuro di continuare?",
|
||||
"DELETE_SELECTION_TITLE": "Rimuovere IDP",
|
||||
"DELETE_SELECTION_DESCRIPTION": "Stai per rimuovere un fornitore di identità. I cambiamenti risultanti sono irrevocabili. Vuoi davvero farlo?",
|
||||
"FEDERATEDLOGOUTENABLED": "Logout Federato Abilitato",
|
||||
"FEDERATEDLOGOUTENABLED_DESC": "Se abilitato, l'utente verrà disconnesso anche dall'IdP se termina la sessione in ZITADEL.",
|
||||
"EMPTY": "Nessun IDP disponible",
|
||||
"OIDC": {
|
||||
"GENERAL": "Informazioni generali",
|
||||
|
@@ -75,7 +75,8 @@
|
||||
"FLOWS": {
|
||||
"TITLE": "フロー",
|
||||
"DESCRIPTION": "認証フローを選択し、そのフロー内の特定のイベントでアクションをトリガーします。"
|
||||
}
|
||||
},
|
||||
"ACTIONSTWO_NOTE": "Actions V2(アクションズV2)、改善された新しいバージョンが利用可能になりました。現在のバージョンも引き続き利用可能ですが、今後の開発は新バージョンに集中し、最終的には現在のバージョンを置き換える予定です。"
|
||||
},
|
||||
"SETTINGS": {
|
||||
"INSTANCE": {
|
||||
@@ -529,13 +530,14 @@
|
||||
"APPLY": "アプライ"
|
||||
},
|
||||
"ACTIONSTWO": {
|
||||
"BETA_NOTE": "現在、新しいActions V2(ベータ版)を使用しています。以前のバージョン1はまだ利用可能ですが、今後廃止される予定です。問題やフィードバックがあればお知らせください。",
|
||||
"EXECUTION": {
|
||||
"TITLE": "アクション",
|
||||
"DESCRIPTION": "アクションを使用すると、APIリクエスト、イベント、または特定の関数に応答してカスタムコードを実行できます。これらを使用して、Zitadelを拡張し、ワークフローを自動化し、他のシステムと統合します。",
|
||||
"TYPES": {
|
||||
"request": "リクエスト",
|
||||
"response": "レスポンス",
|
||||
"events": "イベント",
|
||||
"event": "イベント",
|
||||
"function": "関数"
|
||||
},
|
||||
"DIALOG": {
|
||||
@@ -566,6 +568,7 @@
|
||||
"TITLE": "すべて",
|
||||
"DESCRIPTION": "すべてのリクエストでアクションを実行する場合は、これを選択します"
|
||||
},
|
||||
"ALL_EVENTS": "すべてのイベントでアクションを実行する場合はこれを選択してください",
|
||||
"SELECT_SERVICE": {
|
||||
"TITLE": "サービスを選択",
|
||||
"DESCRIPTION": "アクションのZitadelサービスを選択します。"
|
||||
@@ -619,6 +622,7 @@
|
||||
"restCall": "REST 呼び出し",
|
||||
"restAsync": "REST 非同期"
|
||||
},
|
||||
"TYPES_DESCRIPTION": "Webhook、呼び出しはステータスコードを処理しますが、応答は無関係です\nCall、呼び出しはステータスコードと応答を処理します\nAsync、呼び出しはステータスコードも応答も処理しませんが、他のターゲットと並行して呼び出すことができます",
|
||||
"ENDPOINT": "エンドポイント",
|
||||
"ENDPOINT_DESCRIPTION": "コードがホストされているエンドポイントを入力します。アクセス可能であることを確認してください。",
|
||||
"TIMEOUT": "タイムアウト",
|
||||
@@ -689,6 +693,7 @@
|
||||
"EMAIL": "Eメール",
|
||||
"USERNAME": "ユーザー名",
|
||||
"ORGNAME": "組織名",
|
||||
"ORGID": "組織ID",
|
||||
"PRIMARYDOMAIN": "プライマリドメイン",
|
||||
"PROJECTNAME": "プロジェクト名",
|
||||
"RESOURCEOWNER": "リソース所有者",
|
||||
@@ -1508,7 +1513,8 @@
|
||||
"APPEARANCE": "設定",
|
||||
"OTHER": "その他",
|
||||
"STORAGE": "ストレージ"
|
||||
}
|
||||
},
|
||||
"BETA": "ベータ"
|
||||
},
|
||||
"SETTING": {
|
||||
"LANGUAGES": {
|
||||
@@ -2145,6 +2151,7 @@
|
||||
"ACTIVATE": "プロジェクトのアクティブ化",
|
||||
"DELETE": "プロジェクトの削除",
|
||||
"ORGNAME": "組織名",
|
||||
"ORGID": "組織ID",
|
||||
"ORGDOMAIN": "組織ドメイン",
|
||||
"STATE": "ステータス",
|
||||
"TYPE": "タイプ",
|
||||
@@ -2341,6 +2348,8 @@
|
||||
"REMOVE_WARN_DESCRIPTION": "ID プロバイダーを削除しようとしています。これにより、ユーザーが使用できる IDP の選択が削除され、すでに登録されているユーザーは再度ログインできなくなります。続けてもよろしいですか?",
|
||||
"DELETE_SELECTION_TITLE": "IDPの削除",
|
||||
"DELETE_SELECTION_DESCRIPTION": "IDプロバイダーを削除しようとしています。変更は取消できません。本当によろしいですか?",
|
||||
"FEDERATEDLOGOUTENABLED": "フェデレーションログアウト有効",
|
||||
"FEDERATEDLOGOUTENABLED_DESC": "有効にすると、ユーザーがZITADELでセッションを終了した場合、IdPからもログアウトされます。",
|
||||
"EMPTY": "IDPは利用できません",
|
||||
"OIDC": {
|
||||
"GENERAL": "一般",
|
||||
|
@@ -75,7 +75,8 @@
|
||||
"FLOWS": {
|
||||
"TITLE": "플로우",
|
||||
"DESCRIPTION": "인증 플로우를 선택하고 이 플로우 내의 특정 이벤트에서 작업을 트리거하세요."
|
||||
}
|
||||
},
|
||||
"ACTIONSTWO_NOTE": "Actions V2, 개선된 새로운 버전이 출시되었습니다. 현재 버전은 여전히 접근할 수 있지만, 앞으로의 개발은 새로운 버전에 집중될 것이며, 결국 현재 버전을 대체할 것입니다."
|
||||
},
|
||||
"SETTINGS": {
|
||||
"INSTANCE": {
|
||||
@@ -529,13 +530,14 @@
|
||||
"APPLY": "적용"
|
||||
},
|
||||
"ACTIONSTWO": {
|
||||
"BETA_NOTE": "현재 베타 버전인 새로운 Actions V2를 사용하고 있습니다. 이전 버전 1은 여전히 사용 가능하지만, 향후 중단될 예정입니다. 문제나 피드백이 있으면 알려주세요.",
|
||||
"EXECUTION": {
|
||||
"TITLE": "작업",
|
||||
"DESCRIPTION": "작업을 통해 API 요청, 이벤트 또는 특정 함수에 대한 응답으로 사용자 지정 코드를 실행할 수 있습니다. 이를 사용하여 Zitadel을 확장하고 워크플로를 자동화하며 다른 시스템과 통합합니다.",
|
||||
"TYPES": {
|
||||
"request": "요청",
|
||||
"response": "응답",
|
||||
"events": "이벤트",
|
||||
"event": "이벤트",
|
||||
"function": "함수"
|
||||
},
|
||||
"DIALOG": {
|
||||
@@ -566,6 +568,7 @@
|
||||
"TITLE": "모두",
|
||||
"DESCRIPTION": "모든 요청에서 작업을 실행하려면 이것을 선택하십시오."
|
||||
},
|
||||
"ALL_EVENTS": "모든 이벤트에서 작업을 실행하려면 이 항목을 선택하세요",
|
||||
"SELECT_SERVICE": {
|
||||
"TITLE": "서비스 선택",
|
||||
"DESCRIPTION": "작업에 대한 Zitadel 서비스를 선택하십시오."
|
||||
@@ -619,6 +622,7 @@
|
||||
"restCall": "REST 호출",
|
||||
"restAsync": "REST 비동기"
|
||||
},
|
||||
"TYPES_DESCRIPTION": "Webhook, 호출은 상태 코드를 처리하지만 응답은 중요하지 않습니다\nCall, 호출은 상태 코드와 응답을 처리합니다\nAsync, 호출은 상태 코드나 응답을 처리하지 않지만 다른 대상과 병렬로 호출할 수 있습니다",
|
||||
"ENDPOINT": "엔드포인트",
|
||||
"ENDPOINT_DESCRIPTION": "코드가 호스팅되는 엔드포인트를 입력하십시오. 우리에게 액세스할 수 있는지 확인하십시오!",
|
||||
"TIMEOUT": "시간 초과",
|
||||
@@ -689,6 +693,7 @@
|
||||
"EMAIL": "이메일",
|
||||
"USERNAME": "사용자 이름",
|
||||
"ORGNAME": "조직 이름",
|
||||
"ORGID": "조직 ID",
|
||||
"PRIMARYDOMAIN": "기본 도메인",
|
||||
"PROJECTNAME": "프로젝트 이름",
|
||||
"RESOURCEOWNER": "리소스 소유자",
|
||||
@@ -1508,7 +1513,8 @@
|
||||
"APPEARANCE": "외형",
|
||||
"OTHER": "기타",
|
||||
"STORAGE": "저장소"
|
||||
}
|
||||
},
|
||||
"BETA": "베타"
|
||||
},
|
||||
"SETTING": {
|
||||
"LANGUAGES": {
|
||||
@@ -2145,6 +2151,7 @@
|
||||
"ACTIVATE": "프로젝트 활성화",
|
||||
"DELETE": "프로젝트 삭제",
|
||||
"ORGNAME": "조직 이름",
|
||||
"ORGID": "조직 ID",
|
||||
"ORGDOMAIN": "조직 도메인",
|
||||
"STATE": "상태",
|
||||
"TYPE": "유형",
|
||||
@@ -2341,6 +2348,8 @@
|
||||
"REMOVE_WARN_DESCRIPTION": "ID 제공자를 제거하려고 합니다. 사용자가 선택할 수 있는 ID 제공자가 제거되며, 이미 등록된 사용자는 다시 로그인할 수 없습니다. 계속하시겠습니까?",
|
||||
"DELETE_SELECTION_TITLE": "ID 제공자 삭제",
|
||||
"DELETE_SELECTION_DESCRIPTION": "ID 제공자를 삭제하려고 합니다. 이 변경 사항은 되돌릴 수 없습니다. 정말로 삭제하시겠습니까?",
|
||||
"FEDERATEDLOGOUTENABLED": "연합 로그아웃 활성화됨",
|
||||
"FEDERATEDLOGOUTENABLED_DESC": "활성화되면 사용자가 ZITADEL에서 세션을 종료할 경우 IdP에서도 로그아웃됩니다.",
|
||||
"EMPTY": "사용 가능한 ID 제공자가 없습니다.",
|
||||
"OIDC": {
|
||||
"GENERAL": "일반 정보",
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user