chore: merge (#5713)

```[tasklist]

### Definition of Ready

- [ ] I am happy with the code
- [ ] Short description of the feature/issue is added in the pr description
- [ ] PR is linked to the corresponding user story
- [ ] Acceptance criteria are met
- [ ] All open todos and follow ups are defined in a new ticket and justified
- [ ] Deviations from the acceptance criteria and design are agreed with the PO and documented.
- [ ] No debug or dead code
- [ ] Critical parts are tested automatically
- [ ] Where possible E2E tests are implemented
- [ ] Documentation/examples are up-to-date
- [ ] All non-functional requirements are met
- [ ] Functionality of the acceptance criteria is checked manually on the dev system.
```
This commit is contained in:
Silvan
2023-04-19 10:14:28 +02:00
committed by GitHub
276 changed files with 12483 additions and 3915 deletions

View File

@@ -37,7 +37,7 @@ jobs:
- name: linting
uses: golangci/golangci-lint-action@v3
with:
version: v1.50.1
version: v1.52
only-new-issues: true
skip-pkg-cache: true
- name: Publish go coverage

View File

@@ -66,7 +66,7 @@ jobs:
with:
distribution: goreleaser
version: v1.11.0
args: release
args: release --timeout 50m
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GORELEASER_TOKEN_TAP: ${{ steps.generate-token.outputs.token }}
@@ -78,3 +78,11 @@ jobs:
with:
file: .artifacts/codecov/profile.cov
name: go-codecov
- name: Bump Chart Version
uses: peter-evans/repository-dispatch@v2
if: steps.semantic.outputs.new_release_published == 'true' && github.ref == 'refs/heads/main'
with:
token: ${{ steps.generate-token.outputs.token }}
repository: zitadel/zitadel-charts
event-type: zitadel-released
client-payload: '{"semanticoutputs": "${{ steps.semantic.outputs }}"}'

View File

@@ -1,8 +1,7 @@
module.exports = {
branches: [
{name: 'main'},
{name: 'next'},
{name: '1.87.x', range: '1.87.x', channel: '1.87.x'}
{name: 'next'}
],
plugins: [
"@semantic-release/commit-analyzer"

View File

@@ -35,7 +35,7 @@ Do you have project that requires a multi-tenant user management with self-servi
Look no further — ZITADEL combines the ease of Auth0 with the versatility of Keycloak.
We provide you with a wide range of out of the box features to accelerate your project.
Multi-tenancy with branding customization, secure login, self-service, OpenID Connect, OAuth2.x, SAML2, Passwordless with FIDO2 (including Passkeys), OTP, U2F, and an unlimited audit trail is there for you, ready to use.
Multi-tenancy with branding customization, secure login, self-service, OpenID Connect, OAuth2.x, SAML2, LDAP, Passwordless with FIDO2 (including Passkeys), OTP, U2F, and an unlimited audit trail is there for you, ready to use.
With ZITADEL you can rely on a hardened and extensible turnkey solution to solve all of your authentication and authorization needs.
@@ -90,20 +90,34 @@ Yet it offers everything you need for a customer identity ([CIAM](https://zitade
## Features
Authentication
- Single Sign On (SSO)
- Passwordless with FIDO2 support (Including Passkeys)
- Username / Password
- Multifactor authentication with OTP, U2F
- [Identity Brokering](https://zitadel.com/docs/guides/integrate/identity-brokering)
- [Machine-to-machine (JWT profile)](https://zitadel.com/docs/guides/integrate/serviceusers)
- Personal Access Tokens (PAT)
- Role Based Access Control (RBAC)
- [Delegate role management to third-parties](https://zitadel.com/docs/guides/manage/console/projects)
- [Self-registration](https://zitadel.com/docs/concepts/features/selfservice#registration) including verification
- [Self-service](https://zitadel.com/docs/concepts/features/selfservice) for end-users, business customers, and administrators
- Multifactor authentication with OTP, U2F, SMS
- LDAP
- [OpenID Connect certified](https://openid.net/certification/#OPs) => [OIDC Endpoints](https://zitadel.com/docs/apis/openidoauth/endpoints)
- [SAML 2.0](http://docs.oasis-open.org/security/saml/Post2.0/sstc-saml-tech-overview-2.0.html) => [SAML Endpoints](https://zitadel.com/docs/apis/saml/endpoints)
- [Machine-to-machine](https://zitadel.com/docs/guides/integrate/serviceusers) with JWT profile, Personal Access Tokens (PAT), and Client Credentials
Multi-Tenancy
- [Identity Brokering](https://zitadel.com/docs/guides/integrate/identity-brokering) with templates for popular identity providers
- [Delegate role management to third-parties](https://zitadel.com/docs/guides/manage/console/projects)
- Domain discovery
Integration
- [GRPC and REST APIs](https://zitadel.com/docs/apis/introduction)
- [Actions](https://zitadel.com/docs/apis/actions/introduction) to call any API, send webhooks, adjust workflows, or customize tokens
- Role Based Access Control (RBAC)
Self-Service
- [Self-registration](https://zitadel.com/docs/concepts/features/selfservice#registration) including verification
- [Self-service](https://zitadel.com/docs/concepts/features/selfservice) for end-users, business customers, and administrators
- [Administration UI (Console)](https://zitadel.com/docs/guides/manage/console/overview)
Deployment
- [Postgres](https://zitadel.com/docs/self-hosting/manage/database#postgres) (version >= 14) or [CockroachDB](https://zitadel.com/docs/self-hosting/manage/database#cockroach) (version >= 22.0)
- [Zero Downtime Updates](https://zitadel.com/docs/concepts/architecture/solution#zero-downtime-updates)
Track upcoming features on our [roadmap](https://zitadel.com/roadmap).

View File

@@ -93,4 +93,36 @@ protoc \
mv ${ZITADEL_PATH}/pkg/grpc/auth/zitadel/* ${ZITADEL_PATH}/pkg/grpc/auth
rm -r ${ZITADEL_PATH}/pkg/grpc/auth/zitadel
protoc \
-I=/proto/include \
--grpc-gateway_out ${GOPATH}/src \
--grpc-gateway_opt logtostderr=true \
--grpc-gateway_opt allow_delete_body=true \
--openapiv2_out ${OPENAPI_PATH} \
--openapiv2_opt logtostderr=true \
--openapiv2_opt allow_delete_body=true \
--authoption_out=${GRPC_PATH}/user \
--validate_out=lang=go:${GOPATH}/src \
${PROTO_PATH}/user/v2alpha/user_service.proto
# authoptions are generated into the wrong folder
cp -r ${ZITADEL_PATH}/pkg/grpc/user/zitadel/* ${ZITADEL_PATH}/pkg/grpc
rm -r ${ZITADEL_PATH}/pkg/grpc/user/zitadel
protoc \
-I=/proto/include \
--grpc-gateway_out ${GOPATH}/src \
--grpc-gateway_opt logtostderr=true \
--grpc-gateway_opt allow_delete_body=true \
--openapiv2_out ${OPENAPI_PATH} \
--openapiv2_opt logtostderr=true \
--openapiv2_opt allow_delete_body=true \
--authoption_out=${GRPC_PATH}/session \
--validate_out=lang=go:${GOPATH}/src \
${PROTO_PATH}/session/v2alpha/session_service.proto
# authoptions are generated into the wrong folder
cp -r ${ZITADEL_PATH}/pkg/grpc/session/zitadel/* ${ZITADEL_PATH}/pkg/grpc
rm -r ${ZITADEL_PATH}/pkg/grpc/session/zitadel
echo "done generating grpc"

View File

@@ -554,7 +554,7 @@ DefaultInstance:
Title: Zitadel - User initialisieren
PreHeader: User initialisieren
Subject: User initialisieren
Greeting: Hallo {{.FirstName}} {{.LastName}},
Greeting: Hallo {{.DisplayName}},
Text: Dieser Benutzer wurde soeben im Zitadel erstellt. Mit dem Benutzernamen <br><strong>{{.PreferredLoginName}}</strong><br> kannst du dich anmelden. Nutze den untenstehenden Button, um die Initialisierung abzuschliessen <br>(Code <strong>{{.Code}}</strong>).<br> Falls du dieses Mail nicht angefordert hast, kannst du es einfach ignorieren.
ButtonText: Initialisierung abschliessen
- MessageTextType: PasswordReset
@@ -562,7 +562,7 @@ DefaultInstance:
Title: Zitadel - Passwort zurücksetzen
PreHeader: Passwort zurücksetzen
Subject: Passwort zurücksetzen
Greeting: Hallo {{.FirstName}} {{.LastName}},
Greeting: Hallo {{.DisplayName}},
Text: Wir haben eine Anfrage für das Zurücksetzen deines Passwortes bekommen. Du kannst den untenstehenden Button verwenden, um dein Passwort zurückzusetzen <br>(Code <strong>{{.Code}}</strong>).<br> Falls du dieses Mail nicht angefordert hast, kannst du es ignorieren.
ButtonText: Passwort zurücksetzen
- MessageTextType: VerifyEmail
@@ -570,7 +570,7 @@ DefaultInstance:
Title: Zitadel - Email verifizieren
PreHeader: Email verifizieren
Subject: Email verifizieren
Greeting: Hallo {{.FirstName}} {{.LastName}},
Greeting: Hallo {{.DisplayName}},
Text: Eine neue E-Mail Adresse wurde hinzugefügt. Bitte verwende den untenstehenden Button um diese zu verifizieren <br>(Code <strong>{{.Code}}</strong>).<br> Falls du deine E-Mail Adresse nicht selber hinzugefügt hast, kannst du dieses E-Mail ignorieren.
ButtonText: Email verifizieren
- MessageTextType: VerifyPhone
@@ -578,7 +578,7 @@ DefaultInstance:
Title: Zitadel - Telefonnummer verifizieren
PreHeader: Telefonnummer verifizieren
Subject: Telefonnummer verifizieren
Greeting: Hallo {{.FirstName}} {{.LastName}},
Greeting: Hallo {{.DisplayName}},
Text: Eine Telefonnummer wurde hinzugefügt. Bitte verifiziere diese in dem du folgenden Code eingibst (Code {{.Code}})
ButtonText: Telefon verifizieren
- MessageTextType: DomainClaimed
@@ -586,7 +586,7 @@ DefaultInstance:
Title: Zitadel - Domain wurde beansprucht
PreHeader: Email / Username ändern
Subject: Domain wurde beansprucht
Greeting: Hallo {{.FirstName}} {{.LastName}},
Greeting: Hallo {{.DisplayName}},
Text: Die Domain {{.Domain}} wurde von einer Organisation beansprucht. Dein derzeitiger User {{.Username}} ist nicht Teil dieser Organisation. Daher musst du beim nächsten Login eine neue Email hinterlegen. Für diesen Login haben wir dir einen temporären Usernamen ({{.TempUsername}}) erstellt.
ButtonText: Login
- MessageTextType: PasswordChange
@@ -594,7 +594,7 @@ DefaultInstance:
Title: ZITADEL - Passwort von Benutzer wurde geändert
PreHeader: Passwort Änderung
Subject: Passwort von Benutzer wurde geändert
Greeting: Hallo {{.FirstName}} {{.LastName}},
Greeting: Hallo {{.DisplayName}},
Text: Das Password vom Benutzer wurde geändert. Wenn diese Änderung von jemand anderem gemacht wurde, empfehlen wir die sofortige Zurücksetzung ihres Passworts.
ButtonText: Login
- MessageTextType: InitCode
@@ -602,7 +602,7 @@ DefaultInstance:
Title: Zitadel - Initialize User
PreHeader: Initialize User
Subject: Initialize User
Greeting: Hello {{.FirstName}} {{.LastName}},
Greeting: Hello {{.DisplayName}},
Text: This user was created in Zitadel. Use the username {{.PreferredLoginName}} to login. Please click the button below to finish the initialization process. (Code {{.Code}}) If you didn't ask for this mail, please ignore it.
ButtonText: Finish initialization
- MessageTextType: PasswordReset
@@ -610,7 +610,7 @@ DefaultInstance:
Title: Zitadel - Reset password
PreHeader: Reset password
Subject: Reset password
Greeting: Hello {{.FirstName}} {{.LastName}},
Greeting: Hello {{.DisplayName}},
Text: We received a password reset request. Please use the button below to reset your password. (Code {{.Code}}) If you didn't ask for this mail, please ignore it.
ButtonText: Reset password
- MessageTextType: VerifyEmail
@@ -618,7 +618,7 @@ DefaultInstance:
Title: Zitadel - Verify email
PreHeader: Verify email
Subject: Verify email
Greeting: Hello {{.FirstName}} {{.LastName}},
Greeting: Hello {{.DisplayName}},
Text: A new email has been added. Please use the button below to verify your mail. (Code {{.Code}}) If you din't add a new email, please ignore this email.
ButtonText: Verify email
- MessageTextType: VerifyPhone
@@ -626,7 +626,7 @@ DefaultInstance:
Title: Zitadel - Verify phone
PreHeader: Verify phone
Subject: Verify phone
Greeting: Hello {{.FirstName}} {{.LastName}},
Greeting: Hello {{.DisplayName}},
Text: A new phonenumber has been added. Please use the following code to verify it {{.Code}}.
ButtonText: Verify phone
- MessageTextType: DomainClaimed
@@ -634,7 +634,7 @@ DefaultInstance:
Title: Zitadel - Domain has been claimed
PreHeader: Change email / username
Subject: Domain has been claimed
Greeting: Hello {{.FirstName}} {{.LastName}},
Greeting: Hello {{.DisplayName}},
Text: The domain {{.Domain}} has been claimed by an organisation. Your current user {{.UserName}} is not part of this organisation. Therefore you'll have to change your email when you login. We have created a temporary username ({{.TempUsername}}) for this login.
ButtonText: Login
- MessageTextType: PasswordChange
@@ -642,7 +642,7 @@ DefaultInstance:
Title: ZITADEL - Password of user has changed
PreHeader: Change password
Subject: Password of user has changed
Greeting: Hello {{.FirstName}} {{.LastName}},
Greeting: Hello {{.DisplayName}},
Text: The password of your user has changed. If this change was not done by you, please be advised to immediately reset your password.
ButtonText: Login

53
cmd/setup/10.go Normal file
View File

@@ -0,0 +1,53 @@
package setup
import (
"context"
_ "embed"
"github.com/zitadel/logging"
"github.com/zitadel/zitadel/internal/database"
)
var (
//go:embed 10.sql
correctCreationDate10 string
)
type CorrectCreationDate struct {
dbClient *database.DB
}
func (mig *CorrectCreationDate) Execute(ctx context.Context) (err error) {
tx, err := mig.dbClient.Begin()
if err != nil {
return err
}
if mig.dbClient.Type() == "cockroach" {
if _, err := tx.Exec("SET experimental_enable_temp_tables=on"); err != nil {
return err
}
}
defer func() {
if err != nil {
logging.OnError(tx.Rollback()).Debug("rollback failed")
return
}
err = tx.Commit()
}()
for {
res, err := tx.ExecContext(ctx, correctCreationDate10)
if err != nil {
return err
}
affected, _ := res.RowsAffected()
logging.WithFields("count", affected).Info("creation dates changed")
if affected == 0 {
return nil
}
}
}
func (mig *CorrectCreationDate) String() string {
return "10_correct_creation_date"
}

28
cmd/setup/10.sql Normal file
View File

@@ -0,0 +1,28 @@
CREATE temporary TABLE IF NOT EXISTS wrong_events (
instance_id STRING
, event_sequence INT8
, current_cd TIMESTAMPTZ
, next_cd TIMESTAMPTZ
);
TRUNCATE wrong_events;
INSERT INTO wrong_events (
SELECT * FROM (
SELECT
instance_id
, event_sequence
, creation_date AS current_cd
, lead(creation_date) OVER (
PARTITION BY instance_id
ORDER BY event_sequence DESC
) AS next_cd
FROM
eventstore.events
) WHERE
current_cd < next_cd
ORDER BY
event_sequence DESC
);
UPDATE eventstore.events e SET creation_date = we.next_cd FROM wrong_events we WHERE e.event_sequence = we.event_sequence and e.instance_id = we.instance_id;

View File

@@ -56,15 +56,16 @@ func MustNewConfig(v *viper.Viper) *Config {
}
type Steps struct {
s1ProjectionTable *ProjectionTable
s2AssetsTable *AssetTable
FirstInstance *FirstInstance
s4EventstoreIndexes *EventstoreIndexesNew
s5LastFailed *LastFailed
s6OwnerRemoveColumns *OwnerRemoveColumns
s7LogstoreTables *LogstoreTables
s8AuthTokens *AuthTokenIndexes
s9EventstoreIndexes2 *EventstoreIndexesNew
s1ProjectionTable *ProjectionTable
s2AssetsTable *AssetTable
FirstInstance *FirstInstance
s4EventstoreIndexes *EventstoreIndexesNew
s5LastFailed *LastFailed
s6OwnerRemoveColumns *OwnerRemoveColumns
s7LogstoreTables *LogstoreTables
s8AuthTokens *AuthTokenIndexes
s9EventstoreIndexes2 *EventstoreIndexesNew
s10EventstoreCreationDate *CorrectCreationDate
}
type encryptionKeyConfig struct {

View File

@@ -88,6 +88,7 @@ func Setup(config *Config, steps *Steps, masterKey string) {
steps.s7LogstoreTables = &LogstoreTables{dbClient: dbClient.DB, username: config.Database.Username(), dbType: config.Database.Type()}
steps.s8AuthTokens = &AuthTokenIndexes{dbClient: dbClient}
steps.s9EventstoreIndexes2 = New09(dbClient)
steps.s10EventstoreCreationDate = &CorrectCreationDate{dbClient: dbClient}
err = projection.Create(ctx, dbClient, eventstoreClient, config.Projections, nil, nil)
logging.OnError(err).Fatal("unable to start projections")
@@ -123,6 +124,8 @@ func Setup(config *Config, steps *Steps, masterKey string) {
logging.OnError(err).Fatal("unable to migrate step 8")
err = migration.Migrate(ctx, eventstoreClient, steps.s9EventstoreIndexes2)
logging.OnError(err).Fatal("unable to migrate step 9")
err = migration.Migrate(ctx, eventstoreClient, steps.s10EventstoreCreationDate)
logging.OnError(err).Fatal("unable to migrate step 10")
for _, repeatableStep := range repeatableSteps {
err = migration.Migrate(ctx, eventstoreClient, repeatableStep)

View File

@@ -33,7 +33,9 @@ import (
"github.com/zitadel/zitadel/internal/api/grpc/admin"
"github.com/zitadel/zitadel/internal/api/grpc/auth"
"github.com/zitadel/zitadel/internal/api/grpc/management"
"github.com/zitadel/zitadel/internal/api/grpc/session/v2"
"github.com/zitadel/zitadel/internal/api/grpc/system"
"github.com/zitadel/zitadel/internal/api/grpc/user/v2"
http_util "github.com/zitadel/zitadel/internal/api/http"
"github.com/zitadel/zitadel/internal/api/http/middleware"
"github.com/zitadel/zitadel/internal/api/oidc"
@@ -223,7 +225,10 @@ func startAPIs(
logging.Warn("access logs are currently in beta")
}
accessInterceptor := middleware.NewAccessInterceptor(accessSvc, config.Quotas.Access)
apis := api.New(config.Port, router, queries, verifier, config.InternalAuthZ, tlsConfig, config.HTTP2HostHeader, config.HTTP1HostHeader, accessSvc)
apis, err := api.New(ctx, config.Port, router, queries, verifier, config.InternalAuthZ, tlsConfig, config.HTTP2HostHeader, config.HTTP1HostHeader, accessSvc)
if err != nil {
return fmt.Errorf("error creating api %w", err)
}
authRepo, err := auth_es.Start(ctx, config.Auth, config.SystemDefaults, commands, queries, dbClient, eventstore, keys.OIDC, keys.User)
if err != nil {
return fmt.Errorf("error starting auth repo: %w", err)
@@ -244,9 +249,15 @@ func startAPIs(
if err := apis.RegisterServer(ctx, auth.CreateServer(commands, queries, authRepo, config.SystemDefaults, keys.User, config.ExternalSecure, config.AuditLogRetention)); err != nil {
return err
}
if err := apis.RegisterService(ctx, user.CreateServer(commands, queries)); err != nil {
return err
}
if err := apis.RegisterService(ctx, session.CreateServer(commands, queries)); err != nil {
return err
}
instanceInterceptor := middleware.InstanceInterceptor(queries, config.HTTP1HostHeader, login.IgnoreInstanceEndpoints...)
assetsCache := middleware.AssetsCacheInterceptor(config.AssetStorage.Cache.MaxAge, config.AssetStorage.Cache.SharedMaxAge)
apis.RegisterHandler(assets.HandlerPrefix, assets.NewHandler(commands, verifier, config.InternalAuthZ, id.SonyFlakeGenerator(), store, queries, middleware.CallDurationHandler, instanceInterceptor.Handler, assetsCache.Handler, accessInterceptor.Handle))
apis.RegisterHandlerOnPrefix(assets.HandlerPrefix, assets.NewHandler(commands, verifier, config.InternalAuthZ, id.SonyFlakeGenerator(), store, queries, middleware.CallDurationHandler, instanceInterceptor.Handler, assetsCache.Handler, accessInterceptor.Handle))
userAgentInterceptor, err := middleware.NewUserAgentHandler(config.UserAgentCookie, keys.UserAgentCookieKey, id.SonyFlakeGenerator(), config.ExternalSecure, login.EndpointResources)
if err != nil {
@@ -258,37 +269,34 @@ func startAPIs(
if err != nil {
return fmt.Errorf("unable to start openapi handler: %w", err)
}
apis.RegisterHandler(openapi.HandlerPrefix, openAPIHandler)
apis.RegisterHandlerOnPrefix(openapi.HandlerPrefix, openAPIHandler)
oidcProvider, err := oidc.NewProvider(ctx, config.OIDC, login.DefaultLoggedOutPath, config.ExternalSecure, commands, queries, authRepo, keys.OIDC, keys.OIDCKey, eventstore, dbClient, userAgentInterceptor, instanceInterceptor.Handler, accessInterceptor.Handle)
oidcProvider, err := oidc.NewProvider(config.OIDC, login.DefaultLoggedOutPath, config.ExternalSecure, commands, queries, authRepo, keys.OIDC, keys.OIDCKey, eventstore, dbClient, userAgentInterceptor, instanceInterceptor.Handler, accessInterceptor.Handle)
if err != nil {
return fmt.Errorf("unable to start oidc provider: %w", err)
}
apis.RegisterHandlerPrefixes(oidcProvider.HttpHandler(), "/.well-known/openid-configuration", "/oidc/v1", "/oauth/v2")
samlProvider, err := saml.NewProvider(ctx, config.SAML, config.ExternalSecure, commands, queries, authRepo, keys.OIDC, keys.SAML, eventstore, dbClient, instanceInterceptor.Handler, userAgentInterceptor, accessInterceptor.Handle)
samlProvider, err := saml.NewProvider(config.SAML, config.ExternalSecure, commands, queries, authRepo, keys.OIDC, keys.SAML, eventstore, dbClient, instanceInterceptor.Handler, userAgentInterceptor, accessInterceptor.Handle)
if err != nil {
return fmt.Errorf("unable to start saml provider: %w", err)
}
apis.RegisterHandler(saml.HandlerPrefix, samlProvider.HttpHandler())
apis.RegisterHandlerOnPrefix(saml.HandlerPrefix, samlProvider.HttpHandler())
c, err := console.Start(config.Console, config.ExternalSecure, oidcProvider.IssuerFromRequest, middleware.CallDurationHandler, instanceInterceptor.Handler, accessInterceptor.Handle, config.CustomerPortal)
if err != nil {
return fmt.Errorf("unable to start console: %w", err)
}
apis.RegisterHandler(console.HandlerPrefix, c)
apis.RegisterHandlerOnPrefix(console.HandlerPrefix, c)
l, err := login.CreateLogin(config.Login, commands, queries, authRepo, store, console.HandlerPrefix+"/", op.AuthCallbackURL(oidcProvider), provider.AuthCallbackURL(samlProvider), config.ExternalSecure, userAgentInterceptor, op.NewIssuerInterceptor(oidcProvider.IssuerFromRequest).Handler, provider.NewIssuerInterceptor(samlProvider.IssuerFromRequest).Handler, instanceInterceptor.Handler, assetsCache.Handler, accessInterceptor.Handle, keys.User, keys.IDPConfig, keys.CSRFCookieKey)
if err != nil {
return fmt.Errorf("unable to start login: %w", err)
}
apis.RegisterHandler(login.HandlerPrefix, l.Handler())
apis.RegisterHandlerOnPrefix(login.HandlerPrefix, l.Handler())
//handle oidc at last, to be able to handle the root
//we might want to change that in the future
//esp. if we want to have multiple well-known endpoints
//it might make sense to handle the discovery endpoint and oauth and oidc prefixes individually
//but this will require a change in the oidc lib
apis.RegisterHandler("", oidcProvider.HttpHandler())
// handle grpc at last to be able to handle the root, because grpc and gateway require a lot of different prefixes
apis.RouteGRPC()
return nil
}

View File

@@ -1,12 +0,0 @@
# This file is used by the build system to adjust CSS and JS output to support the specified browsers below.
# For additional information regarding the format and rule options, please see:
# https://github.com/browserslist/browserslist#queries
# You can see what browsers were selected by your queries by running:
# npx browserslist
> 0.5%
last 2 versions
Firefox ESR
not dead
not IE 9-11 # For IE 9-11 support, remove 'not'.

View File

@@ -22,6 +22,7 @@
"main": "src/main.ts",
"polyfills": ["zone.js"],
"tsConfig": "tsconfig.app.json",
"inlineStyleLanguage": "scss",
"assets": ["src/favicon.ico", "src/assets", "src/manifest.webmanifest"],
"styles": ["src/styles.scss"],
"scripts": ["./node_modules/tinycolor2/dist/tinycolor-min.js"],
@@ -33,59 +34,16 @@
"@angular/common/locales/de",
"codemirror/mode/javascript/javascript",
"codemirror/mode/xml/xml",
"src/app/proto/generated/zitadel/admin_pb",
"src/app/proto/generated/zitadel/org_pb",
"src/app/proto/generated/zitadel/management_pb",
"src/app/proto/generated/zitadel/user_pb",
"src/app/proto/generated/**",
"google-protobuf/google/protobuf/empty_pb",
"file-saver",
"qrcode"
],
"vendorChunk": true,
"extractLicenses": false,
"buildOptimizer": false,
"sourceMap": true,
"optimization": {
"scripts": true,
"fonts": {
"inline": true
},
"styles": {
"minify": true,
"inlineCritical": false
}
},
"namedChunks": true
]
},
"configurations": {
"production": {
"fileReplacements": [
{
"replace": "src/environments/environment.ts",
"with": "src/environments/environment.prod.ts"
}
],
"optimization": {
"scripts": true,
"fonts": {
"inline": false
},
"styles": {
"minify": true,
"inlineCritical": false
}
},
"outputHashing": "all",
"sourceMap": false,
"namedChunks": false,
"extractLicenses": true,
"vendorChunk": false,
"buildOptimizer": true,
"budgets": [
{
"type": "initial",
"maximumWarning": "7mb",
"maximumWarning": "6mb",
"maximumError": "7mb"
},
{
@@ -94,16 +52,21 @@
"maximumError": "10kb"
}
],
"serviceWorker": false,
"ngswConfigPath": "ngsw-config.json"
"outputHashing": "all"
},
"development": {}
"development": {
"buildOptimizer": false,
"optimization": false,
"vendorChunk": true,
"extractLicenses": false,
"sourceMap": true,
"namedChunks": true
}
},
"defaultConfiguration": "production"
"defaultConfiguration": "development"
},
"serve": {
"builder": "@angular-devkit/build-angular:dev-server",
"options": {},
"configurations": {
"production": {
"browserTarget": "console:build:production"
@@ -123,13 +86,12 @@
"test": {
"builder": "@angular-devkit/build-angular:karma",
"options": {
"main": "src/test.ts",
"polyfills": ["zone.js"],
"polyfills": ["zone.js", "zone.js/testing"],
"tsConfig": "tsconfig.spec.json",
"karmaConfig": "karma.conf.js",
"assets": ["src/favicon.ico", "src/assets", "src/manifest.webmanifest"],
"styles": ["./node_modules/@angular/material/prebuilt-themes/pink-bluegrey.css", "src/styles.scss"],
"scripts": ["./node_modules/tinycolor2/dist/tinycolor-min.js"]
"inlineStyleLanguage": "scss",
"assets": ["src/favicon.ico", "src/assets"],
"styles": ["src/styles.scss"],
"scripts": []
}
},
"lint": {
@@ -137,35 +99,12 @@
"options": {
"lintFilePatterns": ["src/**/*.ts", "src/**/*.html"]
}
},
"e2e": {
"builder": "@angular-devkit/build-angular:protractor",
"options": {
"protractorConfig": "e2e/protractor.conf.js"
},
"configurations": {
"production": {
"devServerTarget": "console:serve:production"
},
"development": {
"devServerTarget": "console:serve:development"
}
},
"defaultConfiguration": "development"
}
}
}
},
"cli": {
"analytics": "2b4e8e6c-f053-4562-b7a6-00c6c06a6791",
"analytics": false,
"schematicCollections": ["@angular-eslint/schematics"]
},
"schematics": {
"@angular-eslint/schematics:application": {
"setParserOptionsProject": true
},
"@angular-eslint/schematics:library": {
"setParserOptionsProject": true
}
}
}

4793
console/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -12,18 +12,18 @@
},
"private": true,
"dependencies": {
"@angular/animations": "^15.2.2",
"@angular/cdk": "^15.2.2",
"@angular/common": "^15.2.2",
"@angular/compiler": "^15.2.2",
"@angular/core": "^15.2.2",
"@angular/forms": "^15.2.2",
"@angular/material": "^15.2.2",
"@angular/material-moment-adapter": "^15.2.2",
"@angular/platform-browser": "^15.2.2",
"@angular/platform-browser-dynamic": "^15.2.2",
"@angular/router": "^15.2.2",
"@angular/service-worker": "^15.2.2",
"@angular/animations": "^15.2.6",
"@angular/cdk": "^15.2.6",
"@angular/common": "^15.2.6",
"@angular/compiler": "^15.2.6",
"@angular/core": "^15.2.6",
"@angular/forms": "^15.2.6",
"@angular/material": "^15.2.6",
"@angular/material-moment-adapter": "^15.2.6",
"@angular/platform-browser": "^15.2.6",
"@angular/platform-browser-dynamic": "^15.2.6",
"@angular/router": "^15.2.6",
"@angular/service-worker": "^15.2.6",
"@ctrl/ngx-codemirror": "^6.1.0",
"@grpc/grpc-js": "^1.8.12",
"@ngx-translate/core": "^14.0.0",
@@ -41,7 +41,7 @@
"google-protobuf": "^3.21.2",
"grpc-web": "^1.4.1",
"i18n-iso-countries": "^7.5.0",
"libphonenumber-js": "^1.10.19",
"libphonenumber-js": "^1.10.24",
"material-design-icons-iconfont": "^6.1.1",
"moment": "^2.29.4",
"ngx-color": "^8.0.3",
@@ -49,36 +49,36 @@
"tinycolor2": "^1.6.0",
"tslib": "^2.4.1",
"uuid": "^9.0.0",
"zone.js": "~0.12.0"
"zone.js": "~0.13.0"
},
"devDependencies": {
"@angular-devkit/build-angular": "^15.2.2",
"@angular-eslint/builder": "^15.2.1",
"@angular-eslint/eslint-plugin": "^15.2.1",
"@angular-eslint/eslint-plugin-template": "^15.2.1",
"@angular-eslint/schematics": "^15.2.1",
"@angular-eslint/template-parser": "^15.2.1",
"@angular/cli": "^15.2.2",
"@angular/compiler-cli": "^15.2.2",
"@angular/language-service": "^15.2.2",
"@angular-devkit/build-angular": "^15.2.5",
"@angular-eslint/builder": "15.2.1",
"@angular-eslint/eslint-plugin": "15.2.1",
"@angular-eslint/eslint-plugin-template": "15.2.1",
"@angular-eslint/schematics": "15.2.1",
"@angular-eslint/template-parser": "15.2.1",
"@angular/cli": "^15.2.5",
"@angular/compiler-cli": "^15.2.6",
"@angular/language-service": "^15.2.6",
"@bufbuild/buf": "^1.14.0",
"@types/jasmine": "~4.3.0",
"@types/jasminewd2": "~2.0.10",
"@types/jsonwebtoken": "^9.0.1",
"@types/node": "^18.13.0",
"@types/node": "^18.15.11",
"@types/qrcode": "^1.5.0",
"@typescript-eslint/eslint-plugin": "5.54.1",
"@typescript-eslint/parser": "5.54.1",
"@typescript-eslint/eslint-plugin": "5.48.2",
"@typescript-eslint/parser": "5.48.2",
"codelyzer": "^6.0.2",
"eslint": "^8.33.0",
"jasmine-core": "~4.5.0",
"jasmine-core": "~4.6.0",
"jasmine-spec-reporter": "~7.0.0",
"karma": "~6.4.1",
"karma-chrome-launcher": "~3.1.0",
"karma-coverage-istanbul-reporter": "~3.0.2",
"karma-jasmine": "~5.1.0",
"karma-jasmine-html-reporter": "^2.0.0",
"prettier": "^2.8.4",
"prettier": "^2.8.7",
"prettier-plugin-organize-imports": "^3.2.2",
"protractor": "~7.0.0",
"typescript": "^4.9.5"

View File

@@ -201,7 +201,7 @@ export class AppComponent implements OnDestroy {
}
});
this.activatedRoute.queryParams.pipe(filter((params) => !!params.org)).subscribe((params) => {
this.activatedRoute.queryParams.pipe(filter((params) => !!params['org'])).subscribe((params) => {
const { org } = params;
this.authService.getActiveOrg(org);
});
@@ -252,7 +252,7 @@ export class AppComponent implements OnDestroy {
}
public prepareRoute(outlet: RouterOutlet): boolean {
return outlet && outlet.activatedRouteData && outlet.activatedRouteData.animation;
return outlet && outlet.activatedRouteData && outlet.activatedRouteData['animation'];
}
public onSetTheme(theme: string): void {
@@ -267,15 +267,15 @@ export class AppComponent implements OnDestroy {
}
private setLanguage(): void {
this.translate.addLangs(['de', 'en', 'fr', 'it', 'ja', 'pl', 'zh']);
this.translate.addLangs(['de', 'en', 'es', 'fr', 'it', 'ja', 'pl', 'zh']);
this.translate.setDefaultLang('en');
this.authService.user.subscribe((userprofile) => {
if (userprofile) {
const cropped = navigator.language.split('-')[0] ?? 'en';
const fallbackLang = cropped.match(/de|en|fr|it|ja|pl|zh/) ? cropped : 'en';
const fallbackLang = cropped.match(/de|en|es|fr|it|ja|pl|zh/) ? cropped : 'en';
const lang = userprofile?.human?.profile?.preferredLanguage.match(/de|en|fr|it|ja|pl|zh/)
const lang = userprofile?.human?.profile?.preferredLanguage.match(/de|en|es|fr|it|ja|pl|zh/)
? userprofile.human.profile?.preferredLanguage
: fallbackLang;
this.translate.use(lang);

View File

@@ -2,6 +2,7 @@ import { CommonModule, registerLocaleData } from '@angular/common';
import { HttpClientModule } from '@angular/common/http';
import localeDe from '@angular/common/locales/de';
import localeEn from '@angular/common/locales/en';
import localeEs from '@angular/common/locales/es';
import localeFr from '@angular/common/locales/fr';
import localeIt from '@angular/common/locales/it';
import localeJa from '@angular/common/locales/ja';
@@ -64,6 +65,8 @@ registerLocaleData(localeDe);
i18nIsoCountries.registerLocale(require('i18n-iso-countries/langs/de.json'));
registerLocaleData(localeEn);
i18nIsoCountries.registerLocale(require('i18n-iso-countries/langs/en.json'));
registerLocaleData(localeEs);
i18nIsoCountries.registerLocale(require('i18n-iso-countries/langs/es.json'));
registerLocaleData(localeFr);
i18nIsoCountries.registerLocale(require('i18n-iso-countries/langs/fr.json'));
registerLocaleData(localeIt);

View File

@@ -16,7 +16,7 @@ export class UserGuard implements CanActivate {
state: RouterStateSnapshot,
): Observable<boolean> | Promise<boolean> | boolean {
return this.authService.user.pipe(
map((user) => user?.id !== route.params.id),
map((user) => user?.id !== route.params['id']),
tap((isNotMe) => {
if (!isNotMe) {
this.router.navigate(['/users', 'me']);

View File

@@ -1,16 +1,12 @@
<div class="accounts-card">
<cnsl-avatar
(click)="editUserProfile()"
*ngIf="user && user.human?.profile && user.human?.profile?.displayName"
*ngIf="user"
class="avatar"
[ngClass]="{ 'iam-user': iamuser }"
[forColor]="user.preferredLoginName"
[avatarUrl]="user.human?.profile?.avatarUrl || ''"
[name]="
user.human && user.human.profile && user.human.profile.displayName
? user.human.profile.displayName
: user.human?.profile?.firstName + ' ' + user.human?.profile?.lastName
"
[name]="user.human?.profile?.displayName ?? ''"
[size]="80"
>
</cnsl-avatar>

View File

@@ -16,9 +16,9 @@
<input cnslInput [matDatepicker]="picker" [min]="startDate" [formControl]="dateControl" />
<mat-datepicker-toggle style="top: 0" cnslSuffix [for]="picker"></mat-datepicker-toggle>
<mat-datepicker #picker [startAt]="startDate"></mat-datepicker>
<span cnslError *ngIf="dateControl?.errors?.matDatepickerMin?.min">
<span cnslError *ngIf="dateControl && dateControl.errors && dateControl.errors['matDatepickerMin']?.min">
{{ 'USER.MACHINE.CHOOSEDATEAFTER' | translate }}:
{{ dateControl.errors?.matDatepickerMin.min.toDate() | localizedDate : 'EEE dd. MMM' }}
{{ dateControl.errors['matDatepickerMin'].min.toDate() | localizedDate : 'EEE dd. MMM' }}
</span>
</cnsl-form-field>
</div>

View File

@@ -7,9 +7,9 @@
<input cnslInput [matDatepicker]="picker" [min]="startDate" [formControl]="dateControl" />
<mat-datepicker-toggle style="top: 0" cnslSuffix [for]="picker"></mat-datepicker-toggle>
<mat-datepicker #picker startView="year" [startAt]="startDate"></mat-datepicker>
<span cnslError *ngIf="dateControl?.errors?.matDatepickerMin?.min">
<span cnslError *ngIf="dateControl && dateControl.errors && dateControl.errors['matDatepickerMin']?.min">
{{ 'USER.PERSONALACCESSTOKEN.ADD.CHOOSEDATEAFTER' | translate }}:
{{ dateControl.errors?.matDatepickerMin.min.toDate() | localizedDate : 'EEE dd. MMM' }}
{{ dateControl.errors['matDatepickerMin'].min.toDate() | localizedDate : 'EEE dd. MMM' }}
</span>
</cnsl-form-field>
</div>

View File

@@ -13,10 +13,10 @@
data-e2e="member-avatar"
>
<cnsl-avatar
*ngIf="member && member.displayName && member.firstName && member.lastName; else cog"
*ngIf="member && member.userType === UserType.TYPE_HUMAN; else cog"
class="contributor-avatar dontcloseonclick"
[avatarUrl]="member.avatarUrl || ''"
[name]="member.displayName ? member.displayName : member.firstName + ' ' + member.lastName"
[name]="member.displayName"
[forColor]="member.preferredLoginName"
[size]="32"
>

View File

@@ -2,6 +2,7 @@ import { animate, animateChild, keyframes, query, stagger, style, transition, tr
import { Component, EventEmitter, Input, Output } from '@angular/core';
import { BehaviorSubject } from 'rxjs';
import { Member } from 'src/app/proto/generated/zitadel/member_pb';
import { Type } from 'src/app/proto/generated/zitadel/user_pb';
@Component({
selector: 'cnsl-contributors',
@@ -36,6 +37,8 @@ export class ContributorsComponent {
@Output() showDetailClicked: EventEmitter<void> = new EventEmitter();
@Output() refreshClicked: EventEmitter<void> = new EventEmitter();
public UserType: any = Type;
public emitAddMember(): void {
this.addClicked.emit();
}

View File

@@ -1,3 +1,33 @@
@use '@angular/material' as mat;
@mixin app-create-theme($theme) {
$primary: map-get($theme, primary);
$primary-color: mat.get-color-from-palette($primary, 500);
// Number of steps creating app
$steps: 3;
.app-create-wrapper {
// Reference: https://github.com/angular/components/issues/10681#issuecomment-695185806
@for $i from 1 through $steps {
@for $j from 1 through $i {
.last-edited-step-#{$i}
.mat-horizontal-stepper-header-container
.mat-step-header:nth-child(#{1 + 2 * ($j - 1)})::after,
.last-edited-step-#{$i}
.mat-horizontal-stepper-header-container
.mat-stepper-horizontal-line:nth-child(#{2 + 2 * ($j - 1)}),
.last-edited-step-#{$i}
.mat-horizontal-stepper-header-container
.mat-step-header:nth-child(#{3 + 2 * ($j - 1)})::before {
border-top-width: 3px;
border-top-color: $primary-color;
}
}
}
}
}
.create-layout-container {
display: flex;
align-items: center;

View File

@@ -5,6 +5,7 @@
(click)="showFilter = !showFilter"
class="cnsl-action-button"
#triggereventfilter="cdkOverlayOrigin"
data-e2e="open-filter-button"
>
<i class="las la-filter no-margin"></i>
<span>{{ 'ACTIONS.FILTER' | translate }}</span>
@@ -15,8 +16,6 @@
<ng-template
cdkConnectedOverlay
[cdkConnectedOverlayHasBackdrop]="true"
[flexibleDimensions]="true"
[lockPosition]="true"
[cdkConnectedOverlayOffsetY]="10"
[cdkConnectedOverlayPositions]="positions"
[cdkConnectedOverlayOrigin]="triggereventfilter"
@@ -29,7 +28,7 @@
<div class="filter-events-top">
<button mat-stroked-button type="button" (click)="reset()">{{ 'ACTIONS.RESET' | translate }}</button>
<span class="filter-middle">{{ 'FILTER.TITLE' | translate }}</span>
<button mat-raised-button color="primary" type="button" (click)="finish()">
<button mat-raised-button color="primary" type="button" (click)="finish()" data-e2e="filter-finish-button">
{{ 'ACTIONS.FINISH' | translate }}
</button>
</div>
@@ -96,6 +95,7 @@
name="eventTypesFilterSet"
class="cb"
formControlName="eventTypesFilterSet"
data-e2e="event-type-filter-checkbox"
>{{ 'IAM.EVENTS.FILTERS.TYPE.CHECKBOX' | translate }}
</mat-checkbox>
</div>

View File

@@ -22,5 +22,28 @@
</cnsl-form-field>
</div>
</div>
<div class="name-query">
<mat-checkbox
id="state"
class="cb"
[checked]="getSubFilter(SubQuery.STATE)"
(change)="changeCheckbox(SubQuery.STATE, $event)"
>{{ 'FILTER.STATE' | translate }}
</mat-checkbox>
<div class="subquery" *ngIf="getSubFilter(SubQuery.STATE) as sq">
<span class="nomethod cnsl-secondary-text">
{{ 'FILTER.METHODS.1' | translate }}
</span>
<cnsl-form-field class="filter-select-value">
<mat-select [value]="sq.getState()" (selectionChange)="setValue(SubQuery.STATE, sq, $event)">
<mat-option *ngFor="let state of states" [value]="state">
{{ 'USER.STATE.' + state | translate }}
</mat-option>
</mat-select>
</cnsl-form-field>
</div>
</div>
</div>
</cnsl-filter>

View File

@@ -3,13 +3,14 @@ import { MatLegacyCheckboxChange as MatCheckboxChange } from '@angular/material/
import { ActivatedRoute, Router } from '@angular/router';
import { take } from 'rxjs';
import { TextQueryMethod } from 'src/app/proto/generated/zitadel/object_pb';
import { OrgNameQuery, OrgQuery, OrgState } from 'src/app/proto/generated/zitadel/org_pb';
import { OrgNameQuery, OrgQuery, OrgState, OrgStateQuery } from 'src/app/proto/generated/zitadel/org_pb';
import { UserNameQuery } from 'src/app/proto/generated/zitadel/user_pb';
import { FilterComponent } from '../filter/filter.component';
enum SubQuery {
NAME,
STATE,
}
@Component({
@@ -21,9 +22,9 @@ export class FilterOrgComponent extends FilterComponent implements OnInit {
public SubQuery: any = SubQuery;
public searchQueries: OrgQuery[] = [];
public states: OrgState[] = [OrgState.ORG_STATE_ACTIVE, OrgState.ORG_STATE_INACTIVE];
public states: OrgState[] = [OrgState.ORG_STATE_ACTIVE, OrgState.ORG_STATE_INACTIVE, OrgState.ORG_STATE_REMOVED];
constructor(router: Router, protected route: ActivatedRoute) {
constructor(router: Router, protected override route: ActivatedRoute) {
super(router, route);
}
@@ -37,13 +38,17 @@ export class FilterOrgComponent extends FilterComponent implements OnInit {
const orgQueries = filters.map((filter) => {
if (filter.nameQuery) {
const orgQuery = new OrgQuery();
const orgNameQuery = new OrgNameQuery();
orgNameQuery.setName(filter.nameQuery.name);
orgNameQuery.setMethod(filter.nameQuery.method);
orgQuery.setNameQuery(orgNameQuery);
return orgQuery;
} else if (filter.stateQuery) {
const orgQuery = new OrgQuery();
const orgStateQuery = new OrgStateQuery();
orgStateQuery.setState(filter.stateQuery.state);
orgQuery.setStateQuery(orgStateQuery);
return orgQuery;
} else {
return undefined;
}
@@ -64,12 +69,17 @@ export class FilterOrgComponent extends FilterComponent implements OnInit {
const nq = new OrgNameQuery();
nq.setMethod(TextQueryMethod.TEXT_QUERY_METHOD_CONTAINS_IGNORE_CASE);
nq.setName('');
const oq = new OrgQuery();
oq.setNameQuery(nq);
this.searchQueries.push(oq);
break;
case SubQuery.STATE:
const sq = new OrgStateQuery();
sq.setState(OrgState.ORG_STATE_ACTIVE);
const osq = new OrgQuery();
osq.setStateQuery(sq);
this.searchQueries.push(osq);
break;
}
} else {
switch (subquery) {
@@ -79,6 +89,12 @@ export class FilterOrgComponent extends FilterComponent implements OnInit {
this.searchQueries.splice(index_dn, 1);
}
break;
case SubQuery.STATE:
const index_sn = this.searchQueries.findIndex((q) => (q as OrgQuery).toObject().stateQuery !== undefined);
if (index_sn > -1) {
this.searchQueries.splice(index_sn, 1);
}
break;
}
}
}
@@ -90,6 +106,10 @@ export class FilterOrgComponent extends FilterComponent implements OnInit {
(query as OrgNameQuery).setName(value);
this.filterChanged.emit(this.searchQueries ? this.searchQueries : []);
break;
case SubQuery.STATE:
(query as OrgStateQuery).setState(value);
this.filterChanged.emit(this.searchQueries ? this.searchQueries : []);
break;
}
}
@@ -102,6 +122,13 @@ export class FilterOrgComponent extends FilterComponent implements OnInit {
} else {
return undefined;
}
case SubQuery.STATE:
const sn = this.searchQueries.find((q) => (q as OrgQuery).toObject().stateQuery !== undefined);
if (sn) {
return (sn as OrgQuery).getStateQuery();
} else {
return undefined;
}
}
}
@@ -110,7 +137,7 @@ export class FilterOrgComponent extends FilterComponent implements OnInit {
this.filterChanged.emit(this.searchQueries ? this.searchQueries : []);
}
public emitFilter(): void {
public override emitFilter(): void {
this.filterChanged.emit(this.searchQueries ? this.searchQueries : []);
this.showFilter = false;
this.filterOpen.emit(false);

View File

@@ -110,7 +110,7 @@ export class FilterProjectComponent extends FilterComponent implements OnInit {
this.filterChanged.emit(this.searchQueries ? this.searchQueries : []);
}
public emitFilter(): void {
public override emitFilter(): void {
this.filterChanged.emit(this.searchQueries ? this.searchQueries : []);
this.showFilter = false;
this.filterOpen.emit(false);

View File

@@ -226,7 +226,7 @@ export class FilterUserGrantsComponent extends FilterComponent implements OnInit
this.filterChanged.emit(this.searchQueries ? this.searchQueries : []);
}
public emitFilter(): void {
public override emitFilter(): void {
this.filterChanged.emit(this.searchQueries ? this.searchQueries : []);
this.showFilter = false;
this.filterOpen.emit(false);

View File

@@ -236,7 +236,7 @@ export class FilterUserComponent extends FilterComponent implements OnInit {
this.filterChanged.emit(this.searchQueries ? this.searchQueries : []);
}
public emitFilter(): void {
public override emitFilter(): void {
this.filterChanged.emit(this.searchQueries ? this.searchQueries : []);
this.showFilter = false;
this.filterOpen.emit(false);

View File

@@ -15,8 +15,6 @@
<ng-template
cdkConnectedOverlay
[cdkConnectedOverlayHasBackdrop]="true"
[flexibleDimensions]="true"
[lockPosition]="true"
[cdkConnectedOverlayOffsetY]="10"
[cdkConnectedOverlayPositions]="positions"
[cdkConnectedOverlayOrigin]="trigger"

View File

@@ -1,5 +1,14 @@
<ng-template #labelTemplate>
<label class="cnsl-label-wrapper" [attr.for]="_control.id" [attr.aria-owns]="_control.id">
<ng-content select="cnsl-label"></ng-content>
<span *ngIf="_control.required && !hideRequiredMarker" aria-hidden="true" class="cnsl-form-field-required-marker"
>*</span
>
</label>
</ng-template>
<div class="cnsl-form-field-wrapper" (click)="_control.onContainerClick && _control.onContainerClick($event)">
<ng-content select="cnsl-label"></ng-content>
<ng-template [ngTemplateOutlet]="labelTemplate"></ng-template>
<div class="cnsl-rel" #inputContainer>
<ng-content></ng-content>
<ng-content select="cnslSuffix"></ng-content>

View File

@@ -51,6 +51,7 @@ interface ValidationError {
'[class.ng-valid]': '_shouldForward("valid")',
'[class.ng-invalid]': '_shouldForward("invalid")',
'[class.ng-pending]': '_shouldForward("pending")',
'[class.ng-required]': '_control.required',
'[class.cnsl-form-field-disabled]': '_control.disabled',
'[class.cnsl-form-field-autofilled]': '_control.autofilled',
'[class.cnsl-focused]': '_control.focused',
@@ -69,6 +70,7 @@ export class CnslFormFieldComponent extends CnslFormFieldBase implements OnDestr
@ContentChild(MatFormFieldControl) _controlNonStatic!: MatFormFieldControl<any>;
@ContentChild(MatFormFieldControl, { static: true }) _controlStatic!: MatFormFieldControl<any>;
@Input() public disableValidationErrors = false;
@Input() public hideRequiredMarker = false;
get _control(): MatFormFieldControl<any> {
return this._explicitFormFieldControl || this._controlNonStatic || this._controlStatic;
@@ -95,7 +97,7 @@ export class CnslFormFieldComponent extends CnslFormFieldBase implements OnDestr
}
constructor(
public _elementRef: ElementRef,
public override _elementRef: ElementRef,
private _changeDetectorRef: ChangeDetectorRef,
@Inject(ElementRef)
_labelOptions: // Use `ElementRef` here so Angular has something to inject.

View File

@@ -24,6 +24,12 @@ export function requiredValidator(c: AbstractControl): ValidationErrors | null {
return i18nErr(Validators.required(c), 'ERRORS.REQUIRED');
}
export function minArrayLengthValidator(minArrLength: number): ValidatorFn {
return (c: AbstractControl): ValidationErrors | null => {
return arrayLengthValidator(c, minArrLength, 'ERRORS.ATLEASTONE');
};
}
export function emailValidator(c: AbstractControl): ValidationErrors | null {
return i18nErr(Validators.email(c), 'ERRORS.NOTANEMAIL');
}
@@ -56,6 +62,12 @@ function regexpValidator(c: AbstractControl, regexp: RegExp, i18nKey: string): V
return !c.value || regexp.test(c.value) ? null : i18nErr({ invalid: true }, i18nKey, { regexp: regexp });
}
function arrayLengthValidator(c: AbstractControl, length: number, i18nKey: string): ValidationErrors | null {
const arr: string[] = c.value;
const invalidStrings: string[] = arr.filter((val: string) => val.trim() === '');
return arr && invalidStrings.length === 0 && arr.length >= length ? null : i18nErr({ invalid: true }, i18nKey);
}
function i18nErr(err: ValidationErrors | null | undefined, i18nKey: string, params?: any): ValidationErrors | null {
if (err === null) {
return null;

View File

@@ -110,8 +110,6 @@
<ng-template
cdkConnectedOverlay
[cdkConnectedOverlayOrigin]="trigger"
[flexibleDimensions]="true"
[lockPosition]="true"
[cdkConnectedOverlayOffsetY]="10"
[cdkConnectedOverlayHasBackdrop]="true"
[cdkConnectedOverlayPositions]="positions"
@@ -197,9 +195,7 @@
</a>
</div>
<ng-container
*ngIf="user && (user.human?.profile?.displayName || (user.human?.profile?.firstName && user.human?.profile?.lastName))"
>
<ng-container *ngIf="user">
<div class="account-card-wrapper">
<button
cdkOverlayOrigin
@@ -214,11 +210,7 @@
[active]="showAccount"
[avatarUrl]="user.human?.profile?.avatarUrl || ''"
[forColor]="user.preferredLoginName || ''"
[name]="
user.human?.profile?.displayName
? user.human?.profile?.displayName ?? ''
: user.human?.profile?.firstName + ' ' + user.human?.profile?.lastName
"
[name]="user.human?.profile?.displayName ?? ''"
[size]="38"
>
</cnsl-avatar>
@@ -227,8 +219,6 @@
<ng-template
cdkConnectedOverlay
[cdkConnectedOverlayOrigin]="accounttrigger"
[flexibleDimensions]="true"
[lockPosition]="true"
[cdkConnectedOverlayOffsetY]="10"
[cdkConnectedOverlayHasBackdrop]="true"
[cdkConnectedOverlayPositions]="accountCardPositions"

View File

@@ -199,7 +199,7 @@ export class InputDirective
protected _type: string = 'text';
/** An object used to control when error messages are shown. */
@Input() errorStateMatcher!: ErrorStateMatcher;
@Input() override errorStateMatcher!: ErrorStateMatcher;
/**
* Implemented as part of MatFormFieldControl.
@@ -241,7 +241,7 @@ export class InputDirective
protected _elementRef: ElementRef<HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement>,
protected _platform: Platform,
/** @docs-private */
@Optional() @Self() public ngControl: NgControl,
@Optional() @Self() public override ngControl: NgControl,
@Optional() _parentForm: NgForm,
@Optional() _parentFormGroup: FormGroupDirective,
_defaultErrorStateMatcher: ErrorStateMatcher,

View File

@@ -9,23 +9,31 @@
$foreground: map-get($theme, foreground);
$secondary-text: map-get($foreground, secondary-text);
.cnsl-label {
display: block;
.cnsl-label-wrapper {
display: flex;
font-size: 12px;
color: $secondary-text;
transition: color 0.2s ease;
margin-bottom: 4px;
font-weight: 400;
.cnsl-label {
display: block;
}
.cnsl-form-field-required-marker {
margin-left: 1px;
}
}
.cnsl-form-field-disabled {
.cnsl-label {
.cnsl-label-wrapper {
color: if($is-dark-theme, #ffffff80, #00000061);
}
}
.cnsl-form-field-invalid {
.cnsl-label {
.cnsl-label-wrapper {
color: $warn-color;
}
}

View File

@@ -36,7 +36,7 @@
[checked]="selection.isSelected(row)"
>
<cnsl-avatar
*ngIf="row?.displayName && row.firstName && row.lastName; else cog"
*ngIf="row?.userType === UserType.TYPE_HUMAN; else cog"
class="avatar"
[name]="row.displayName"
[avatarUrl]="row.avatarUrl || ''"

View File

@@ -10,6 +10,7 @@ import { ProjectGrantMembersDataSource } from 'src/app/pages/projects/owned-proj
import { Member } from 'src/app/proto/generated/zitadel/member_pb';
import { getMembershipColor } from 'src/app/utils/color';
import { Type } from 'src/app/proto/generated/zitadel/user_pb';
import { AddMemberRolesDialogComponent } from '../add-member-roles-dialog/add-member-roles-dialog.component';
import { PageEvent, PaginatorComponent } from '../paginator/paginator.component';
import { ProjectMembersDataSource } from '../project-members/project-members-datasource';
@@ -43,6 +44,7 @@ export class MembersTableComponent implements OnInit, OnDestroy {
private destroyed: Subject<void> = new Subject();
public displayedColumns: string[] = ['select', 'userId', 'displayName', 'loginname', 'email', 'roles'];
public UserType: any = Type;
constructor(private dialog: MatDialog) {
this.selection.changed.pipe(takeUntil(this.destroyed)).subscribe((_) => {

View File

@@ -27,8 +27,8 @@ export class MetadataComponent implements OnChanges {
constructor() {}
ngOnChanges(changes: SimpleChanges): void {
if (changes.metadata?.currentValue) {
this.dataSource = new MatTableDataSource<Metadata.AsObject>(changes.metadata.currentValue);
if (changes['metadata']?.currentValue) {
this.dataSource = new MatTableDataSource<Metadata.AsObject>(changes['metadata'].currentValue);
}
}
}

View File

@@ -111,7 +111,7 @@
<span class="label">{{ 'MENU.DASHBOARD' | translate }}</span>
</a>
<ng-container class="org-list" *ngIf="org" [@navAnimation]="org">
<ng-container class="org-list" *ngIf="org">
<ng-template cnslHasRole [hasRole]="['org.read']">
<a
class="nav-item"
@@ -232,8 +232,6 @@
<ng-template
cdkConnectedOverlay
[cdkConnectedOverlayOrigin]="trigger"
[flexibleDimensions]="true"
[lockPosition]="true"
[cdkConnectedOverlayOffsetY]="10"
[cdkConnectedOverlayHasBackdrop]="true"
[cdkConnectedOverlayPositions]="positions"

View File

@@ -189,6 +189,7 @@
border: none;
padding: 2px;
border-radius: 50vw;
cursor: pointer;
&:hover {
background: if($is-dark-theme, #ffffff, $primary-color);

View File

@@ -72,6 +72,7 @@
margin-right: 1rem;
background-color: if($is-dark-theme, map-get($background, state), #e4e7e4);
box-shadow: 0 0 3px #0000001a;
border: 1px solid rgba(#8795a1, 0.2);
i {
font-size: 1rem;

View File

@@ -1,7 +1,5 @@
<div class="onboarding-header">
<h1 class="title" data-e2e="authenticated-welcome">{{ 'HOME.WELCOME' | translate }}</h1>
<p class="desc cnsl-secondary-text">{{ 'ONBOARDING.DESCRIPTION' | translate }}</p>
<h2 class="desc">{{ 'ONBOARDING.DESCRIPTION' | translate }}</h2>
<ng-container *ngIf="!adminService.hideOnboarding && (adminService.progressAllDone | async) === false">
<div class="onboarding-progress-bar-wrapper">
@@ -38,16 +36,34 @@
</div>
<div class="action-content">
<div class="text-block">
<span class="name">{{ 'ONBOARDING.EVENTS.' + action[0] + '.title' | translate }}</span>
<span class="cnsl-secondary-text description">{{
'ONBOARDING.EVENTS.' + action[0] + '.description' | translate
}}</span>
<div class="action-content-row">
<div
class="icon-wrapper"
[ngStyle]="{
background: (themeService.isDarkTheme | async) ? action[1].darkcolor + 50 : action[1].lightcolor + 50
}"
>
<div
class="inner"
[ngStyle]="{
background: (themeService.isDarkTheme | async) ? action[1].darkcolor : action[1].lightcolor,
color: (themeService.isDarkTheme | async) ? action[1].lightcolor : action[1].darkcolor
}"
>
<i class="{{ action[1].iconClasses }}"></i>
</div>
</div>
<div class="text-block">
<span class="name">{{ 'ONBOARDING.EVENTS.' + action[0] + '.title' | translate }}</span>
<span class="cnsl-secondary-text description">{{
'ONBOARDING.EVENTS.' + action[0] + '.description' | translate
}}</span>
</div>
</div>
<span class="fill-space"></span>
<div class="action-row">
<span>{{ 'ACTIONS.SETUP' | translate }}</span>
<span>{{ 'ONBOARDING.EVENTS.' + action[0] + '.action' | translate }}</span>
<mat-icon class="icon">keyboard_arrow_right</mat-icon>
</div>
</div>

View File

@@ -23,14 +23,10 @@
flex-direction: column;
margin-bottom: 2rem;
.title {
font-size: 2rem;
margin-bottom: 1rem;
}
.desc {
font-size: 14px;
font-size: 1.2rem;
margin-top: 0;
text-transform: uppercase;
}
.onboarding-progress-bar-wrapper {
@@ -71,7 +67,6 @@
.action-card {
position: relative;
z-index: 100;
margin: 1rem;
flex-basis: 270px;
text-decoration: none;
@@ -91,18 +86,46 @@
height: 100%;
padding-right: 0.5rem;
.text-block {
.action-content-row {
display: flex;
flex-direction: column;
color: map-get($foreground, text);
padding-top: 1rem;
flex-direction: row;
align-items: flex-start;
.name {
margin-bottom: 1rem;
.icon-wrapper {
display: flex;
justify-content: center;
align-items: center;
padding: 0.5rem;
border-radius: 50vw;
flex-shrink: 0;
margin-right: 1rem;
margin-top: 1rem;
.inner {
border-radius: 50vw;
height: 40px;
width: 40px;
display: flex;
justify-content: center;
align-items: center;
font-size: 1.75rem;
}
}
.description {
font-size: 14px;
.text-block {
display: flex;
flex-direction: column;
color: map-get($foreground, text);
padding-top: 1rem;
flex: 1;
.name {
margin-bottom: 1rem;
}
.description {
font-size: 14px;
}
}
}
@@ -111,6 +134,12 @@
}
}
&.done {
.action-content {
opacity: 0.5;
}
}
.state-circle {
position: absolute;
top: 0;
@@ -126,6 +155,7 @@
margin-right: 1rem;
background-color: if($is-dark-theme, map-get($background, state), #e4e7e4);
box-shadow: 0 0 3px #0000001a;
border: 1px solid rgba(#8795a1, 0.2);
i {
font-size: 1rem;
@@ -168,6 +198,12 @@
.state-circle {
display: none;
}
.action-card {
.action-content {
opacity: 1;
}
}
}
}
}

View File

@@ -1,5 +1,6 @@
import { Component } from '@angular/core';
import { AdminService } from 'src/app/services/admin.service';
import { ThemeService } from 'src/app/services/theme.service';
import { ONBOARDING_EVENTS } from 'src/app/utils/onboarding';
@Component({
@@ -10,7 +11,7 @@ import { ONBOARDING_EVENTS } from 'src/app/utils/onboarding';
export class OnboardingComponent {
public actions = this.adminService.progressEvents;
constructor(public adminService: AdminService) {
constructor(public adminService: AdminService, public themeService: ThemeService) {
this.adminService.loadEvents.next(ONBOARDING_EVENTS);
}
}

View File

@@ -3,7 +3,7 @@ import { Component, ElementRef, EventEmitter, Input, OnInit, Output, ViewChild }
import { UntypedFormControl } from '@angular/forms';
import { BehaviorSubject, catchError, debounceTime, finalize, from, map, Observable, of, pipe, tap } from 'rxjs';
import { TextQueryMethod } from 'src/app/proto/generated/zitadel/object_pb';
import { Org, OrgNameQuery, OrgQuery } from 'src/app/proto/generated/zitadel/org_pb';
import { Org, OrgNameQuery, OrgQuery, OrgState, OrgStateQuery } from 'src/app/proto/generated/zitadel/org_pb';
import { AuthenticationService } from 'src/app/services/authentication.service';
import { GrpcAuthService } from 'src/app/services/grpc-auth.service';
@@ -47,9 +47,12 @@ export class OrgContextComponent implements OnInit {
}
}
let query;
let query = new OrgQuery();
const orgStateQuery = new OrgStateQuery();
orgStateQuery.setState(OrgState.ORG_STATE_ACTIVE);
query.setStateQuery(orgStateQuery);
if (filter) {
query = new OrgQuery();
const orgNameQuery = new OrgNameQuery();
orgNameQuery.setName(filter);
orgNameQuery.setMethod(TextQueryMethod.TEXT_QUERY_METHOD_CONTAINS_IGNORE_CASE);

View File

@@ -4,7 +4,10 @@
<ng-template #showSpinner>
<div
*ngIf="password?.errors?.minlength || password?.value?.length === 0 as currentError; else trueminlength"
*ngIf="
(password && password.errors && password.errors['minlength']) || password?.value?.length === 0 as currentError;
else trueminlength
"
class="complexity-sp-wrapper"
>
<mat-progress-spinner
@@ -28,23 +31,23 @@
</span>
</div>
<div class="val" *ngIf="this.policy.hasSymbol">
<i *ngIf="password?.pristine || password?.errors?.errorssymbolerror" class="las la-times red"></i>
<i *ngIf="password?.dirty && !password?.errors?.errorssymbolerror" class="las la-check green"></i>
<i *ngIf="password?.pristine || password?.errors?.['errorssymbolerror']" class="las la-times red"></i>
<i *ngIf="password?.dirty && !password?.errors?.['errorssymbolerror']" class="las la-check green"></i>
<span class="cnsl-secondary-text"> {{ 'ERRORS.SYMBOLERROR' | translate }}</span>
</div>
<div class="val" *ngIf="this.policy.hasNumber">
<i *ngIf="password?.pristine || password?.errors?.errorsnumbererror" class="las la-times red"></i>
<i *ngIf="password?.dirty && !password?.errors?.errorsnumbererror" class="las la-check green"></i>
<i *ngIf="password?.pristine || password?.errors?.['errorsnumbererror']" class="las la-times red"></i>
<i *ngIf="password?.dirty && !password?.errors?.['errorsnumbererror']" class="las la-check green"></i>
<span class="cnsl-secondary-text"> {{ 'ERRORS.NUMBERERROR' | translate }}</span>
</div>
<div class="val" *ngIf="this.policy.hasUppercase">
<i *ngIf="password?.pristine || password?.errors?.errorsuppercasemissing" class="las la-times red"></i>
<i *ngIf="password?.dirty && !password?.errors?.errorsuppercasemissing" class="las la-check green"></i>
<i *ngIf="password?.pristine || password?.errors?.['errorsuppercasemissing']" class="las la-times red"></i>
<i *ngIf="password?.dirty && !password?.errors?.['errorsuppercasemissing']" class="las la-check green"></i>
<span class="cnsl-secondary-text"> {{ 'ERRORS.UPPERCASEMISSING' | translate }}</span>
</div>
<div class="val" *ngIf="this.policy.hasLowercase">
<i *ngIf="password?.pristine || password?.errors?.errorslowercasemissing" class="las la-times red"></i>
<i *ngIf="password?.dirty && !password?.errors?.errorslowercasemissing" class="las la-check green"></i>
<i *ngIf="password?.pristine || password?.errors?.['errorslowercasemissing']" class="las la-times red"></i>
<i *ngIf="password?.dirty && !password?.errors?.['errorslowercasemissing']" class="las la-check green"></i>
<span class="cnsl-secondary-text">{{ 'ERRORS.LOWERCASEMISSING' | translate }}</span>
</div>
</div>

View File

@@ -109,7 +109,7 @@ export class LoginTextsComponent implements OnInit, OnDestroy {
@Input() public serviceType: PolicyComponentServiceType = PolicyComponentServiceType.MGMT;
public KeyNamesArray: string[] = KeyNamesArray;
public LOCALES: string[] = ['de', 'en', 'fr', 'it', 'ja', 'pl', 'zh'];
public LOCALES: string[] = ['de', 'en', 'es', 'fr', 'it', 'ja', 'pl', 'zh'];
private sub: Subscription = new Subscription();

View File

@@ -46,7 +46,7 @@
<div class="message-text-actions">
<button
class="reset-button"
*ngIf="(getCustomInitMessageTextMap$ | async) && (getCustomInitMessageTextMap$ | async)?.isDefault === false"
*ngIf="(getCustomInitMessageTextMap$ | async) && (getCustomInitMessageTextMap$ | async)?.['isDefault'] === false"
[disabled]="(canWrite$ | async) === false"
(click)="resetDefault()"
color="message-text-warn"

View File

@@ -441,7 +441,7 @@ export class MessageTextsComponent implements OnInit, OnDestroy {
};
public locale: string = 'en';
public LOCALES: string[] = ['de', 'en', 'fr', 'it', 'ja', 'pl', 'zh'];
public LOCALES: string[] = ['de', 'en', 'es', 'fr', 'it', 'ja', 'pl', 'zh'];
private sub: Subscription = new Subscription();
public canWrite$: Observable<boolean> = this.authService.isAllowed([
this.serviceType === PolicyComponentServiceType.ADMIN

View File

@@ -45,13 +45,13 @@ export class ProjectMembersComponent {
private route: ActivatedRoute,
) {
this.route.data.pipe(take(1)).subscribe((data) => {
this.projectType = data.type;
this.projectType = data['type'];
this.getRoleOptions();
this.route.params.subscribe((params) => {
this.projectId = params.projectid;
this.grantId = params.grantid;
this.projectId = params['projectid'];
this.grantId = params['grantid'];
this.loadMembers();
});
});

View File

@@ -1,54 +1,65 @@
<form [formGroup]="form" class="attribute-form">
<cnsl-form-field class="formfield">
<cnsl-label>{{ 'IDP.LDAPIDATTRIBUTE' | translate }}*</cnsl-label>
<input cnslInput formControlName="idAttribute" />
</cnsl-form-field>
<cnsl-form-field class="formfield">
<cnsl-label>{{ 'IDP.AVATARURLATTRIBUTE' | translate }}</cnsl-label>
<input cnslInput formControlName="avatarUrlAttribute" />
</cnsl-form-field>
<cnsl-form-field class="formfield">
<cnsl-label>{{ 'IDP.DISPLAYNAMEATTRIBUTE' | translate }}</cnsl-label>
<input cnslInput formControlName="displayNameAttribute" />
</cnsl-form-field>
<cnsl-form-field class="formfield">
<cnsl-label>{{ 'IDP.EMAILATTRIBUTEATTRIBUTE' | translate }}</cnsl-label>
<input cnslInput formControlName="emailAttribute" />
</cnsl-form-field>
<cnsl-form-field class="formfield">
<cnsl-label>{{ 'IDP.EMAILVERIFIEDATTRIBUTE' | translate }}</cnsl-label>
<input cnslInput formControlName="emailVerifiedAttribute" />
</cnsl-form-field>
<cnsl-form-field class="formfield">
<cnsl-label>{{ 'IDP.FIRSTNAMEATTRIBUTE' | translate }}</cnsl-label>
<input cnslInput formControlName="firstNameAttribute" />
</cnsl-form-field>
<cnsl-form-field class="formfield">
<cnsl-label>{{ 'IDP.LASTNAMEATTRIBUTE' | translate }}</cnsl-label>
<input cnslInput formControlName="lastNameAttribute" />
</cnsl-form-field>
<cnsl-form-field class="formfield">
<cnsl-label>{{ 'IDP.NICKNAMEATTRIBUTE' | translate }}</cnsl-label>
<input cnslInput formControlName="nickNameAttribute" />
</cnsl-form-field>
<cnsl-form-field class="formfield">
<cnsl-label>{{ 'IDP.PHONEATTRIBUTE' | translate }}</cnsl-label>
<input cnslInput formControlName="phoneAttribute" />
</cnsl-form-field>
<cnsl-form-field class="formfield">
<cnsl-label>{{ 'IDP.PHONEVERIFIEDATTRIBUTE' | translate }}</cnsl-label>
<input cnslInput formControlName="phoneVerifiedAttribute" />
</cnsl-form-field>
<cnsl-form-field class="formfield">
<cnsl-label>{{ 'IDP.PREFERREDLANGUAGEATTRIBUTE' | translate }}</cnsl-label>
<input cnslInput formControlName="preferredLanguageAttribute" />
</cnsl-form-field>
<cnsl-form-field class="formfield">
<cnsl-label>{{ 'IDP.PREFERREDUSERNAMEATTRIBUTE' | translate }}</cnsl-label>
<input cnslInput formControlName="preferredUsernameAttribute" />
</cnsl-form-field>
<cnsl-form-field class="formfield">
<cnsl-label>{{ 'IDP.PROFILEATTRIBUTE' | translate }}</cnsl-label>
<input cnslInput formControlName="profileAttribute" />
<cnsl-label>{{ 'IDP.LDAPIDATTRIBUTE' | translate }}</cnsl-label>
<input cnslInput formControlName="idAttribute" required />
</cnsl-form-field>
<div class="attribute-more-row">
<span>{{ 'ACTIONS.MORE' | translate }}</span>
<button (click)="showMore = !showMore" type="button" mat-icon-button>
<mat-icon *ngIf="showMore">keyboard_arrow_up</mat-icon>
<mat-icon *ngIf="!showMore">keyboard_arrow_down</mat-icon>
</button>
</div>
<ng-container *ngIf="showMore">
<cnsl-form-field class="formfield">
<cnsl-label>{{ 'IDP.AVATARURLATTRIBUTE' | translate }}</cnsl-label>
<input cnslInput formControlName="avatarUrlAttribute" />
</cnsl-form-field>
<cnsl-form-field class="formfield">
<cnsl-label>{{ 'IDP.DISPLAYNAMEATTRIBUTE' | translate }}</cnsl-label>
<input cnslInput formControlName="displayNameAttribute" />
</cnsl-form-field>
<cnsl-form-field class="formfield">
<cnsl-label>{{ 'IDP.EMAILATTRIBUTEATTRIBUTE' | translate }}</cnsl-label>
<input cnslInput formControlName="emailAttribute" />
</cnsl-form-field>
<cnsl-form-field class="formfield">
<cnsl-label>{{ 'IDP.EMAILVERIFIEDATTRIBUTE' | translate }}</cnsl-label>
<input cnslInput formControlName="emailVerifiedAttribute" />
</cnsl-form-field>
<cnsl-form-field class="formfield">
<cnsl-label>{{ 'IDP.FIRSTNAMEATTRIBUTE' | translate }}</cnsl-label>
<input cnslInput formControlName="firstNameAttribute" />
</cnsl-form-field>
<cnsl-form-field class="formfield">
<cnsl-label>{{ 'IDP.LASTNAMEATTRIBUTE' | translate }}</cnsl-label>
<input cnslInput formControlName="lastNameAttribute" />
</cnsl-form-field>
<cnsl-form-field class="formfield">
<cnsl-label>{{ 'IDP.NICKNAMEATTRIBUTE' | translate }}</cnsl-label>
<input cnslInput formControlName="nickNameAttribute" />
</cnsl-form-field>
<cnsl-form-field class="formfield">
<cnsl-label>{{ 'IDP.PHONEATTRIBUTE' | translate }}</cnsl-label>
<input cnslInput formControlName="phoneAttribute" />
</cnsl-form-field>
<cnsl-form-field class="formfield">
<cnsl-label>{{ 'IDP.PHONEVERIFIEDATTRIBUTE' | translate }}</cnsl-label>
<input cnslInput formControlName="phoneVerifiedAttribute" />
</cnsl-form-field>
<cnsl-form-field class="formfield">
<cnsl-label>{{ 'IDP.PREFERREDLANGUAGEATTRIBUTE' | translate }}</cnsl-label>
<input cnslInput formControlName="preferredLanguageAttribute" />
</cnsl-form-field>
<cnsl-form-field class="formfield">
<cnsl-label>{{ 'IDP.PREFERREDUSERNAMEATTRIBUTE' | translate }}</cnsl-label>
<input cnslInput formControlName="preferredUsernameAttribute" />
</cnsl-form-field>
<cnsl-form-field class="formfield">
<cnsl-label>{{ 'IDP.PROFILEATTRIBUTE' | translate }}</cnsl-label>
<input cnslInput formControlName="profileAttribute" />
</cnsl-form-field>
</ng-container>
</form>

View File

@@ -4,3 +4,8 @@
max-width: 400px;
padding-bottom: 1rem;
}
.attribute-more-row {
display: flex;
align-items: center;
}

View File

@@ -29,6 +29,7 @@ export class LDAPAttributesComponent implements OnChanges, OnDestroy {
profileAttribute: new FormControl('', []),
});
public showMore: boolean = false;
constructor() {
this.form.valueChanges.pipe(takeUntil(this.destroy$)).subscribe((value) => {
if (value) {

View File

@@ -25,9 +25,14 @@
<input cnslInput formControlName="clientId" />
</cnsl-form-field>
<mat-checkbox *ngIf="provider" [(ngModel)]="updateClientSecret" [ngModelOptions]="{ standalone: true }">{{
'IDP.UPDATECLIENTSECRET' | translate
}}</mat-checkbox>
<mat-checkbox
class="update-secret-checkbox"
*ngIf="provider"
[(ngModel)]="updateClientSecret"
[ngModelOptions]="{ standalone: true }"
>{{ 'IDP.UPDATECLIENTSECRET' | translate }}</mat-checkbox
>
<cnsl-form-field *ngIf="!provider || (provider && updateClientSecret)" class="formfield">
<cnsl-label>{{ 'IDP.CLIENTSECRET' | translate }}</cnsl-label>
<input cnslInput formControlName="clientSecret" />
@@ -85,7 +90,7 @@
</mat-select>
</cnsl-form-field>
<cnsl-form-field class="formfield">
<cnsl-form-field class="formfield" *ngIf="tenantType?.value === AzureTenantIDType">
<cnsl-label>{{ 'IDP.AZUREADTENANTID' | translate }}</cnsl-label>
<input cnslInput formControlName="tenantId" />
</cnsl-form-field>

View File

@@ -44,7 +44,10 @@ export class ProviderAzureADComponent {
public provider?: Provider.AsObject;
public updateClientSecret: boolean = false;
public AzureTenantIDType: number = 3;
public tenantTypes = [
this.AzureTenantIDType,
AzureADTenantType.AZURE_AD_TENANT_TYPE_COMMON,
AzureADTenantType.AZURE_AD_TENANT_TYPE_ORGANISATIONS,
AzureADTenantType.AZURE_AD_TENANT_TYPE_CONSUMERS,
@@ -86,7 +89,7 @@ export class ProviderAzureADComponent {
});
this.route.data.pipe(take(1)).subscribe((data) => {
this.serviceType = data.serviceType;
this.serviceType = data['serviceType'];
switch (this.serviceType) {
case PolicyComponentServiceType.MGMT:
@@ -126,15 +129,34 @@ export class ProviderAzureADComponent {
: new MgmtGetProviderByIDRequest();
req.setId(id);
this.service
.getProviderByID(req)
.getProviderID(req)
.then((resp) => {
this.provider = resp.idp;
const object = resp.toObject();
this.provider = object.idp;
this.loading = false;
if (this.provider?.config?.azureAd) {
this.form.patchValue(this.provider.config.azureAd);
this.name?.setValue(this.provider.name);
this.tenantId?.setValue(this.provider.config.azureAd.tenant?.tenantId);
this.tenantType?.setValue(this.provider.config.azureAd.tenant?.tenantType);
const tenant = resp.getIdp()?.getConfig()?.getAzureAd()?.getTenant();
if (tenant) {
switch (tenant.getTypeCase()) {
case AzureADTenant.TypeCase.TENANT_ID:
this.tenantId?.setValue(tenant.getTenantId());
this.tenantType?.setValue(this.AzureTenantIDType);
break;
case AzureADTenant.TypeCase.TENANT_TYPE:
this.tenantType?.setValue(tenant.getTenantType());
this.tenantId?.setValue('');
break;
case AzureADTenant.TypeCase.TYPE_NOT_SET:
this.tenantType?.setValue(this.AzureTenantIDType);
break;
}
}
}
})
.catch((error) => {
@@ -159,8 +181,11 @@ export class ProviderAzureADComponent {
req.setEmailVerified(this.emailVerified?.value);
const tenant = new AzureADTenant();
tenant.setTenantId(this.tenantId?.value);
tenant.setTenantType(this.tenantType?.value);
if (this.tenantType?.value === this.AzureTenantIDType) {
tenant.setTenantId(this.tenantId?.value);
} else {
tenant.setTenantType(this.tenantType?.value);
}
req.setTenant(tenant);
req.setScopesList(this.scopesList?.value);
@@ -194,9 +219,11 @@ export class ProviderAzureADComponent {
req.setEmailVerified(this.emailVerified?.value);
const tenant = new AzureADTenant();
tenant.setTenantId(this.tenantId?.value);
tenant.setTenantType(this.tenantType?.value);
if (this.tenantType?.value === this.AzureTenantIDType) {
tenant.setTenantId(this.tenantId?.value);
} else {
tenant.setTenantType(this.tenantType?.value);
}
req.setTenant(tenant);
req.setScopesList(this.scopesList?.value);

View File

@@ -40,9 +40,14 @@
<input cnslInput formControlName="clientId" />
</cnsl-form-field>
<mat-checkbox *ngIf="provider" [(ngModel)]="updateClientSecret" [ngModelOptions]="{ standalone: true }">{{
'IDP.UPDATECLIENTSECRET' | translate
}}</mat-checkbox>
<mat-checkbox
class="update-secret-checkbox"
*ngIf="provider"
[(ngModel)]="updateClientSecret"
[ngModelOptions]="{ standalone: true }"
>{{ 'IDP.UPDATECLIENTSECRET' | translate }}</mat-checkbox
>
<cnsl-form-field *ngIf="!provider || (provider && updateClientSecret)" class="formfield">
<cnsl-label>{{ 'IDP.CLIENTSECRET' | translate }}</cnsl-label>
<input cnslInput formControlName="clientSecret" />

View File

@@ -80,7 +80,7 @@ export class ProviderGithubESComponent {
});
this.route.data.pipe(take(1)).subscribe((data) => {
this.serviceType = data.serviceType;
this.serviceType = data['serviceType'];
switch (this.serviceType) {
case PolicyComponentServiceType.MGMT:
@@ -153,6 +153,7 @@ export class ProviderGithubESComponent {
req.setClientId(this.clientId?.value);
req.setClientSecret(this.clientSecret?.value);
req.setScopesList(this.scopesList?.value);
req.setProviderOptions(this.options);
this.loading = true;
this.service
@@ -183,6 +184,7 @@ export class ProviderGithubESComponent {
req.setClientId(this.clientId?.value);
req.setClientSecret(this.clientSecret?.value);
req.setScopesList(this.scopesList?.value);
req.setProviderOptions(this.options);
this.loading = true;
this.service

View File

@@ -21,9 +21,14 @@
<input cnslInput formControlName="clientId" />
</cnsl-form-field>
<mat-checkbox *ngIf="provider" [(ngModel)]="updateClientSecret" [ngModelOptions]="{ standalone: true }">{{
'IDP.UPDATECLIENTSECRET' | translate
}}</mat-checkbox>
<mat-checkbox
class="update-secret-checkbox"
*ngIf="provider"
[(ngModel)]="updateClientSecret"
[ngModelOptions]="{ standalone: true }"
>{{ 'IDP.UPDATECLIENTSECRET' | translate }}</mat-checkbox
>
<cnsl-form-field *ngIf="!provider || (provider && updateClientSecret)" class="formfield">
<cnsl-label>{{ 'IDP.CLIENTSECRET' | translate }}</cnsl-label>
<input cnslInput formControlName="clientSecret" />

View File

@@ -78,7 +78,7 @@ export class ProviderGithubComponent {
});
this.route.data.pipe(take(1)).subscribe((data) => {
this.serviceType = data.serviceType;
this.serviceType = data['serviceType'];
switch (this.serviceType) {
case PolicyComponentServiceType.MGMT:

View File

@@ -30,9 +30,14 @@
<input cnslInput formControlName="clientId" />
</cnsl-form-field>
<mat-checkbox *ngIf="provider" [(ngModel)]="updateClientSecret" [ngModelOptions]="{ standalone: true }">{{
'IDP.UPDATECLIENTSECRET' | translate
}}</mat-checkbox>
<mat-checkbox
class="update-secret-checkbox"
*ngIf="provider"
[(ngModel)]="updateClientSecret"
[ngModelOptions]="{ standalone: true }"
>{{ 'IDP.UPDATECLIENTSECRET' | translate }}</mat-checkbox
>
<cnsl-form-field *ngIf="!provider || (provider && updateClientSecret)" class="formfield">
<cnsl-label>{{ 'IDP.CLIENTSECRET' | translate }}</cnsl-label>
<input cnslInput formControlName="clientSecret" />

View File

@@ -79,7 +79,7 @@ export class ProviderGitlabSelfHostedComponent {
});
this.route.data.pipe(take(1)).subscribe((data) => {
this.serviceType = data.serviceType;
this.serviceType = data['serviceType'];
switch (this.serviceType) {
case PolicyComponentServiceType.MGMT:

View File

@@ -20,9 +20,14 @@
<input cnslInput formControlName="clientId" />
</cnsl-form-field>
<mat-checkbox *ngIf="provider" [(ngModel)]="updateClientSecret" [ngModelOptions]="{ standalone: true }">{{
'IDP.UPDATECLIENTSECRET' | translate
}}</mat-checkbox>
<mat-checkbox
class="update-secret-checkbox"
*ngIf="provider"
[(ngModel)]="updateClientSecret"
[ngModelOptions]="{ standalone: true }"
>{{ 'IDP.UPDATECLIENTSECRET' | translate }}</mat-checkbox
>
<cnsl-form-field *ngIf="!provider || (provider && updateClientSecret)" class="formfield">
<cnsl-label>{{ 'IDP.CLIENTSECRET' | translate }}</cnsl-label>
<input cnslInput formControlName="clientSecret" />

View File

@@ -78,7 +78,7 @@ export class ProviderGitlabComponent {
});
this.route.data.pipe(take(1)).subscribe((data) => {
this.serviceType = data.serviceType;
this.serviceType = data['serviceType'];
switch (this.serviceType) {
case PolicyComponentServiceType.MGMT:

View File

@@ -20,9 +20,13 @@
<input cnslInput formControlName="clientId" />
</cnsl-form-field>
<mat-checkbox *ngIf="provider" [(ngModel)]="updateClientSecret" [ngModelOptions]="{ standalone: true }">{{
'IDP.UPDATECLIENTSECRET' | translate
}}</mat-checkbox>
<mat-checkbox
class="update-secret-checkbox"
*ngIf="provider"
[(ngModel)]="updateClientSecret"
[ngModelOptions]="{ standalone: true }"
>{{ 'IDP.UPDATECLIENTSECRET' | translate }}</mat-checkbox
>
<cnsl-form-field *ngIf="!provider || (provider && updateClientSecret)" class="formfield">
<cnsl-label>{{ 'IDP.CLIENTSECRET' | translate }}</cnsl-label>
<input cnslInput formControlName="clientSecret" />

View File

@@ -78,7 +78,7 @@ export class ProviderGoogleComponent {
});
this.route.data.pipe(take(1)).subscribe((data) => {
this.serviceType = data.serviceType;
this.serviceType = data['serviceType'];
switch (this.serviceType) {
case PolicyComponentServiceType.MGMT:

View File

@@ -51,7 +51,7 @@ export class ProviderJWTComponent {
breadcrumbService: BreadcrumbService,
) {
this.route.data.pipe(take(1)).subscribe((data) => {
this.serviceType = data.serviceType;
this.serviceType = data['serviceType'];
switch (this.serviceType) {
case PolicyComponentServiceType.MGMT:

View File

@@ -17,12 +17,13 @@
<div class="identity-provider-content">
<cnsl-form-field class="formfield">
<cnsl-label>{{ 'IDP.NAME' | translate }}</cnsl-label>
<input cnslInput formControlName="name" />
<input cnslInput formControlName="name" required />
</cnsl-form-field>
<h2 class="subheader">{{ 'IDP.LDAPCONNECTION' | translate }}</h2>
<cnsl-string-list
class="string-list-component-wrapper"
title="{{ 'IDP.SERVERS' | translate }}"
formControlName="serversList"
[required]="true"
@@ -30,21 +31,31 @@
<cnsl-form-field class="formfield">
<cnsl-label>{{ 'IDP.BASEDN' | translate }}</cnsl-label>
<input cnslInput formControlName="baseDn" />
<input cnslInput formControlName="baseDn" required />
</cnsl-form-field>
<div [ngClass]="{ 'identity-provider-2-col': !provider }">
<cnsl-form-field class="formfield">
<cnsl-label>{{ 'IDP.BINDDN' | translate }}</cnsl-label>
<input cnslInput formControlName="bindDn" />
<input cnslInput formControlName="bindDn" required />
</cnsl-form-field>
<mat-checkbox *ngIf="provider" [(ngModel)]="updateBindPassword" [ngModelOptions]="{ standalone: true }">{{
'IDP.UPDATEBINDPASSWORD' | translate
}}</mat-checkbox>
<cnsl-form-field *ngIf="!provider || (provider && updateBindPassword)" class="formfield">
<mat-checkbox
class="update-secret-checkbox"
*ngIf="provider"
[(ngModel)]="updateBindPassword"
[ngModelOptions]="{ standalone: true }"
>{{ 'IDP.UPDATEBINDPASSWORD' | translate }}</mat-checkbox
>
<cnsl-form-field class="formfield pwd" [ngClass]="{ show: !provider || (provider && updateBindPassword) }">
<cnsl-label>{{ 'IDP.BINDPASSWORD' | translate }}</cnsl-label>
<input cnslInput formControlName="bindPassword" />
<input
cnslInput
formControlName="bindPassword"
type="password"
autocomplete="new-password"
[required]="!provider"
/>
</cnsl-form-field>
</div>
@@ -52,16 +63,18 @@
<cnsl-form-field class="formfield">
<cnsl-label>{{ 'IDP.USERBASE' | translate }}</cnsl-label>
<input cnslInput formControlName="userBase" />
<input cnslInput formControlName="userBase" required />
</cnsl-form-field>
<cnsl-string-list
class="string-list-component-wrapper"
title="{{ 'IDP.USERFILTERS' | translate }}"
formControlName="userFiltersList"
[required]="true"
></cnsl-string-list>
<cnsl-string-list
class="string-list-component-wrapper"
title="{{ 'IDP.USEROBJECTCLASSES' | translate }}"
formControlName="userObjectClassesList"
[required]="true"
@@ -69,22 +82,11 @@
<div class="identity-provider-optional-h-wrapper">
<h2>{{ 'IDP.LDAPATTRIBUTES' | translate }}</h2>
<button (click)="showAttributes = !showAttributes" type="button" mat-icon-button>
<mat-icon *ngIf="showAttributes">keyboard_arrow_up</mat-icon>
<mat-icon *ngIf="!showAttributes">keyboard_arrow_down</mat-icon>
</button>
<span *ngIf="!provider?.config?.ldap?.attributes?.idAttribute" class="state error">{{
'IDP.REQUIRED' | translate
}}</span>
</div>
<div *ngIf="showAttributes">
<cnsl-ldap-attributes
[initialAttributes]="provider?.config?.ldap?.attributes"
(attributesChanged)="attributes = $event"
></cnsl-ldap-attributes>
</div>
<cnsl-ldap-attributes
[initialAttributes]="provider?.config?.ldap?.attributes"
(attributesChanged)="attributes = $event"
></cnsl-ldap-attributes>
<div class="identity-provider-optional-h-wrapper">
<h2>{{ 'IDP.OPTIONAL' | translate }}</h2>
@@ -113,7 +115,7 @@
color="primary"
mat-raised-button
class="continue-button"
[disabled]="(form.invalid && attributes.toObject().idAttribute !== '') || form.disabled"
[disabled]="!form.valid || !attributes.toObject().idAttribute || form.disabled"
type="submit"
>
<span *ngIf="id">{{ 'ACTIONS.SAVE' | translate }}</span>

View File

@@ -20,7 +20,7 @@ import { Breadcrumb, BreadcrumbService, BreadcrumbType } from 'src/app/services/
import { GrpcAuthService } from 'src/app/services/grpc-auth.service';
import { ManagementService } from 'src/app/services/mgmt.service';
import { ToastService } from 'src/app/services/toast.service';
import { requiredValidator } from '../../form-field/validators/validators';
import { minArrayLengthValidator, requiredValidator } from '../../form-field/validators/validators';
import { PolicyComponentServiceType } from '../../policies/policy-component-types.enum';
@@ -30,7 +30,6 @@ import { PolicyComponentServiceType } from '../../policies/policy-component-type
})
export class ProviderLDAPComponent {
public updateBindPassword: boolean = false;
public showAttributes: boolean = false;
public showOptional: boolean = false;
public options: Options = new Options().setIsCreationAllowed(true).setIsLinkingAllowed(true);
public attributes: LDAPAttributes = new LDAPAttributes();
@@ -54,15 +53,15 @@ export class ProviderLDAPComponent {
) {
this.form = new FormGroup({
name: new FormControl('', [requiredValidator]),
serversList: new FormControl('', [requiredValidator]),
serversList: new FormControl<string[]>([''], [minArrayLengthValidator(1)]),
baseDn: new FormControl('', [requiredValidator]),
bindDn: new FormControl('', [requiredValidator]),
bindPassword: new FormControl('', [requiredValidator]),
userBase: new FormControl('', [requiredValidator]),
userFiltersList: new FormControl('', [requiredValidator]),
userObjectClassesList: new FormControl('', [requiredValidator]),
userFiltersList: new FormControl<string[]>([''], [minArrayLengthValidator(1)]),
userObjectClassesList: new FormControl<string[]>([''], [minArrayLengthValidator(1)]),
timeout: new FormControl<number>(0),
startTls: new FormControl(false),
startTls: new FormControl<boolean>(false),
});
this.authService
@@ -83,7 +82,7 @@ export class ProviderLDAPComponent {
});
this.route.data.pipe(take(1)).subscribe((data) => {
this.serviceType = data.serviceType;
this.serviceType = data['serviceType'];
switch (this.serviceType) {
case PolicyComponentServiceType.MGMT:
@@ -112,6 +111,7 @@ export class ProviderLDAPComponent {
if (this.id) {
this.getData(this.id);
this.bindPassword?.setValidators([]);
this.bindPassword?.updateValueAndValidity();
}
});
}
@@ -125,12 +125,25 @@ export class ProviderLDAPComponent {
this.service
.getProviderByID(req)
.then((resp) => {
this.provider = resp.idp;
this.loading = false;
if (this.provider?.config?.ldap) {
this.form.patchValue(this.provider.config.ldap);
if (resp.idp) {
this.provider = resp.idp;
this.loading = false;
this.name?.setValue(this.provider.name);
this.timeout?.setValue(this.provider.config.ldap.timeout?.seconds);
const config = this.provider?.config?.ldap;
if (config) {
this.serversList?.setValue(config.serversList);
this.startTls?.setValue(config.startTls);
this.baseDn?.setValue(config.baseDn);
this.bindDn?.setValue(config.bindDn);
this.userBase?.setValue(config.userBase);
this.userObjectClassesList?.setValue(config.userObjectClassesList);
this.userFiltersList?.setValue(config.userFiltersList);
if (this.provider?.config?.ldap?.timeout?.seconds) {
this.timeout?.setValue(this.provider?.config?.ldap?.timeout?.seconds);
}
}
}
})
.catch((error) => {

View File

@@ -13,40 +13,45 @@
<p class="identity-provider-desc cnsl-secondary-text">{{ 'IDP.CREATE.OAUTH.DESCRIPTION' | translate }}</p>
<form [formGroup]="form" (ngSubmit)="submitForm()">
<cnsl-form-field class="formfield">
<cnsl-label>{{ 'IDP.NAME' | translate }}</cnsl-label>
<input cnslInput formControlName="name" />
</cnsl-form-field>
<cnsl-form-field class="formfield">
<cnsl-label>{{ 'IDP.AUTHORIZATIONENDPOINT' | translate }}</cnsl-label>
<input cnslInput formControlName="authorizationEndpoint" />
</cnsl-form-field>
<cnsl-form-field class="formfield">
<cnsl-label>{{ 'IDP.TOKENENDPOINT' | translate }}</cnsl-label>
<input cnslInput formControlName="tokenEndpoint" />
</cnsl-form-field>
<cnsl-form-field class="formfield">
<cnsl-label>{{ 'IDP.USERENDPOINT' | translate }}</cnsl-label>
<input cnslInput formControlName="userEndpoint" />
</cnsl-form-field>
<cnsl-form-field class="formfield">
<cnsl-label>{{ 'IDP.IDATTRIBUTE' | translate }}</cnsl-label>
<input cnslInput formControlName="idAttribute" />
</cnsl-form-field>
<div class="identity-provider-content">
<cnsl-form-field class="formfield">
<cnsl-label>{{ 'IDP.NAME' | translate }}</cnsl-label>
<input cnslInput formControlName="name" />
</cnsl-form-field>
<cnsl-form-field class="formfield">
<cnsl-label>{{ 'IDP.AUTHORIZATIONENDPOINT' | translate }}</cnsl-label>
<input cnslInput formControlName="authorizationEndpoint" />
</cnsl-form-field>
<cnsl-form-field class="formfield">
<cnsl-label>{{ 'IDP.TOKENENDPOINT' | translate }}</cnsl-label>
<input cnslInput formControlName="tokenEndpoint" />
</cnsl-form-field>
<cnsl-form-field class="formfield">
<cnsl-label>{{ 'IDP.USERENDPOINT' | translate }}</cnsl-label>
<input cnslInput formControlName="userEndpoint" />
</cnsl-form-field>
<cnsl-form-field class="formfield">
<cnsl-label>{{ 'IDP.IDATTRIBUTE' | translate }}</cnsl-label>
<input cnslInput formControlName="idAttribute" />
</cnsl-form-field>
<cnsl-form-field class="formfield">
<cnsl-label>{{ 'IDP.CLIENTID' | translate }}</cnsl-label>
<input cnslInput formControlName="clientId" />
</cnsl-form-field>
<mat-checkbox *ngIf="provider" [(ngModel)]="updateClientSecret" [ngModelOptions]="{ standalone: true }">{{
'IDP.UPDATECLIENTSECRET' | translate
}}</mat-checkbox>
<mat-checkbox
class="update-secret-checkbox"
*ngIf="provider"
[(ngModel)]="updateClientSecret"
[ngModelOptions]="{ standalone: true }"
>{{ 'IDP.UPDATECLIENTSECRET' | translate }}</mat-checkbox
>
<cnsl-form-field *ngIf="!provider || (provider && updateClientSecret)" class="formfield">
<cnsl-label>{{ 'IDP.CLIENTSECRET' | translate }}</cnsl-label>
<input cnslInput formControlName="clientSecret" />

View File

@@ -81,7 +81,7 @@ export class ProviderOAuthComponent {
});
this.route.data.pipe(take(1)).subscribe((data) => {
this.serviceType = data.serviceType;
this.serviceType = data['serviceType'];
switch (this.serviceType) {
case PolicyComponentServiceType.MGMT:
@@ -155,6 +155,7 @@ export class ProviderOAuthComponent {
req.setClientId(this.clientId?.value);
req.setClientSecret(this.clientSecret?.value);
req.setScopesList(this.scopesList?.value);
req.setProviderOptions(this.options);
this.loading = true;
this.service
@@ -186,6 +187,7 @@ export class ProviderOAuthComponent {
req.setClientId(this.clientId?.value);
req.setClientSecret(this.clientSecret?.value);
req.setScopesList(this.scopesList?.value);
req.setProviderOptions(this.options);
this.loading = true;
this.service

View File

@@ -59,7 +59,7 @@ export class ProviderOIDCComponent {
});
this.route.data.pipe(take(1)).subscribe((data) => {
this.serviceType = data.serviceType;
this.serviceType = data['serviceType'];
switch (this.serviceType) {
case PolicyComponentServiceType.MGMT:
@@ -130,6 +130,7 @@ export class ProviderOIDCComponent {
req.setClientSecret(this.clientSecret?.value);
req.setIssuer(this.issuer?.value);
req.setScopesList(this.scopesList?.value);
req.setProviderOptions(this.options);
this.loading = true;
this.service
@@ -158,6 +159,7 @@ export class ProviderOIDCComponent {
req.setClientSecret(this.clientSecret?.value);
req.setIssuer(this.issuer?.value);
req.setScopesList(this.scopesList?.value);
req.setProviderOptions(this.options);
this.loading = true;
this.service

View File

@@ -1,5 +1,8 @@
@use '@angular/material' as mat;
@mixin identity-provider-theme($theme) {
$is-dark-theme: map-get($theme, is-dark);
$background: map-get($theme, background);
.identity-provider-desc {
font-size: 14px;
@@ -36,10 +39,22 @@
}
}
.update-secret-checkbox {
margin: 0.5rem 0 0 0;
}
.formfield {
display: block;
max-width: 400px;
&.pwd {
display: none;
}
&.pwd.show {
display: block;
}
.name-hint {
font-size: 12px;
}
@@ -59,6 +74,10 @@
}
}
.string-list-component-wrapper {
max-width: 400px;
}
.identity-provider-content {
display: flex;
flex-direction: column;

View File

@@ -2,14 +2,7 @@
<div class="found-user-row" *ngFor="let user of users; index as i">
<div class="circle">
<cnsl-avatar
*ngIf="
user.human &&
user.human.profile &&
user.human.profile.displayName &&
user.human.profile.firstName &&
user.human.profile.lastName;
else cog
"
*ngIf="user.human && user.human.profile; else cog"
class="avatar"
[name]="user.human.profile.displayName"
[avatarUrl]="user.human.profile.avatarUrl || ''"
@@ -76,14 +69,7 @@
<div class="user-option" data-e2e="user-option">
<div class="circle">
<cnsl-avatar
*ngIf="
user.human &&
user.human.profile &&
user.human.profile.displayName &&
user.human.profile.firstName &&
user.human.profile.lastName;
else cog
"
*ngIf="user.human && user.human.profile; else cog"
class="avatar"
[name]="user.human.profile.displayName"
[avatarUrl]="user.human.profile.avatarUrl || ''"

View File

@@ -19,10 +19,10 @@ export class SettingsListComponent implements OnChanges {
constructor() {}
ngOnChanges(changes: SimpleChanges): void {
if (changes.selectedId?.currentValue) {
if (changes['selectedId']?.currentValue) {
this.currentSetting =
this.settingsList && this.settingsList.find((l) => l.id === changes.selectedId.currentValue)
? changes.selectedId.currentValue
this.settingsList && this.settingsList.find((l) => l.id === changes['selectedId'].currentValue)
? changes['selectedId'].currentValue
: '';
} else {
this.currentSetting = this.settingsList ? this.settingsList[0].id : '';

View File

@@ -28,6 +28,11 @@
display: flex;
align-items: center;
h2 {
text-transform: uppercase;
font-size: 1.2rem;
}
.shortcut-btn {
margin-left: 0.5rem;
}

View File

@@ -1,30 +1,43 @@
<form class="string-list-form" (ngSubmit)="add(redInput)">
<cnsl-form-field class="formfield">
<cnsl-label>{{ title }}</cnsl-label>
<input #redInput cnslInput [formControl]="control" />
</cnsl-form-field>
<button
matTooltip="{{ 'ACTIONS.ADD' | translate }}"
type="submit"
mat-icon-button
[disabled]="control.invalid || control.disabled"
>
<mat-icon>add</mat-icon>
</button>
</form>
<div class="form-array-list">
<div class="form-field-list">
<div class="list-header-wrapper">
<p class="list-header cnsl-secondary-text">{{ title }}*</p>
<button
class="add-element-btn"
matTooltip="{{ 'ACTIONS.ADD' | translate }}"
type="button"
mat-icon-button
(click)="addArrayEntry()"
>
<mat-icon>add</mat-icon>
</button>
</div>
<ng-container *ngFor="let formControl of formArray.controls; index as i">
<div class="element-row">
<cnsl-form-field class="formfield" [hideRequiredMarker]="true">
<input cnslInput title="{{ 'IDP.SERVERS' | translate }}" [formControl]="$any(formControl)" required />
</cnsl-form-field>
<div class="string-list">
<div *ngFor="let str of value" class="value-line">
<span>{{ str }}</span>
<span class="fill-space"></span>
<button
type="button"
matTooltip="{{ 'ACTIONS.DELETE' | translate }}"
mat-icon-button
(click)="remove(str)"
class="icon-button"
>
<mat-icon class="icon">cancel</mat-icon>
</button>
<button
class="add-element-btn"
[disabled]="i === 0 && formArray.controls.length === 1 && formControl.value === ''"
[matTooltip]="
i === 0 && formArray.controls.length === 1 ? ('ACTIONS.CLEAR' | translate) : ('ACTIONS.REMOVE' | translate)
"
type="button"
mat-icon-button
color="warn"
(click)="i === 0 && formArray.controls.length === 1 ? clearEntryAtIndex(i) : removeEntryAtIndex(i)"
>
<i *ngIf="i === 0 && formArray.controls.length === 1; else removeIcon" class="las la-times-circle"></i>
<ng-template #removeIcon>
<i class="las la-minus-circle"></i>
</ng-template>
</button>
</div>
</ng-container>
<span class="control-error" *ngIf="control.touched && control.errors && control.errors['errorsatleastone']">{{
control.errors['errorsatleastone'].i18nKey | translate
}}</span>
</div>
</div>

View File

@@ -5,56 +5,50 @@
$background: map-get($theme, background);
$is-dark-theme: map-get($theme, is-dark);
$warn: map-get($theme, warn);
$warn-color: map-get($warn, 500);
$button-text-color: map-get($foreground, text);
$button-disabled-text-color: map-get($foreground, disabled-button);
$divider-color: map-get($foreground, dividers);
$secondary-text: map-get($foreground, secondary-text);
$warncolor: map-get($warn, 500);
.string-list {
width: 100%;
.form-array-list {
display: flex;
flex-direction: row;
max-width: 400px;
background: if($is-dark-theme, #00000020, mat.get-color-from-palette($background, cards));
margin-left: -1rem;
margin-right: -1rem;
padding: 0 1rem 0.5rem 1rem;
margin-top: 0.5rem;
.value-line {
.list-header-wrapper {
display: flex;
align-items: center;
margin: 0.5rem 0;
padding: 0 0 0 0.75rem;
border-radius: 4px;
background: map-get($background, infosection);
margin: 0.5rem -0.5rem 0 0;
.fill-space {
flex: 1;
.list-header {
font-size: 12px;
}
}
.icon-button {
height: 30px;
line-height: 30px;
.form-field-list {
flex: 1;
display: flex;
flex-direction: column;
.icon {
font-size: 1rem;
margin-bottom: 3px;
}
.element-row {
display: flex;
align-items: center;
&:not(:hover) {
color: $secondary-text;
.formfield {
flex: 1;
}
}
.control-error {
font-size: 12px;
color: $warncolor;
}
}
.add-element-btn {
margin-bottom: 0rem;
}
}
}
.string-list-form {
display: flex;
align-items: flex-end;
min-width: 320px;
.formfield {
width: 500px;
}
button {
margin-bottom: 0.9rem;
margin-right: -0.5rem;
}
}

View File

@@ -1,7 +1,7 @@
import { Component, forwardRef, Input, OnDestroy, OnInit, ViewChild, ViewEncapsulation } from '@angular/core';
import { ControlValueAccessor, FormControl, NG_VALUE_ACCESSOR } from '@angular/forms';
import { Observable, Subject, takeUntil } from 'rxjs';
import { requiredValidator } from '../form-field/validators/validators';
import { Component, forwardRef, Input, OnDestroy, ViewChildren, ViewEncapsulation } from '@angular/core';
import { ControlValueAccessor, FormArray, FormControl, NG_VALUE_ACCESSOR } from '@angular/forms';
import { distinctUntilChanged, Subject, takeUntil } from 'rxjs';
import { minArrayLengthValidator, requiredValidator } from '../form-field/validators/validators';
@Component({
selector: 'cnsl-string-list',
@@ -16,22 +16,23 @@ import { requiredValidator } from '../form-field/validators/validators';
},
],
})
export class StringListComponent implements ControlValueAccessor, OnInit, OnDestroy {
export class StringListComponent implements ControlValueAccessor, OnDestroy {
@Input() title: string = '';
@Input() required: boolean = false;
@Input() public getValues: Observable<void> = new Observable(); // adds formfieldinput to array on emission
@Input() public control: FormControl = new FormControl<string>({ value: '', disabled: true });
@Input() public control: FormControl = new FormControl<string[]>({ value: [], disabled: true });
private destroy$: Subject<void> = new Subject();
@ViewChild('redInput') input!: any;
private val: string[] = [];
@ViewChildren('stringInput') input!: any[];
public val: string[] = [];
ngOnInit(): void {
this.getValues.pipe(takeUntil(this.destroy$)).subscribe(() => {
this.add(this.input.nativeElement);
public formArray: FormArray = new FormArray([new FormControl('', [requiredValidator])]);
constructor() {
this.control.setValidators([minArrayLengthValidator(1)]);
this.formArray.valueChanges.pipe(takeUntil(this.destroy$), distinctUntilChanged()).subscribe((value) => {
this.value = value;
});
this.required ? this.control.setValidators([requiredValidator]) : this.control.setValidators([]);
}
ngOnDestroy(): void {
@@ -50,12 +51,24 @@ export class StringListComponent implements ControlValueAccessor, OnInit, OnDest
}
}
addArrayEntry() {
this.formArray.push(new FormControl('', [requiredValidator]));
}
removeEntryAtIndex(index: number) {
this.formArray.removeAt(index);
}
clearEntryAtIndex(index: number) {
this.formArray.controls[index].setValue('');
}
get value() {
return this.val;
}
writeValue(value: string[]) {
this.value = value;
value.map((v, i) => this.formArray.setControl(i, new FormControl(v, [requiredValidator])));
}
registerOnChange(fn: any) {
@@ -73,28 +86,4 @@ export class StringListComponent implements ControlValueAccessor, OnInit, OnDest
this.control.enable();
}
}
public add(input: any): void {
if (this.control.valid) {
const trimmed = input.value.trim();
if (trimmed) {
this.val ? this.val.push(input.value) : (this.val = [input.value]);
this.onChange(this.val);
this.onTouch(this.val);
}
if (input) {
input.value = '';
}
}
}
public remove(str: string): void {
const index = this.value.indexOf(str);
if (index >= 0) {
this.value.splice(index, 1);
this.onChange(this.value);
this.onTouch(this.value);
}
}
}

View File

@@ -3,6 +3,7 @@ import { NgModule } from '@angular/core';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { MatIconModule } from '@angular/material/icon';
import { MatLegacyButtonModule } from '@angular/material/legacy-button';
import { MatLegacyChipsModule } from '@angular/material/legacy-chips';
import { MatLegacyTooltipModule } from '@angular/material/legacy-tooltip';
import { TranslateModule } from '@ngx-translate/core';
import { InputModule } from '../input/input.module';
@@ -15,6 +16,7 @@ import { StringListComponent } from './string-list.component';
InputModule,
FormsModule,
ReactiveFormsModule,
MatLegacyChipsModule,
TranslateModule,
MatIconModule,
MatLegacyTooltipModule,

View File

@@ -77,7 +77,7 @@
[checked]="selection.isSelected(row)"
>
<cnsl-avatar
*ngIf="row && row?.displayName && row.firstName && row.lastName; else cog"
*ngIf="row && row.userType === Type.TYPE_HUMAN; else cog"
class="avatar"
[name]="row.displayName"
[avatarUrl]="row.avatarUrl || ''"

View File

@@ -68,7 +68,6 @@ export class UserGrantsComponent implements OnInit, AfterViewInit {
@Input() public type: Type | undefined = undefined;
public filterOpen: boolean = false;
constructor(
private authService: GrpcAuthService,
private userService: ManagementService,

View File

@@ -91,7 +91,7 @@
<ng-container matColumnDef="type">
<th mat-header-cell *matHeaderCellDef>{{ 'IAM.EVENTS.TYPE' | translate }}</th>
<td mat-cell *matCellDef="let event">
<td mat-cell *matCellDef="let event" data-e2e="event-type-cell">
<ng-container *ngIf="event | toobject as event">
<span *ngIf="event.type?.localized?.localizedMessage">{{ event.type.localized.localizedMessage }}</span>
</ng-container>

View File

@@ -1,4 +1,6 @@
<div class="max-width-container">
<h1 class="home-title" data-e2e="authenticated-welcome">{{ 'HOME.WELCOME' | translate }}</h1>
<div class="home-wrapper enlarged-container">
<ng-container *ngIf="['iam.read$'] | hasRole | async; else defaultHome">
<cnsl-onboarding></cnsl-onboarding>
@@ -10,24 +12,66 @@
<p class="disclaimer cnsl-secondary-text">{{ 'HOME.DISCLAIMER' | translate }}</p>
<span class="fill-space"></span>
<h2 class="desc">{{ 'ONBOARDING.MOREDESCRIPTION' | translate }}</h2>
<div class="home-grid-container">
<a href="https://zitadel.com/docs" target="_blank" rel="noreferrer" class="grid-item blue">
<div class="grid-item-avatar blue">
<i class="icon las la-file-alt"></i>
<a href="https://zitadel.com/docs" target="_blank" rel="noreferrer" class="grid-item">
<div
class="icon-wrapper"
[ngStyle]="{
background: (themeService.isDarkTheme | async) ? bluedark + 50 : bluelight + 50
}"
>
<div
class="inner"
[ngStyle]="{
background: (themeService.isDarkTheme | async) ? bluedark : bluelight,
color: (themeService.isDarkTheme | async) ? bluelight : bluedark
}"
>
<i class="las la-file-alt"></i>
</div>
</div>
<span>{{ 'HOME.DOCUMENTATION.TITLE' | translate }}</span>
</a>
<a href="https://zitadel.com/docs/guides/start/quickstart" target="_blank" rel="noreferrer" class="grid-item green">
<div class="grid-item-avatar green">
<i class="icon las la-play"></i>
<a href="https://zitadel.com/docs/guides/start/quickstart" target="_blank" rel="noreferrer" class="grid-item">
<div
class="icon-wrapper"
[ngStyle]="{
background: (themeService.isDarkTheme | async) ? greendark + 50 : greenlight + 50
}"
>
<div
class="inner"
[ngStyle]="{
background: (themeService.isDarkTheme | async) ? greendark : greenlight,
color: (themeService.isDarkTheme | async) ? greenlight : greendark
}"
>
<i class="las la-play"></i>
</div>
</div>
<span>{{ 'HOME.GETSTARTED.TITLE' | translate }}</span>
</a>
<a href="https://zitadel.com/docs/examples/introduction" target="_blank" rel="noreferrer" class="grid-item green">
<div class="grid-item-avatar green">
<i class="icon las la-play"></i>
<a href="https://zitadel.com/docs/examples/introduction" target="_blank" rel="noreferrer" class="grid-item">
<div
class="icon-wrapper"
[ngStyle]="{
background: (themeService.isDarkTheme | async) ? cyandark + 50 : cyanlight + 50
}"
>
<div
class="inner"
[ngStyle]="{
background: (themeService.isDarkTheme | async) ? cyandark : cyanlight,
color: (themeService.isDarkTheme | async) ? cyanlight : cyandark
}"
>
<i class="las la-play"></i>
</div>
</div>
<span>{{ 'HOME.QUICKSTARTS.TITLE' | translate }}</span>
</a>

View File

@@ -18,6 +18,11 @@
$border-color: if($is-dark-theme, rgba(#8795a1, 0.2), rgba(#8795a1, 0.2));
$border-selected-color: if($is-dark-theme, #fff, #000);
.home-title {
font-size: 2rem;
margin-bottom: 1rem;
}
.home-wrapper {
position: relative;
display: flex;
@@ -34,6 +39,12 @@
}
}
.desc {
font-size: 1.2rem;
margin-top: 0;
text-transform: uppercase;
}
.home-grid-container {
display: grid;
grid-template-columns: 1fr;
@@ -58,24 +69,15 @@
font-size: 14px;
background-color: $card-background-color;
border: 1px solid $border-color;
border-radius: 1rem;
border-radius: 0.5rem;
margin: 0;
text-decoration: none;
color: white;
box-shadow: 0 0 15px #0000001a;
color: inherit;
&.edit-state {
cursor: move;
}
&.green {
background: linear-gradient(40deg, #059669 30%, #047857);
}
&.blue {
background: linear-gradient(40deg, #3b82f6 30%, #4f46e5);
}
.grid-item-avatar {
height: 40px;
width: 40px;
@@ -118,6 +120,26 @@
color: white;
}
}
.icon-wrapper {
display: flex;
justify-content: center;
align-items: center;
padding: 0.5rem;
border-radius: 50vw;
flex-shrink: 0;
margin-right: 1rem;
.inner {
border-radius: 50vw;
height: 40px;
width: 40px;
display: flex;
justify-content: center;
align-items: center;
font-size: 1.75rem;
}
}
}
}

View File

@@ -1,6 +1,8 @@
import { Component } from '@angular/core';
import { Breadcrumb, BreadcrumbService, BreadcrumbType } from 'src/app/services/breadcrumb.service';
import { GrpcAuthService } from 'src/app/services/grpc-auth.service';
import { ThemeService } from 'src/app/services/theme.service';
import { COLORS } from 'src/app/utils/color';
@Component({
selector: 'cnsl-home',
@@ -8,9 +10,18 @@ import { GrpcAuthService } from 'src/app/services/grpc-auth.service';
styleUrls: ['./home.component.scss'],
})
export class HomeComponent {
public greendark: string = COLORS[6][700];
public greenlight = COLORS[6][200];
public cyandark: string = COLORS[7][700];
public cyanlight = COLORS[7][200];
public bluedark: string = COLORS[9][700];
public bluelight = COLORS[9][200];
public dark: boolean = true;
constructor(public authService: GrpcAuthService, breadcrumbService: BreadcrumbService) {
constructor(public authService: GrpcAuthService, breadcrumbService: BreadcrumbService, public themeService: ThemeService) {
const bread: Breadcrumb = {
type: BreadcrumbType.ORG,
routerLink: ['/org'],

View File

@@ -45,7 +45,7 @@ export class OrgCreateComponent {
public pwdForm?: UntypedFormGroup;
public genders: Gender[] = [Gender.GENDER_FEMALE, Gender.GENDER_MALE, Gender.GENDER_UNSPECIFIED];
public languages: string[] = ['de', 'en', 'fr', 'it', 'ja', 'pl', 'zh'];
public languages: string[] = ['de', 'en', 'es', 'fr', 'it', 'ja', 'pl', 'zh'];
public policy?: PasswordComplexityPolicy.AsObject;
public usePassword: boolean = false;

View File

@@ -1,5 +1,6 @@
<cnsl-create-layout
title="{{ 'APP.PAGES.CREATE' | translate }}"
class="app-create-wrapper"
[createSteps]="
appType?.value?.createType === AppCreateType.OIDC
? appType?.value.oidcAppType !== OIDCAppType.OIDC_APP_TYPE_NATIVE
@@ -22,7 +23,7 @@
</mat-checkbox>
<mat-horizontal-stepper
class="stepper"
class="stepper {{ 'last-edited-step-' + stepper.selectedIndex }}"
*ngIf="!devmode"
linear
#stepper
@@ -334,6 +335,11 @@
</button>
</div>
</mat-step>
<!-- Icon override -->
<ng-template matStepperIcon="edit">
<mat-icon>check</mat-icon>
</ng-template>
</mat-horizontal-stepper>
<div *ngIf="devmode" class="dev">

View File

@@ -50,6 +50,14 @@ p.desc {
.step-description {
font-size: 0.9rem;
}
.mat-step-icon-content {
position: absolute;
transform: translate(-50%, -50%);
display: flex;
top: 50%;
left: 50%;
}
}
.checkbox-container {

View File

@@ -398,10 +398,10 @@
<ng-container *ngIf="currentSetting === 'urls'">
<cnsl-card title=" {{ 'APP.URLS' | translate }}">
<cnsl-info-section *ngIf="environmentMap?.issuer">
<cnsl-info-section *ngIf="environmentMap['issuer']">
<div
[innerHtml]="
'APP.OIDC.WELLKNOWN' | translate : { url: environmentMap.issuer + '/.well-known/openid-configuration' }
'APP.OIDC.WELLKNOWN' | translate : { url: environmentMap['issuer'] + '/.well-known/openid-configuration' }
"
></div>
</cnsl-info-section>

View File

@@ -34,7 +34,7 @@ export class ProjectGrantCreateComponent implements OnInit, OnDestroy {
public ngOnInit(): void {
this.route.params.pipe(takeUntil(this.destroy$)).subscribe((params) => {
this.projectId = params.projectid;
this.projectId = params['projectid'];
const breadcrumbs = [
new Breadcrumb({

View File

@@ -49,19 +49,19 @@ export class ProjectGrantDetailComponent {
private breadcrumbService: BreadcrumbService,
) {
this.route.params.subscribe((params) => {
this.projectid = params.projectid;
this.grantid = params.grantid;
this.projectid = params['projectid'];
this.grantid = params['grantid'];
this.dataSource = new ProjectGrantMembersDataSource(this.mgmtService);
this.dataSource.loadMembers(params.projectid, params.grantid, 0, this.INITIALPAGESIZE);
this.dataSource.loadMembers(params['projectid'], params['grantid'], 0, this.INITIALPAGESIZE);
this.getRoleOptions(params.projectid);
this.getRoleOptions(params['projectid']);
this.getMemberRoleOptions();
this.changePageFactory = (event?: PageEvent) => {
return this.dataSource.loadMembers(
params.projectid,
params.grantid,
params['projectid'],
params['grantid'],
event?.pageIndex ?? 0,
event?.pageSize ?? this.INITIALPAGESIZE,
);

View File

@@ -24,7 +24,7 @@ export class ProjectsComponent {
breadcrumbService: BreadcrumbService,
) {
this.activatedRoute.queryParams.pipe(take(1)).subscribe((params: Params) => {
const type = params.type;
const type = params['type'];
if (type && type === 'owned') {
this.setType(ProjectType.PROJECTTYPE_OWNED);
} else if (type && type === 'granted') {

View File

@@ -10,11 +10,11 @@
<form *ngIf="userForm" [formGroup]="userForm" (ngSubmit)="createUser()" class="machine-create-form">
<div class="machine-create-content">
<cnsl-form-field class="formfield">
<cnsl-label>{{ 'USER.MACHINE.USERNAME' | translate }}*</cnsl-label>
<cnsl-label>{{ 'USER.MACHINE.USERNAME' | translate }}</cnsl-label>
<input cnslInput formControlName="userName" required />
</cnsl-form-field>
<cnsl-form-field class="formfield">
<cnsl-label>{{ 'USER.MACHINE.NAME' | translate }}*</cnsl-label>
<cnsl-label>{{ 'USER.MACHINE.NAME' | translate }}</cnsl-label>
<input cnslInput formControlName="name" required />
</cnsl-form-field>
<cnsl-form-field class="formfield">

View File

@@ -13,11 +13,11 @@
<div class="user-create-grid">
<cnsl-form-field>
<cnsl-label>{{ 'USER.PROFILE.EMAIL' | translate }}*</cnsl-label>
<cnsl-label>{{ 'USER.PROFILE.EMAIL' | translate }}</cnsl-label>
<input cnslInput matRipple formControlName="email" required />
</cnsl-form-field>
<cnsl-form-field>
<cnsl-label>{{ 'USER.PROFILE.USERNAME' | translate }}*</cnsl-label>
<cnsl-label>{{ 'USER.PROFILE.USERNAME' | translate }}</cnsl-label>
<input
cnslInput
formControlName="userName"
@@ -28,11 +28,11 @@
</cnsl-form-field>
<cnsl-form-field>
<cnsl-label>{{ 'USER.PROFILE.FIRSTNAME' | translate }}*</cnsl-label>
<cnsl-label>{{ 'USER.PROFILE.FIRSTNAME' | translate }}</cnsl-label>
<input cnslInput formControlName="firstName" required />
</cnsl-form-field>
<cnsl-form-field>
<cnsl-label>{{ 'USER.PROFILE.LASTNAME' | translate }}*</cnsl-label>
<cnsl-label>{{ 'USER.PROFILE.LASTNAME' | translate }}</cnsl-label>
<input cnslInput formControlName="lastName" required />
</cnsl-form-field>
<cnsl-form-field>

View File

@@ -33,7 +33,7 @@ import {
export class UserCreateComponent implements OnInit, OnDestroy {
public user: AddHumanUserRequest.AsObject = new AddHumanUserRequest().toObject();
public genders: Gender[] = [Gender.GENDER_FEMALE, Gender.GENDER_MALE, Gender.GENDER_UNSPECIFIED];
public languages: string[] = ['de', 'en', 'fr', 'it', 'ja', 'pl', 'zh'];
public languages: string[] = ['de', 'en', 'es', 'fr', 'it', 'ja', 'pl', 'zh'];
public selected: CountryPhoneCode | undefined;
public countryPhoneCodes: CountryPhoneCode[] = [];
public userForm!: UntypedFormGroup;

View File

@@ -33,7 +33,7 @@ import { EditDialogComponent, EditDialogType } from './edit-dialog/edit-dialog.c
export class AuthUserDetailComponent implements OnDestroy {
public user?: User.AsObject;
public genders: Gender[] = [Gender.GENDER_MALE, Gender.GENDER_FEMALE, Gender.GENDER_DIVERSE];
public languages: string[] = ['de', 'en', 'fr', 'it', 'ja', 'pl', 'zh'];
public languages: string[] = ['de', 'en', 'es', 'fr', 'it', 'ja', 'pl', 'zh'];
private subscription: Subscription = new Subscription();

View File

@@ -11,10 +11,10 @@
<i class="las la-camera"></i>
</div>
<cnsl-avatar
*ngIf="user && user.profile?.displayName && user.profile?.firstName && user.profile?.lastName"
*ngIf="user && user.profile"
class="avatar"
[name]="user.profile?.displayName ?? ''"
[avatarUrl]="user.profile?.avatarUrl || ''"
[name]="user.profile.displayName"
[avatarUrl]="user.profile.avatarUrl || ''"
[forColor]="preferredLoginName"
[size]="80"
>

View File

@@ -11,8 +11,8 @@ export class PhoneDetailComponent implements OnChanges {
public country: string | undefined;
ngOnChanges(changes: SimpleChanges): void {
if (changes.phone.currentValue) {
const phoneNumber = formatPhone(changes.phone.currentValue);
if (changes['phone'].currentValue) {
const phoneNumber = formatPhone(changes['phone'].currentValue);
if (this.phone !== phoneNumber.phone) {
this.phone = phoneNumber.phone;
this.country = phoneNumber.country;

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