diff --git a/docs/docs/apis/proto/instance.md b/docs/docs/apis/proto/instance.md index a20b1f0802..43152e5cea 100644 --- a/docs/docs/apis/proto/instance.md +++ b/docs/docs/apis/proto/instance.md @@ -91,6 +91,23 @@ IdQuery is always equals | details | zitadel.v1.ObjectDetails | - | | | state | State | - | | | name | string | - | | +| version | string | - | | + + + + +### InstanceDetail + + + +| Field | Type | Description | Validation | +| ----- | ---- | ----------- | ----------- | +| id | string | - | | +| details | zitadel.v1.ObjectDetails | - | | +| state | State | - | | +| name | string | - | | +| version | string | - | | +| domains | repeated Domain | - | | diff --git a/docs/docs/apis/proto/system.md b/docs/docs/apis/proto/system.md index caf4c73ecc..83207df658 100644 --- a/docs/docs/apis/proto/system.md +++ b/docs/docs/apis/proto/system.md @@ -25,7 +25,7 @@ It respondes as soon as ZITADEL started > **rpc** ListInstances([ListInstancesRequest](#listinstancesrequest)) [ListInstancesResponse](#listinstancesresponse) -Returns a list of ZITADEL instances/tenants +Returns a list of ZITADEL instances @@ -41,7 +41,7 @@ Returns the detail of an instance - GET: /instances/{id} + GET: /instances/{instance_id} ### AddInstance @@ -67,7 +67,19 @@ This might take some time - DELETE: /instances/{id} + DELETE: /instances/{instance_id} + + +### ExistsDomain + +> **rpc** ExistsDomain([ExistsDomainRequest](#existsdomainrequest)) +[ExistsDomainResponse](#existsdomainresponse) + +Checks if a domain exists + + + + POST: /domains/{domain}/_exists ### ListDomains @@ -79,7 +91,7 @@ Returns the custom domains of an instance - POST: /instances/{id}/domains/_search + POST: /instances/{instance_id}/domains/_search ### AddDomain @@ -91,7 +103,7 @@ Returns the domain of an instance - POST: /instances/{id}/domains + POST: /instances/{instance_id}/domains ### RemoveDomain @@ -103,7 +115,7 @@ Returns the domain of an instance - DELETE: /instances/{id}/domains/{domain} + DELETE: /instances/{instance_id}/domains/{domain} ### SetPrimaryDomain @@ -115,7 +127,7 @@ Returns the domain of an instance - POST: /instances/{id}/domains/_set_primary + POST: /instances/{instance_id}/domains/_set_primary ### ListViews @@ -191,7 +203,7 @@ failed event. You can find out if it worked on the `failure_count` | Field | Type | Description | Validation | | ----- | ---- | ----------- | ----------- | -| id | string | - | string.min_len: 1
string.max_len: 200
| +| instance_id | string | - | string.min_len: 1
string.max_len: 200
| | domain | string | - | string.min_len: 1
string.max_len: 200
| @@ -217,12 +229,47 @@ failed event. You can find out if it worked on the `failure_count` | instance_name | string | - | string.min_len: 1
string.max_len: 200
| | first_org_name | string | - | string.max_len: 200
| | custom_domain | string | - | string.max_len: 200
| -| owner_first_name | string | - | string.max_len: 200
| -| owner_last_name | string | - | string.max_len: 200
| -| owner_email | string | - | string.min_len: 1
string.max_len: 200
| -| owner_username | string | - | string.max_len: 200
| -| request_limit | uint64 | - | | -| action_mins_limit | uint64 | - | | +| owner_user_name | string | - | string.max_len: 200
| +| owner_email | AddInstanceRequest.Email | - | message.required: true
| +| owner_profile | AddInstanceRequest.Profile | - | message.required: false
| +| owner_password | AddInstanceRequest.Password | - | message.required: false
| + + + + +### AddInstanceRequest.Email + + + +| Field | Type | Description | Validation | +| ----- | ---- | ----------- | ----------- | +| email | string | - | string.min_len: 1
string.max_len: 200
| +| is_email_verified | bool | - | | + + + + +### AddInstanceRequest.Password + + + +| Field | Type | Description | Validation | +| ----- | ---- | ----------- | ----------- | +| password | string | - | string.max_len: 200
| +| password_change_required | bool | - | | + + + + +### AddInstanceRequest.Profile + + + +| Field | Type | Description | Validation | +| ----- | ---- | ----------- | ----------- | +| first_name | string | - | string.max_len: 200
| +| last_name | string | - | string.max_len: 200
| +| preferred_language | string | - | string.max_len: 10
| @@ -233,7 +280,7 @@ failed event. You can find out if it worked on the `failure_count` | Field | Type | Description | Validation | | ----- | ---- | ----------- | ----------- | -| id | string | - | | +| instance_id | string | - | | | details | zitadel.v1.ObjectDetails | - | | @@ -282,6 +329,28 @@ This is an empty response +### ExistsDomainRequest + + + +| Field | Type | Description | Validation | +| ----- | ---- | ----------- | ----------- | +| domain | string | - | string.min_len: 1
string.max_len: 200
| + + + + +### ExistsDomainResponse + + + +| Field | Type | Description | Validation | +| ----- | ---- | ----------- | ----------- | +| exists | bool | - | | + + + + ### FailedEvent @@ -303,7 +372,7 @@ This is an empty response | Field | Type | Description | Validation | | ----- | ---- | ----------- | ----------- | -| id | string | - | string.min_len: 1
string.max_len: 200
| +| instance_id | string | - | string.min_len: 1
string.max_len: 200
| @@ -314,7 +383,7 @@ This is an empty response | Field | Type | Description | Validation | | ----- | ---- | ----------- | ----------- | -| instance | zitadel.instance.v1.Instance | - | | +| instance | zitadel.instance.v1.InstanceDetail | - | | @@ -325,7 +394,7 @@ This is an empty response | Field | Type | Description | Validation | | ----- | ---- | ----------- | ----------- | -| id | string | - | string.min_len: 1
string.max_len: 200
| +| instance_id | string | - | string.min_len: 1
string.max_len: 200
| @@ -361,7 +430,7 @@ This is an empty response | Field | Type | Description | Validation | | ----- | ---- | ----------- | ----------- | -| id | string | list limitations and ordering | string.min_len: 1
string.max_len: 200
| +| instance_id | string | list limitations and ordering | string.min_len: 1
string.max_len: 200
| | query | zitadel.v1.ListQuery | - | | | sorting_column | zitadel.instance.v1.DomainFieldName | the field the result is sorted | | | queries | repeated zitadel.instance.v1.DomainSearchQuery | criterias the client is looking for | | @@ -448,7 +517,7 @@ This is an empty request | Field | Type | Description | Validation | | ----- | ---- | ----------- | ----------- | -| id | string | - | string.min_len: 1
string.max_len: 200
| +| instance_id | string | - | string.min_len: 1
string.max_len: 200
| | domain | string | - | string.min_len: 1
string.max_len: 200
| @@ -490,7 +559,7 @@ This is an empty response | Field | Type | Description | Validation | | ----- | ---- | ----------- | ----------- | -| id | string | - | string.min_len: 1
string.max_len: 200
| +| instance_id | string | - | string.min_len: 1
string.max_len: 200
| @@ -512,7 +581,7 @@ This is an empty response | Field | Type | Description | Validation | | ----- | ---- | ----------- | ----------- | -| id | string | - | string.min_len: 1
string.max_len: 200
| +| instance_id | string | - | string.min_len: 1
string.max_len: 200
| | domain | string | - | string.min_len: 1
string.max_len: 200
| diff --git a/internal/api/grpc/instance/converter.go b/internal/api/grpc/instance/converter.go index d8283416e2..bb410316aa 100644 --- a/internal/api/grpc/instance/converter.go +++ b/internal/api/grpc/instance/converter.go @@ -23,7 +23,22 @@ func InstanceToPb(instance *query.Instance) *instance_pb.Instance { instance.ChangeDate, instance.InstanceID(), ), - Id: instance.InstanceID(), + Id: instance.InstanceID(), + Name: instance.Name, + } +} + +func InstanceDetailToPb(instance *query.Instance) *instance_pb.InstanceDetail { + return &instance_pb.InstanceDetail{ + Details: object.ToViewDetailsPb( + instance.Sequence, + instance.CreationDate, + instance.ChangeDate, + instance.InstanceID(), + ), + Id: instance.InstanceID(), + Name: instance.Name, + Domains: DomainsToPb(instance.Domains), } } diff --git a/internal/api/grpc/management/user_converter.go b/internal/api/grpc/management/user_converter.go index 11202db217..77f1d7c09b 100644 --- a/internal/api/grpc/management/user_converter.go +++ b/internal/api/grpc/management/user_converter.go @@ -5,6 +5,7 @@ import ( "time" "github.com/zitadel/logging" + "github.com/zitadel/zitadel/pkg/grpc/user" "golang.org/x/text/language" "github.com/zitadel/zitadel/internal/api/authz" @@ -27,14 +28,38 @@ func ListUsersRequestToModel(req *mgmt_pb.ListUsersRequest) (*query.UserSearchQu } return &query.UserSearchQueries{ SearchRequest: query.SearchRequest{ - Offset: offset, - Limit: limit, - Asc: asc, + Offset: offset, + Limit: limit, + Asc: asc, + SortingColumn: UserFieldNameToSortingColumn(req.SortingColumn), }, Queries: queries, }, nil } +func UserFieldNameToSortingColumn(field user.UserFieldName) query.Column { + switch field { + case user.UserFieldName_USER_FIELD_NAME_EMAIL: + return query.HumanEmailCol + case user.UserFieldName_USER_FIELD_NAME_FIRST_NAME: + return query.HumanFirstNameCol + case user.UserFieldName_USER_FIELD_NAME_LAST_NAME: + return query.HumanLastNameCol + case user.UserFieldName_USER_FIELD_NAME_DISPLAY_NAME: + return query.HumanDisplayNameCol + case user.UserFieldName_USER_FIELD_NAME_USER_NAME: + return query.UserUsernameCol + case user.UserFieldName_USER_FIELD_NAME_STATE: + return query.UserStateCol + case user.UserFieldName_USER_FIELD_NAME_TYPE: + return query.UserTypeCol + case user.UserFieldName_USER_FIELD_NAME_NICK_NAME: + return query.HumanNickNameCol + default: + return query.UserIDCol + } +} + func BulkSetMetadataToDomain(req *mgmt_pb.BulkSetUserMetadataRequest) []*domain.Metadata { metadata := make([]*domain.Metadata, len(req.Metadata)) for i, data := range req.Metadata { diff --git a/internal/api/grpc/system/instance.go b/internal/api/grpc/system/instance.go index 3650392192..2fe8187524 100644 --- a/internal/api/grpc/system/instance.go +++ b/internal/api/grpc/system/instance.go @@ -6,6 +6,7 @@ import ( "github.com/zitadel/zitadel/internal/api/authz" instance_grpc "github.com/zitadel/zitadel/internal/api/grpc/instance" "github.com/zitadel/zitadel/internal/api/grpc/object" + "github.com/zitadel/zitadel/internal/query" object_pb "github.com/zitadel/zitadel/pkg/grpc/object" system_pb "github.com/zitadel/zitadel/pkg/grpc/system" ) @@ -29,13 +30,13 @@ func (s *Server) ListInstances(ctx context.Context, req *system_pb.ListInstances } func (s *Server) GetInstance(ctx context.Context, req *system_pb.GetInstanceRequest) (*system_pb.GetInstanceResponse, error) { - ctx = authz.WithInstanceID(ctx, req.Id) + ctx = authz.WithInstanceID(ctx, req.InstanceId) instance, err := s.query.Instance(ctx) if err != nil { return nil, err } return &system_pb.GetInstanceResponse{ - Instance: instance_grpc.InstanceToPb(instance), + Instance: instance_grpc.InstanceDetailToPb(instance), }, nil } @@ -45,7 +46,7 @@ func (s *Server) AddInstance(ctx context.Context, req *system_pb.AddInstanceRequ return nil, err } return &system_pb.AddInstanceResponse{ - Id: id, + InstanceId: id, Details: object.AddToDetailsPb( details.Sequence, details.EventDate, @@ -55,8 +56,30 @@ func (s *Server) AddInstance(ctx context.Context, req *system_pb.AddInstanceRequ return nil, nil } +func (s *Server) ExistsDomain(ctx context.Context, req *system_pb.ExistsDomainRequest) (*system_pb.ExistsDomainResponse, error) { + domainQuery, err := query.NewInstanceDomainDomainSearchQuery(query.TextEqualsIgnoreCase, req.Domain) + + query := &query.InstanceDomainSearchQueries{ + SearchRequest: query.SearchRequest{ + Offset: 0, + Limit: 1, + Asc: true, + }, + Queries: []query.SearchQuery{ + domainQuery, + }, + } + domains, err := s.query.SearchInstanceDomains(ctx, query) + if err != nil { + return nil, err + } + return &system_pb.ExistsDomainResponse{ + Exists: domains.Count > 0, + }, nil +} + func (s *Server) ListDomains(ctx context.Context, req *system_pb.ListDomainsRequest) (*system_pb.ListDomainsResponse, error) { - ctx = authz.WithInstanceID(ctx, req.Id) + ctx = authz.WithInstanceID(ctx, req.InstanceId) queries, err := ListInstanceDomainsRequestToModel(req) if err != nil { return nil, err @@ -77,7 +100,7 @@ func (s *Server) ListDomains(ctx context.Context, req *system_pb.ListDomainsRequ } func (s *Server) AddDomain(ctx context.Context, req *system_pb.AddDomainRequest) (*system_pb.AddDomainResponse, error) { - ctx = authz.WithInstanceID(ctx, req.Id) + ctx = authz.WithInstanceID(ctx, req.InstanceId) instance, err := s.query.Instance(ctx) if err != nil { return nil, err @@ -97,7 +120,7 @@ func (s *Server) AddDomain(ctx context.Context, req *system_pb.AddDomainRequest) } func (s *Server) RemoveDomain(ctx context.Context, req *system_pb.RemoveDomainRequest) (*system_pb.RemoveDomainResponse, error) { - ctx = authz.WithInstanceID(ctx, req.Id) + ctx = authz.WithInstanceID(ctx, req.InstanceId) details, err := s.command.RemoveInstanceDomain(ctx, req.Domain) if err != nil { return nil, err @@ -112,7 +135,7 @@ func (s *Server) RemoveDomain(ctx context.Context, req *system_pb.RemoveDomainRe } func (s *Server) SetPrimaryDomain(ctx context.Context, req *system_pb.SetPrimaryDomainRequest) (*system_pb.SetPrimaryDomainResponse, error) { - ctx = authz.WithInstanceID(ctx, req.Id) + ctx = authz.WithInstanceID(ctx, req.InstanceId) details, err := s.command.SetPrimaryInstanceDomain(ctx, req.Domain) if err != nil { return nil, err diff --git a/internal/api/grpc/system/instance_converter.go b/internal/api/grpc/system/instance_converter.go index 761e5cd3fe..35ef672b2c 100644 --- a/internal/api/grpc/system/instance_converter.go +++ b/internal/api/grpc/system/instance_converter.go @@ -7,6 +7,7 @@ import ( "github.com/zitadel/zitadel/internal/query" instance_pb "github.com/zitadel/zitadel/pkg/grpc/instance" system_pb "github.com/zitadel/zitadel/pkg/grpc/system" + "golang.org/x/text/language" ) func AddInstancePbToSetupInstance(req *system_pb.AddInstanceRequest, defaultInstance command.InstanceSetup) *command.InstanceSetup { @@ -20,18 +21,32 @@ func AddInstancePbToSetupInstance(req *system_pb.AddInstanceRequest, defaultInst if req.FirstOrgName != "" { defaultInstance.Org.Name = req.FirstOrgName } - if req.OwnerEmail != "" { - defaultInstance.Org.Human.Email.Address = req.OwnerEmail + if req.OwnerEmail.Email != "" { + defaultInstance.Org.Human.Email.Address = req.OwnerEmail.Email + defaultInstance.Org.Human.Email.Verified = req.OwnerEmail.IsEmailVerified } - if req.OwnerUsername != "" { - defaultInstance.Org.Human.Username = req.OwnerUsername + if req.OwnerProfile != nil { + if req.OwnerProfile.FirstName != "" { + defaultInstance.Org.Human.FirstName = req.OwnerProfile.FirstName + } + if req.OwnerProfile.LastName != "" { + defaultInstance.Org.Human.LastName = req.OwnerProfile.LastName + } + if req.OwnerProfile.PreferredLanguage != "" { + lang, err := language.Parse(req.OwnerProfile.PreferredLanguage) + if err == nil { + defaultInstance.Org.Human.PreferredLang = lang + } + } } - if req.OwnerFirstName != "" { - defaultInstance.Org.Human.FirstName = req.OwnerFirstName + if req.OwnerUserName != "" { + defaultInstance.Org.Human.Username = req.OwnerUserName } - if req.OwnerLastName != "" { - defaultInstance.Org.Human.LastName = req.OwnerLastName + if req.OwnerPassword != nil { + defaultInstance.Org.Human.Password = req.OwnerPassword.Password + defaultInstance.Org.Human.PasswordChangeRequired = req.OwnerPassword.PasswordChangeRequired } + return &defaultInstance } func ListInstancesRequestToModel(req *system_pb.ListInstancesRequest) (*query.InstanceSearchQueries, error) { diff --git a/internal/query/instance.go b/internal/query/instance.go index e4f5a83f7d..dbb032101b 100644 --- a/internal/query/instance.go +++ b/internal/query/instance.go @@ -75,6 +75,7 @@ type Instance struct { ChangeDate time.Time CreationDate time.Time Sequence uint64 + Name string GlobalOrgID string IAMProjectID string @@ -83,6 +84,7 @@ type Instance struct { DefaultLang language.Tag SetupStarted domain.Step SetupDone domain.Step + Domains []*InstanceDomain host string } @@ -159,7 +161,7 @@ func (q *Queries) SearchInstances(ctx context.Context, queries *InstanceSearchQu } func (q *Queries) Instance(ctx context.Context) (*Instance, error) { - stmt, scan := prepareInstanceQuery(authz.GetInstance(ctx).RequestedDomain()) + stmt, scan := prepareInstanceDomainQuery(authz.GetInstance(ctx).RequestedDomain()) query, args, err := stmt.Where(sq.Eq{ InstanceColumnID.identifier(): authz.GetInstance(ctx).InstanceID(), }).ToSql() @@ -167,7 +169,10 @@ func (q *Queries) Instance(ctx context.Context) (*Instance, error) { return nil, errors.ThrowInternal(err, "QUERY-d9ngs", "Errors.Query.SQLStatement") } - row := q.client.QueryRowContext(ctx, query, args...) + row, err := q.client.QueryContext(ctx, query, args...) + if err != nil { + return nil, err + } return scan(row) } @@ -181,7 +186,10 @@ func (q *Queries) InstanceByHost(ctx context.Context, host string) (authz.Instan return nil, errors.ThrowInternal(err, "QUERY-SAfg2", "Errors.Query.SQLStatement") } - row := q.client.QueryRowContext(ctx, query, args...) + row, err := q.client.QueryContext(ctx, query, args...) + if err != nil { + return nil, err + } return scan(row) } @@ -226,9 +234,9 @@ func prepareInstanceQuery(host string) (sq.SelectBuilder, func(*sql.Row) (*Insta ) if err != nil { if errs.Is(err, sql.ErrNoRows) { - return nil, errors.ThrowNotFound(err, "QUERY-n0wng", "Errors.IAM.NotFound") + return nil, errors.ThrowNotFound(err, "QUERY-5m09s", "Errors.IAM.NotFound") } - return nil, errors.ThrowInternal(err, "QUERY-d9nw", "Errors.Internal") + return nil, errors.ThrowInternal(err, "QUERY-3j9sf", "Errors.Internal") } instance.DefaultLang = language.Make(lang) return instance, nil @@ -241,6 +249,7 @@ func prepareInstancesQuery() (sq.SelectBuilder, func(*sql.Rows) (*Instances, err InstanceColumnCreationDate.identifier(), InstanceColumnChangeDate.identifier(), InstanceColumnSequence.identifier(), + InstanceColumnName.identifier(), InstanceColumnGlobalOrgID.identifier(), InstanceColumnProjectID.identifier(), InstanceColumnConsoleID.identifier(), @@ -262,6 +271,7 @@ func prepareInstancesQuery() (sq.SelectBuilder, func(*sql.Rows) (*Instances, err &instance.CreationDate, &instance.ChangeDate, &instance.Sequence, + &instance.Name, &instance.GlobalOrgID, &instance.IAMProjectID, &instance.ConsoleID, @@ -290,12 +300,13 @@ func prepareInstancesQuery() (sq.SelectBuilder, func(*sql.Rows) (*Instances, err } } -func prepareInstanceDomainQuery(host string) (sq.SelectBuilder, func(*sql.Row) (*Instance, error)) { +func prepareInstanceDomainQuery(host string) (sq.SelectBuilder, func(*sql.Rows) (*Instance, error)) { return sq.Select( InstanceColumnID.identifier(), InstanceColumnCreationDate.identifier(), InstanceColumnChangeDate.identifier(), InstanceColumnSequence.identifier(), + InstanceColumnName.identifier(), InstanceColumnGlobalOrgID.identifier(), InstanceColumnProjectID.identifier(), InstanceColumnConsoleID.identifier(), @@ -303,33 +314,73 @@ func prepareInstanceDomainQuery(host string) (sq.SelectBuilder, func(*sql.Row) ( InstanceColumnSetupStarted.identifier(), InstanceColumnSetupDone.identifier(), InstanceColumnDefaultLanguage.identifier(), + InstanceDomainDomainCol.identifier(), + InstanceDomainIsPrimaryCol.identifier(), + InstanceDomainIsGeneratedCol.identifier(), + InstanceDomainCreationDateCol.identifier(), + InstanceDomainChangeDateCol.identifier(), + InstanceDomainSequenceCol.identifier(), ). From(instanceTable.identifier()). LeftJoin(join(InstanceDomainInstanceIDCol, InstanceColumnID)). PlaceholderFormat(sq.Dollar), - func(row *sql.Row) (*Instance, error) { - instance := &Instance{host: host} - lang := "" - err := row.Scan( - &instance.ID, - &instance.CreationDate, - &instance.ChangeDate, - &instance.Sequence, - &instance.GlobalOrgID, - &instance.IAMProjectID, - &instance.ConsoleID, - &instance.ConsoleAppID, - &instance.SetupStarted, - &instance.SetupDone, - &lang, - ) - if err != nil { - if errs.Is(err, sql.ErrNoRows) { - return nil, errors.ThrowNotFound(err, "QUERY-n0wng", "Errors.IAM.NotFound") - } - return nil, errors.ThrowInternal(err, "QUERY-d9nw", "Errors.Internal") + func(rows *sql.Rows) (*Instance, error) { + instance := &Instance{ + host: host, + Domains: make([]*InstanceDomain, 0), + } + lang := "" + for rows.Next() { + var ( + domain sql.NullString + isPrimary sql.NullBool + isGenerated sql.NullBool + changeDate sql.NullTime + creationDate sql.NullTime + sequecne sql.NullInt64 + ) + err := rows.Scan( + &instance.ID, + &instance.CreationDate, + &instance.ChangeDate, + &instance.Sequence, + &instance.Name, + &instance.GlobalOrgID, + &instance.IAMProjectID, + &instance.ConsoleID, + &instance.ConsoleAppID, + &instance.SetupStarted, + &instance.SetupDone, + &lang, + &domain, + &isPrimary, + &isGenerated, + &changeDate, + &creationDate, + &sequecne, + ) + if err != nil { + if errs.Is(err, sql.ErrNoRows) { + return nil, errors.ThrowNotFound(err, "QUERY-n0wng", "Errors.IAM.NotFound") + } + return nil, errors.ThrowInternal(err, "QUERY-d9nw", "Errors.Internal") + } + if !domain.Valid { + continue + } + instance.Domains = append(instance.Domains, &InstanceDomain{ + CreationDate: creationDate.Time, + ChangeDate: changeDate.Time, + Sequence: uint64(sequecne.Int64), + Domain: domain.String, + IsPrimary: isPrimary.Bool, + IsGenerated: isGenerated.Bool, + InstanceID: instance.ID, + }) + } + if err := rows.Close(); err != nil { + return nil, errors.ThrowInternal(err, "QUERY-Dfbe2", "Errors.Query.CloseRows") } - instance.DefaultLang = language.Make(lang) return instance, nil } } diff --git a/internal/query/search_query.go b/internal/query/search_query.go index 169e2c8211..2f71a906ce 100644 --- a/internal/query/search_query.go +++ b/internal/query/search_query.go @@ -31,18 +31,16 @@ func (req *SearchRequest) toQuery(query sq.SelectBuilder) sq.SelectBuilder { } if !req.SortingColumn.isZero() { - clause := "LOWER(" + sqlPlaceholder + ")" + clause := req.SortingColumn.orderBy() if !req.Asc { clause += " DESC" } - query = query.OrderByClause(clause, req.SortingColumn.identifier()) + query = query.OrderByClause(clause) } return query } -const sqlPlaceholder = "?" - type SearchQuery interface { toQuery(sq.SelectBuilder) sq.SelectBuilder comp() sq.Sqlizer @@ -367,8 +365,9 @@ func (t table) isZero() bool { } type Column struct { - name string - table table + name string + table table + isOrderByLower bool } func (c Column) identifier() string { @@ -381,6 +380,13 @@ func (c Column) identifier() string { return c.name } +func (c Column) orderBy() string { + if !c.isOrderByLower { + return c.identifier() + } + return "LOWER(" + c.identifier() + ")" +} + func (c Column) setTable(t table) Column { c.table = t return c diff --git a/internal/query/search_query_test.go b/internal/query/search_query_test.go index 7af1c3af77..47ae03c9cf 100644 --- a/internal/query/search_query_test.go +++ b/internal/query/search_query_test.go @@ -19,6 +19,11 @@ var ( name: "test_col", table: testTable, } + testLowerCol = Column{ + name: "test_lower_col", + table: testTable, + isOrderByLower: true, + } testNoCol = Column{ name: "", table: testTable, @@ -34,7 +39,6 @@ func TestSearchRequest_ToQuery(t *testing.T) { } type want struct { stmtAddition string - args []interface{} } tests := []struct { name string @@ -46,7 +50,6 @@ func TestSearchRequest_ToQuery(t *testing.T) { fields: fields{}, want: want{ stmtAddition: "", - args: nil, }, }, { @@ -56,7 +59,6 @@ func TestSearchRequest_ToQuery(t *testing.T) { }, want: want{ stmtAddition: "OFFSET 5", - args: nil, }, }, { @@ -66,7 +68,6 @@ func TestSearchRequest_ToQuery(t *testing.T) { }, want: want{ stmtAddition: "LIMIT 5", - args: nil, }, }, { @@ -76,8 +77,7 @@ func TestSearchRequest_ToQuery(t *testing.T) { Asc: true, }, want: want{ - stmtAddition: "ORDER BY LOWER(?)", - args: []interface{}{"test_table.test_col"}, + stmtAddition: "ORDER BY test_table.test_col", }, }, { @@ -86,8 +86,17 @@ func TestSearchRequest_ToQuery(t *testing.T) { SortingColumn: testCol, }, want: want{ - stmtAddition: "ORDER BY LOWER(?) DESC", - args: []interface{}{"test_table.test_col"}, + stmtAddition: "ORDER BY test_table.test_col DESC", + }, + }, + { + name: "sort lower asc", + fields: fields{ + SortingColumn: testLowerCol, + Asc: true, + }, + want: want{ + stmtAddition: "ORDER BY LOWER(test_table.test_lower_col)", }, }, { @@ -99,8 +108,7 @@ func TestSearchRequest_ToQuery(t *testing.T) { Asc: true, }, want: want{ - stmtAddition: "ORDER BY LOWER(?) LIMIT 10 OFFSET 5", - args: []interface{}{"test_table.test_col"}, + stmtAddition: "ORDER BY test_table.test_col LIMIT 10 OFFSET 5", }, }, } @@ -116,7 +124,7 @@ func TestSearchRequest_ToQuery(t *testing.T) { query := sq.Select((testCol).identifier()).From(testTable.identifier()) expectedQuery, _, _ := query.ToSql() - stmt, args, err := req.toQuery(query).ToSql() + stmt, _, err := req.toQuery(query).ToSql() if len(tt.want.stmtAddition) > 0 { expectedQuery += " " + tt.want.stmtAddition } @@ -124,10 +132,6 @@ func TestSearchRequest_ToQuery(t *testing.T) { t.Errorf("stmt = %q, want %q", stmt, expectedQuery) } - if !reflect.DeepEqual(args, tt.want.args) { - t.Errorf("args = %v, want %v", args, tt.want.stmtAddition) - } - if err != nil { t.Errorf("no error expected but got %v", err) } diff --git a/internal/query/user.go b/internal/query/user.go index 9ae41aa740..65f9a29bb3 100644 --- a/internal/query/user.go +++ b/internal/query/user.go @@ -130,8 +130,9 @@ var ( table: userTable, } UserUsernameCol = Column{ - name: projection.UserUsernameCol, - table: userTable, + name: projection.UserUsernameCol, + table: userTable, + isOrderByLower: true, } UserTypeCol = Column{ name: projection.UserTypeCol, @@ -163,20 +164,24 @@ var ( table: humanTable, } HumanFirstNameCol = Column{ - name: projection.HumanFirstNameCol, - table: humanTable, + name: projection.HumanFirstNameCol, + table: humanTable, + isOrderByLower: true, } HumanLastNameCol = Column{ - name: projection.HumanLastNameCol, - table: humanTable, + name: projection.HumanLastNameCol, + table: humanTable, + isOrderByLower: true, } HumanNickNameCol = Column{ - name: projection.HumanNickNameCol, - table: humanTable, + name: projection.HumanNickNameCol, + table: humanTable, + isOrderByLower: true, } HumanDisplayNameCol = Column{ - name: projection.HumanDisplayNameCol, - table: humanTable, + name: projection.HumanDisplayNameCol, + table: humanTable, + isOrderByLower: true, } HumanPreferredLanguageCol = Column{ name: projection.HumanPreferredLanguageCol, @@ -193,8 +198,9 @@ var ( // email HumanEmailCol = Column{ - name: projection.HumanEmailCol, - table: humanTable, + name: projection.HumanEmailCol, + table: humanTable, + isOrderByLower: true, } HumanIsEmailVerifiedCol = Column{ name: projection.HumanIsEmailVerifiedCol, @@ -221,8 +227,9 @@ var ( table: machineTable, } MachineNameCol = Column{ - name: projection.MachineNameCol, - table: machineTable, + name: projection.MachineNameCol, + table: machineTable, + isOrderByLower: true, } MachineDescriptionCol = Column{ name: projection.MachineDescriptionCol, @@ -397,27 +404,27 @@ func NewUserResourceOwnerSearchQuery(value string, comparison TextComparison) (S } func NewUserUsernameSearchQuery(value string, comparison TextComparison) (SearchQuery, error) { - return NewTextQuery(UserUsernameCol, value, comparison) + return NewTextQuery(Column(UserUsernameCol), value, comparison) } func NewUserFirstNameSearchQuery(value string, comparison TextComparison) (SearchQuery, error) { - return NewTextQuery(HumanFirstNameCol, value, comparison) + return NewTextQuery(Column(HumanFirstNameCol), value, comparison) } func NewUserLastNameSearchQuery(value string, comparison TextComparison) (SearchQuery, error) { - return NewTextQuery(HumanLastNameCol, value, comparison) + return NewTextQuery(Column(HumanLastNameCol), value, comparison) } func NewUserNickNameSearchQuery(value string, comparison TextComparison) (SearchQuery, error) { - return NewTextQuery(HumanNickNameCol, value, comparison) + return NewTextQuery(Column(HumanNickNameCol), value, comparison) } func NewUserDisplayNameSearchQuery(value string, comparison TextComparison) (SearchQuery, error) { - return NewTextQuery(HumanDisplayNameCol, value, comparison) + return NewTextQuery(Column(HumanDisplayNameCol), value, comparison) } func NewUserEmailSearchQuery(value string, comparison TextComparison) (SearchQuery, error) { - return NewTextQuery(HumanEmailCol, value, comparison) + return NewTextQuery(Column(HumanEmailCol), value, comparison) } func NewUserStateSearchQuery(value int32) (SearchQuery, error) { diff --git a/proto/zitadel/instance.proto b/proto/zitadel/instance.proto index e27c0c84b2..e75dd89bf6 100644 --- a/proto/zitadel/instance.proto +++ b/proto/zitadel/instance.proto @@ -25,6 +25,36 @@ message Instance { example: "\"ZITADEL\""; } ]; + string version = 5 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"1.0.0\""; + } + ]; +} + +message InstanceDetail { + string id = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"69629023906488334\"" + } + ]; + zitadel.v1.ObjectDetails details = 2; + State state = 3 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "current state of the instance"; + } + ]; + string name = 4 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"ZITADEL\""; + } + ]; + string version = 5 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"1.0.0\""; + } + ]; + repeated Domain domains = 6; } enum State { diff --git a/proto/zitadel/system.proto b/proto/zitadel/system.proto index 229d81e8d0..baeefec7e4 100644 --- a/proto/zitadel/system.proto +++ b/proto/zitadel/system.proto @@ -102,7 +102,7 @@ service SystemService { }; } - // Returns a list of ZITADEL instances/tenants + // Returns a list of ZITADEL instances rpc ListInstances(ListInstancesRequest) returns (ListInstancesResponse) { option (google.api.http) = { post: "/instances/_search" @@ -113,7 +113,7 @@ service SystemService { // Returns the detail of an instance rpc GetInstance(GetInstanceRequest) returns (GetInstanceResponse) { option (google.api.http) = { - get: "/instances/{id}"; + get: "/instances/{instance_id}"; }; } @@ -130,14 +130,22 @@ service SystemService { // This might take some time rpc RemoveInstance(RemoveInstanceRequest) returns (RemoveInstanceResponse) { option (google.api.http) = { - delete: "/instances/{id}" + delete: "/instances/{instance_id}" + }; + } + + // Checks if a domain exists + rpc ExistsDomain(ExistsDomainRequest) returns (ExistsDomainResponse) { + option (google.api.http) = { + post: "/domains/{domain}/_exists"; + body: "*" }; } // Returns the custom domains of an instance rpc ListDomains(ListDomainsRequest) returns (ListDomainsResponse) { option (google.api.http) = { - post: "/instances/{id}/domains/_search"; + post: "/instances/{instance_id}/domains/_search"; body: "*" }; } @@ -145,7 +153,7 @@ service SystemService { // Returns the domain of an instance rpc AddDomain(AddDomainRequest) returns (AddDomainResponse) { option (google.api.http) = { - post: "/instances/{id}/domains"; + post: "/instances/{instance_id}/domains"; body: "*" }; } @@ -153,14 +161,14 @@ service SystemService { // Returns the domain of an instance rpc RemoveDomain(RemoveDomainRequest) returns (RemoveDomainResponse) { option (google.api.http) = { - delete: "/instances/{id}/domains/{domain}"; + delete: "/instances/{instance_id}/domains/{domain}"; }; } // Returns the domain of an instance rpc SetPrimaryDomain(SetPrimaryDomainRequest) returns (SetPrimaryDomainResponse) { option (google.api.http) = { - post: "/instances/{id}/domains/_set_primary"; + post: "/instances/{instance_id}/domains/_set_primary"; body: "*" }; } @@ -309,32 +317,44 @@ message ListInstancesResponse { } message GetInstanceRequest { - string id = 1 [(validate.rules).string = {min_len: 1, max_len: 200}]; + string instance_id = 1 [(validate.rules).string = {min_len: 1, max_len: 200}]; } message GetInstanceResponse { - zitadel.instance.v1.Instance instance = 1; + zitadel.instance.v1.InstanceDetail instance = 1; } message AddInstanceRequest { + message Profile { + string first_name = 1 [(validate.rules).string = {max_len: 200}]; + string last_name = 2 [(validate.rules).string = {max_len: 200}]; + string preferred_language = 5 [(validate.rules).string = {max_len: 10}]; + } + message Email { + string email = 1[(validate.rules).string = {min_len: 1, max_len: 200}]; + bool is_email_verified = 2; + } + message Password { + string password = 1 [(validate.rules).string = {max_len: 200}]; + bool password_change_required = 2; + } + string instance_name = 1 [(validate.rules).string = {min_len: 1, max_len: 200}]; string first_org_name = 2 [(validate.rules).string = {max_len: 200}]; string custom_domain = 3 [(validate.rules).string = {max_len: 200}]; - string owner_first_name = 4 [(validate.rules).string = {max_len: 200}]; - string owner_last_name = 5 [(validate.rules).string = {max_len: 200}]; - string owner_email = 6 [(validate.rules).string = {min_len: 1, max_len: 200}]; - string owner_username = 7 [(validate.rules).string = {max_len: 200}]; - uint64 request_limit = 8; - uint64 action_mins_limit = 9; + string owner_user_name = 4 [(validate.rules).string = {max_len: 200}]; + Email owner_email = 5 [(validate.rules).message.required = true]; + Profile owner_profile = 6 [(validate.rules).message.required = false]; + Password owner_password = 7 [(validate.rules).message.required = false]; } message AddInstanceResponse { - string id = 1; + string instance_id = 1; zitadel.v1.ObjectDetails details = 2; } message RemoveInstanceRequest { - string id = 1 [(validate.rules).string = {min_len: 1, max_len: 200}]; + string instance_id = 1 [(validate.rules).string = {min_len: 1, max_len: 200}]; } message RemoveInstanceResponse { @@ -342,7 +362,7 @@ message RemoveInstanceResponse { } message GetUsageRequest { - string id = 1 [(validate.rules).string = {min_len: 1, max_len: 200}]; + string instance_id = 1 [(validate.rules).string = {min_len: 1, max_len: 200}]; } message GetUsageResponse { @@ -351,8 +371,16 @@ message GetUsageResponse { uint64 executed_action_mins = 3; } +message ExistsDomainRequest { + string domain = 1 [(validate.rules).string = {min_len: 1, max_len: 200}]; +} + +message ExistsDomainResponse { + bool exists = 1; +} + message ListDomainsRequest { - string id = 1 [(validate.rules).string = {min_len: 1, max_len: 200}];//list limitations and ordering + string instance_id = 1 [(validate.rules).string = {min_len: 1, max_len: 200}];//list limitations and ordering zitadel.v1.ListQuery query = 2; // the field the result is sorted zitadel.instance.v1.DomainFieldName sorting_column = 3; @@ -367,7 +395,7 @@ message ListDomainsResponse { } message AddDomainRequest { - string id = 1 [(validate.rules).string = {min_len: 1, max_len: 200}]; + string instance_id = 1 [(validate.rules).string = {min_len: 1, max_len: 200}]; string domain = 2 [(validate.rules).string = {min_len: 1, max_len: 200}]; } @@ -376,7 +404,7 @@ message AddDomainResponse { } message RemoveDomainRequest { - string id = 1 [(validate.rules).string = {min_len: 1, max_len: 200}]; + string instance_id = 1 [(validate.rules).string = {min_len: 1, max_len: 200}]; string domain = 2 [(validate.rules).string = {min_len: 1, max_len: 200}]; } @@ -385,7 +413,7 @@ message RemoveDomainResponse { } message SetPrimaryDomainRequest { - string id = 1 [(validate.rules).string = {min_len: 1, max_len: 200}]; + string instance_id = 1 [(validate.rules).string = {min_len: 1, max_len: 200}]; string domain = 2 [(validate.rules).string = {min_len: 1, max_len: 200}]; }