Merge branch 'main' into clean-transactional-propsal

This commit is contained in:
Iraq Jaber
2025-06-13 15:05:33 +02:00
538 changed files with 36271 additions and 6036 deletions

View File

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

View File

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

View File

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

File diff suppressed because one or more lines are too long

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

@@ -0,0 +1,27 @@
package setup
import (
"context"
_ "embed"
"github.com/zitadel/zitadel/internal/database"
"github.com/zitadel/zitadel/internal/eventstore"
)
var (
//go:embed 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
View File

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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"

View File

@@ -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: {

View File

@@ -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();
});

View File

@@ -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();
});

View File

@@ -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 {

View File

@@ -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)),
};
});
})

View File

@@ -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,

View File

@@ -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

View File

@@ -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 {

View File

@@ -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();
});
});

View File

@@ -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));
}
}

View File

@@ -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: {

View File

@@ -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();
});

View File

@@ -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();
});

View File

@@ -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 });

View File

@@ -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);

View File

@@ -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);

View File

@@ -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({});
}

View File

@@ -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();
}
});
},
});
}
}

View File

@@ -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",

View File

@@ -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",

View File

@@ -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",

View File

@@ -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",

View File

@@ -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",

View File

@@ -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",

View File

@@ -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",

View File

@@ -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",

View File

@@ -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",

View File

@@ -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": "一般",

View File

@@ -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