mirror of
https://github.com/andwati/zitadel.git
synced 2026-10-18 05:52:51 +00:00
Introduce a new SQL migration that creates the `login_names` table and associated triggers/functions for managing login names based on domain policies and user updates. Manual tests confirm the functionality works as intended.
490 lines
21 KiB
Go
490 lines
21 KiB
Go
package repository
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"time"
|
|
|
|
"github.com/zitadel/zitadel/backend/v3/domain"
|
|
"github.com/zitadel/zitadel/backend/v3/storage/database"
|
|
"github.com/zitadel/zitadel/internal/zerrors"
|
|
)
|
|
|
|
// -------------------------------------------------------------
|
|
// repository
|
|
// -------------------------------------------------------------
|
|
|
|
func UserRepository() domain.UserRepository {
|
|
return new(user)
|
|
}
|
|
|
|
func MachineUserRepository() domain.MachineUserRepository {
|
|
return &userMachine{user: user{}}
|
|
}
|
|
|
|
type user struct {
|
|
verification verification
|
|
tableName string
|
|
machineKey
|
|
userPersonalAccessToken
|
|
userMetadata
|
|
userPasskey
|
|
userIdentityProviderLink
|
|
userLoginName
|
|
}
|
|
|
|
func (u user) HumanRepository() domain.HumanUserRepository {
|
|
return userHuman{user: u}
|
|
}
|
|
|
|
// existingUser is used to get the columns and conditions for the CTE that selects the existing user in update and delete operations.
|
|
var existingUser = user{tableName: "existing_user"}
|
|
|
|
// Create implements [domain.UserRepository].
|
|
func (u user) Create(ctx context.Context, client database.QueryExecutor, user *domain.User) error {
|
|
var create func(context.Context, *database.StatementBuilder, database.QueryExecutor, *domain.User) error
|
|
switch {
|
|
case user.Human != nil:
|
|
create = userHuman{user: u}.create
|
|
case user.Machine != nil:
|
|
create = userMachine{user: u}.create
|
|
default:
|
|
return zerrors.ThrowInvalidArgument(nil, "REPOS-KxMnG", "no user type defined")
|
|
}
|
|
// we need to cheat a bit here to be able to use CTEChanges
|
|
// we pretend we have an existing user to be able to use the existing change mechanisms
|
|
builder := database.NewStatementBuilder("WITH existing_user AS (SELECT $1 AS instance_id, $2 AS organization_id, $3 AS id)", user.InstanceID, user.OrganizationID, user.ID)
|
|
return create(ctx, builder, client, user)
|
|
}
|
|
|
|
// Delete implements [domain.UserRepository].
|
|
func (u user) Delete(ctx context.Context, client database.QueryExecutor, condition database.Condition) (int64, error) {
|
|
if err := checkPKCondition(u, condition); err != nil {
|
|
return 0, err
|
|
}
|
|
|
|
builder := database.NewStatementBuilder("DELETE FROM zitadel.users")
|
|
writeCondition(builder, condition)
|
|
return client.Exec(ctx, builder.String(), builder.Args()...)
|
|
}
|
|
|
|
var queryUserStmt = "SELECT users.instance_id, users.organization_id, users.id, users.username" +
|
|
", users.state, users.created_at, users.updated_at" +
|
|
// metadata
|
|
`, jsonb_agg(DISTINCT jsonb_build_object('instanceId', user_metadata.instance_id, 'key', user_metadata.key, 'value', encode(user_metadata.value, 'base64'), 'createdAt', user_metadata.created_at, 'updatedAt', user_metadata.updated_at)) FILTER (WHERE user_metadata.user_id IS NOT NULL) AS metadata` +
|
|
// login names
|
|
`, jsonb_agg(DISTINCT jsonb_build_object('loginName', login_names.login_name, 'isPreferred', login_names.is_preferred)) FILTER (WHERE login_names.user_id IS NOT NULL) AS login_names` +
|
|
// machine
|
|
`, CASE WHEN users.type = 'machine' THEN jsonb_build_object('name', users.name` +
|
|
`, 'description', users.description, 'secret', users.secret` +
|
|
`, 'accessTokenType', users.access_token_type` +
|
|
`, 'keys', jsonb_agg(DISTINCT jsonb_build_object('id', machine_keys.id, 'publicKey', encode(machine_keys.public_key, 'base64'), 'createdAt', machine_keys.created_at, 'expiresAt', machine_keys.expires_at, 'type', machine_keys.type)) FILTER (WHERE machine_keys.user_id IS NOT NULL)` +
|
|
`, 'pats', jsonb_agg(DISTINCT jsonb_build_object('id', user_personal_access_tokens.id, 'createdAt', user_personal_access_tokens.created_at, 'expiresAt', user_personal_access_tokens.expires_at, 'scopes', user_personal_access_tokens.scopes)) FILTER (WHERE user_personal_access_tokens.user_id IS NOT NULL)` +
|
|
`) END AS machine` +
|
|
// human
|
|
`, CASE WHEN users.type = 'human' THEN jsonb_build_object('firstName', users.first_name, 'lastName', users.last_name` +
|
|
`, 'nickname', users.nickname, 'displayName', users.display_name` +
|
|
`, 'preferredLanguage', users.preferred_language, 'gender', users.gender` +
|
|
`, 'avatarKey', users.avatar_key` +
|
|
`, 'multifactorInitializationSkippedAt', users.multifactor_initialization_skipped_at` +
|
|
`, 'password', jsonb_build_object('hash', users.password_hash, 'isChangeRequired', users.password_change_required, 'changedAt', users.password_changed_at, 'failedAttempts', users.password_failed_attempts, 'lastSuccessfullyCheckedAt', users.password_last_successful_check, 'pendingVerification', ` + verificationQuery(userHuman{}.passwordVerificationIDColumn()) + `)` +
|
|
`, 'email', jsonb_build_object('address', users.email, 'unverifiedAddress', users.unverified_email, 'verifiedAt', users.email_verified_at, 'otp', jsonb_build_object('enabledAt', users.email_otp_enabled_at, 'lastSuccessfullyCheckedAt', users.email_otp_last_successful_check, 'failedAttempts', users.email_otp_failed_attempts), 'pendingVerification', ` + verificationQuery(userHuman{}.emailVerificationIDColumn()) + `)` +
|
|
`, 'phone', CASE WHEN users.phone IS NOT NULL OR users.phone_verification_id IS NOT NULL THEN jsonb_build_object('number', users.phone, 'unverifiedNumber', users.unverified_phone, 'verifiedAt', users.phone_verified_at, 'otp', jsonb_build_object('enabledAt', users.sms_otp_enabled_at, 'lastSuccessfullyCheckedAt', users.sms_otp_last_successful_check, 'failedAttempts', users.sms_otp_failed_attempts), 'pendingVerification', ` + verificationQuery(userHuman{}.phoneVerificationIDColumn()) + `) ELSE NULL END` +
|
|
`, 'totp', CASE WHEN users.totp_secret IS NOT NULL THEN jsonb_build_object('secret', encode(users.totp_secret, 'escape')::JSONB, 'verifiedAt', users.totp_verified_at, 'lastSuccessfullyCheckedAt', users.totp_last_successful_check, 'failedAttempts', users.totp_failed_attempts) ELSE NULL END` +
|
|
`, 'recoveryCodes', CASE WHEN users.recovery_codes IS NOT NULL THEN jsonb_build_object('codes', users.recovery_codes, 'lastSuccessfullyCheckedAt', users.recovery_code_last_successful_check, 'failedAttempts', users.recovery_code_failed_attempts) ELSE NULL END` +
|
|
`, 'passkeys', jsonb_agg(DISTINCT jsonb_build_object('id', user_passkeys.token_id, 'keyId', encode(user_passkeys.key_id, 'base64'), 'type', user_passkeys.type, 'name', user_passkeys.name, 'signCount', user_passkeys.sign_count, 'challenge', encode(user_passkeys.challenge, 'base64'), 'publicKey', encode(user_passkeys.public_key, 'base64'), 'attestationType', user_passkeys.attestation_type, 'aaGuid', encode(user_passkeys.authenticator_attestation_guid, 'base64'), 'rpId', user_passkeys.relying_party_id, 'createdAt', user_passkeys.created_at, 'updatedAt', user_passkeys.updated_at, 'verifiedAt', user_passkeys.verified_at)) FILTER (WHERE user_passkeys.user_id IS NOT NULL)` +
|
|
`, 'verifications', (SELECT jsonb_agg(jsonb_build_object('id', verifications.id, 'code', encode(verifications.code, 'escape')::JSONB, 'createdAt', verifications.created_at, 'updatedAt', verifications.updated_at, 'expiresAt', verifications.created_at+verifications.expiry, 'failedAttempts', verifications.failed_attempts)) FROM zitadel.verifications WHERE verifications.instance_id = users.instance_id AND verifications.user_id = users.id AND verifications.id NOT IN (COALESCE(users.password_verification_id, ''), COALESCE(users.email_verification_id, ''), COALESCE(users.phone_verification_id, ''), COALESCE(users.invite_verification_id, '')))` +
|
|
`, 'identityProviderLinks', jsonb_agg(DISTINCT jsonb_build_object('providerId', user_identity_provider_links.identity_provider_id, 'providedUserId', user_identity_provider_links.provided_user_id, 'providedUsername', user_identity_provider_links.provided_username, 'createdAt', user_identity_provider_links.created_at, 'updatedAt', user_identity_provider_links.updated_at)) FILTER (WHERE user_identity_provider_links.user_id IS NOT NULL)` +
|
|
`, 'invite', CASE WHEN users.invite_verification_id IS NOT NULL OR users.invite_accepted_at IS NOT NULL THEN jsonb_build_object('acceptedAt', users.invite_accepted_at, 'failedAttempts', users.invite_failed_attempts, 'pendingVerification', ` + verificationQuery(userHuman{}.inviteVerificationIDColumn()) + `) ELSE NULL END` +
|
|
`) END AS human FROM zitadel.users`
|
|
|
|
func verificationQuery(column database.Column) string {
|
|
var builder database.StatementBuilder
|
|
builder.WriteString("CASE WHEN ")
|
|
column.WriteQualified(&builder)
|
|
builder.WriteString(` IS NOT NULL THEN (SELECT row_to_json(res.*) FROM (SELECT verifications.id, encode(verifications.code, 'escape')::JSONB AS code, verifications.created_at as "createdAt", verifications.updated_at as "updatedAt", verifications.created_at+verifications.expiry AS "expiresAt", verifications.failed_attempts as "failedAttempts" FROM zitadel.verifications WHERE verifications.instance_id = users.instance_id AND verifications.id = `)
|
|
column.WriteQualified(&builder)
|
|
builder.WriteString(`) AS res) ELSE NULL END`)
|
|
return builder.String()
|
|
}
|
|
|
|
// Get implements [domain.UserRepository].
|
|
func (u user) Get(ctx context.Context, client database.QueryExecutor, opts ...database.QueryOption) (*domain.User, error) {
|
|
options := new(database.QueryOpts)
|
|
for _, opt := range u.appendQueryOpts(opts) {
|
|
opt(options)
|
|
}
|
|
|
|
if !options.Condition.IsRestrictingColumn(u.InstanceIDColumn()) {
|
|
return nil, database.NewMissingConditionError(u.InstanceIDColumn())
|
|
}
|
|
|
|
builder := database.NewStatementBuilder(queryUserStmt)
|
|
options.Write(builder)
|
|
|
|
return scanUser(ctx, client, builder)
|
|
}
|
|
|
|
// List implements [domain.UserRepository].
|
|
func (u user) List(ctx context.Context, client database.QueryExecutor, opts ...database.QueryOption) ([]*domain.User, error) {
|
|
options := new(database.QueryOpts)
|
|
for _, opt := range u.appendQueryOpts(opts) {
|
|
opt(options)
|
|
}
|
|
|
|
if !options.Condition.IsRestrictingColumn(u.InstanceIDColumn()) {
|
|
return nil, database.NewMissingConditionError(u.InstanceIDColumn())
|
|
}
|
|
|
|
builder := database.NewStatementBuilder(queryUserStmt)
|
|
options.Write(builder)
|
|
|
|
return scanUsers(ctx, client, builder)
|
|
}
|
|
|
|
func (u user) appendQueryOpts(opts []database.QueryOption) []database.QueryOption {
|
|
return append(opts,
|
|
u.joinMachineKeys(),
|
|
u.joinPATs(),
|
|
u.joinMetadata(),
|
|
u.joinVerifications(),
|
|
u.joinPasskeys(),
|
|
u.joinIdentityProviderLinks(),
|
|
u.joinLoginNames(),
|
|
database.WithGroupBy(u.PrimaryKeyColumns()...),
|
|
)
|
|
}
|
|
|
|
// Update implements [domain.UserRepository].
|
|
func (u user) Update(ctx context.Context, client database.QueryExecutor, condition database.Condition, changes ...database.Change) (int64, error) {
|
|
if err := checkPKCondition(u, condition); err != nil {
|
|
return 0, err
|
|
}
|
|
if len(changes) == 0 {
|
|
return 0, database.ErrNoChanges
|
|
}
|
|
if !database.Changes(changes).IsOnColumn(u.updatedAtColumn()) {
|
|
changes = append(changes, u.clearUpdatedAt())
|
|
}
|
|
|
|
builder := database.NewStatementBuilder("WITH existing_user AS (SELECT * FROM zitadel.users")
|
|
writeCondition(builder, condition)
|
|
builder.WriteString(") ")
|
|
for i, change := range changes {
|
|
sessionCTE(change, i, 0, builder)
|
|
}
|
|
|
|
builder.WriteString("UPDATE zitadel.users SET ")
|
|
if err := database.Changes(changes).Write(builder); err != nil {
|
|
return 0, err
|
|
}
|
|
|
|
builder.WriteString(" FROM existing_user")
|
|
writeCondition(builder, database.And(
|
|
database.NewColumnCondition(u.IDColumn(), existingUser.IDColumn()),
|
|
database.NewColumnCondition(u.InstanceIDColumn(), existingUser.InstanceIDColumn()),
|
|
))
|
|
|
|
return client.Exec(ctx, builder.String(), builder.Args()...)
|
|
}
|
|
|
|
func (u user) unqualifiedTableName() string {
|
|
if u.tableName != "" {
|
|
return u.tableName
|
|
}
|
|
return "users"
|
|
}
|
|
|
|
// -------------------------------------------------------------
|
|
// changes
|
|
// -------------------------------------------------------------
|
|
|
|
// SetState implements [domain.UserRepository].
|
|
func (u user) SetState(state domain.UserState) database.Change {
|
|
return database.NewChange(u.StateColumn(), state)
|
|
}
|
|
|
|
// SetUpdatedAt implements [domain.UserRepository].
|
|
func (u user) SetUpdatedAt(updatedAt time.Time) database.Change {
|
|
return database.NewChange(u.updatedAtColumn(), updatedAt)
|
|
}
|
|
|
|
// SetUsername implements [domain.UserRepository].
|
|
func (u user) SetUsername(username string) database.Change {
|
|
return database.NewChange(u.UsernameColumn(), username)
|
|
}
|
|
|
|
func (u user) clearUpdatedAt() database.Change {
|
|
return database.NewChange(u.updatedAtColumn(), database.NullInstruction)
|
|
}
|
|
|
|
// -------------------------------------------------------------
|
|
// conditions
|
|
// -------------------------------------------------------------
|
|
|
|
// IDCondition implements [domain.UserRepository].
|
|
func (u user) IDCondition(userID string) database.Condition {
|
|
return database.NewTextCondition(u.IDColumn(), database.TextOperationEqual, userID)
|
|
}
|
|
|
|
// InstanceIDCondition implements [domain.UserRepository].
|
|
func (u user) InstanceIDCondition(instanceID string) database.Condition {
|
|
return database.NewTextCondition(u.InstanceIDColumn(), database.TextOperationEqual, instanceID)
|
|
}
|
|
|
|
// OrganizationIDCondition implements [domain.UserRepository].
|
|
func (u user) OrganizationIDCondition(orgID string) database.Condition {
|
|
return database.NewTextCondition(u.organizationIDColumn(), database.TextOperationEqual, orgID)
|
|
}
|
|
|
|
// PrimaryKeyCondition implements [domain.UserRepository].
|
|
func (u user) PrimaryKeyCondition(instanceID string, userID string) database.Condition {
|
|
return database.And(
|
|
u.InstanceIDCondition(instanceID),
|
|
u.IDCondition(userID),
|
|
)
|
|
}
|
|
|
|
// StateCondition implements [domain.UserRepository].
|
|
func (u user) StateCondition(state domain.UserState) database.Condition {
|
|
return database.NewNumberCondition(u.StateColumn(), database.NumberOperationEqual, state)
|
|
}
|
|
|
|
// TypeCondition implements [domain.UserRepository].
|
|
func (u user) TypeCondition(userType domain.UserType) database.Condition {
|
|
return database.NewNumberCondition(u.TypeColumn(), database.NumberOperationEqual, userType)
|
|
}
|
|
|
|
// UsernameCondition implements [domain.UserRepository].
|
|
func (u user) UsernameCondition(op database.TextOperation, username string) database.Condition {
|
|
return database.NewTextCondition(u.UsernameColumn(), op, username)
|
|
}
|
|
|
|
// ExistsMetadata implements [domain.UserRepository].
|
|
func (u user) ExistsMetadata(condition database.Condition) database.Condition {
|
|
return database.Exists(
|
|
u.userMetadata.qualifiedTableName(),
|
|
database.And(
|
|
database.NewColumnCondition(u.InstanceIDColumn(), u.userMetadata.instanceIDColumn()),
|
|
database.NewColumnCondition(u.IDColumn(), u.userMetadata.userIDColumn()),
|
|
condition,
|
|
),
|
|
)
|
|
}
|
|
|
|
// ExistsPasskey implements [domain.HumanUserRepository].
|
|
func (u user) ExistsPasskey(condition database.Condition) database.Condition {
|
|
return database.Exists(
|
|
u.userPasskey.qualifiedTableName(),
|
|
database.And(
|
|
database.NewColumnCondition(u.InstanceIDColumn(), u.userPasskey.instanceIDColumn()),
|
|
database.NewColumnCondition(u.IDColumn(), u.userPasskey.userIDColumn()),
|
|
condition,
|
|
),
|
|
)
|
|
}
|
|
|
|
// ExistsIdentityProviderLink implements [domain.HumanUserRepository].
|
|
func (u user) ExistsIdentityProviderLink(condition database.Condition) database.Condition {
|
|
return database.Exists(
|
|
u.userIdentityProviderLink.qualifiedTableName(),
|
|
database.And(
|
|
database.NewColumnCondition(u.InstanceIDColumn(), u.userIdentityProviderLink.instanceIDColumn()),
|
|
database.NewColumnCondition(u.IDColumn(), u.userIdentityProviderLink.userIDColumn()),
|
|
condition,
|
|
),
|
|
)
|
|
}
|
|
|
|
// -------------------------------------------------------------
|
|
// columns
|
|
// -------------------------------------------------------------
|
|
|
|
// PrimaryKeyColumns implements [domain.UserRepository].
|
|
func (u user) PrimaryKeyColumns() []database.Column {
|
|
return database.Columns{
|
|
u.InstanceIDColumn(),
|
|
u.IDColumn(),
|
|
}
|
|
}
|
|
|
|
// CreatedAtColumn implements [domain.UserRepository].
|
|
func (u user) CreatedAtColumn() database.Column {
|
|
return database.NewColumn(u.unqualifiedTableName(), "created_at")
|
|
}
|
|
|
|
// StateColumn implements [domain.UserRepository].
|
|
func (u user) StateColumn() database.Column {
|
|
return database.NewColumn(u.unqualifiedTableName(), "state")
|
|
}
|
|
|
|
// TypeColumn implements [domain.UserRepository].
|
|
func (u user) TypeColumn() database.Column {
|
|
return database.NewColumn(u.unqualifiedTableName(), "type")
|
|
}
|
|
|
|
// UsernameColumn implements [domain.UserRepository].
|
|
func (u user) UsernameColumn() database.Column {
|
|
return database.NewColumn(u.unqualifiedTableName(), "username")
|
|
}
|
|
|
|
// IDColumn implements [domain.UserRepository].
|
|
func (u user) IDColumn() database.Column {
|
|
return database.NewColumn(u.unqualifiedTableName(), "id")
|
|
}
|
|
|
|
func (u user) InstanceIDColumn() database.Column {
|
|
return database.NewColumn(u.unqualifiedTableName(), "instance_id")
|
|
}
|
|
|
|
func (u user) updatedAtColumn() database.Column {
|
|
return database.NewColumn(u.unqualifiedTableName(), "updated_at")
|
|
}
|
|
|
|
func (u user) organizationIDColumn() database.Column {
|
|
return database.NewColumn(u.unqualifiedTableName(), "organization_id")
|
|
}
|
|
|
|
// -------------------------------------------------------------
|
|
// scanners
|
|
// -------------------------------------------------------------
|
|
|
|
type rawUser struct {
|
|
domain.User
|
|
Machine *json.RawMessage `json:"machine,omitempty" db:"machine"`
|
|
Human *json.RawMessage `json:"human,omitempty" db:"human"`
|
|
}
|
|
|
|
func (u *rawUser) toDomain() (*domain.User, error) {
|
|
if u.Machine != nil {
|
|
err := json.Unmarshal(*u.Machine, &u.User.Machine)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
} else if u.Human != nil {
|
|
err := json.Unmarshal(*u.Human, &u.User.Human)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
return &u.User, nil
|
|
}
|
|
|
|
func scanUser(ctx context.Context, client database.QueryExecutor, builder *database.StatementBuilder) (*domain.User, error) {
|
|
rows, err := client.Query(ctx, builder.String(), builder.Args()...)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var user rawUser
|
|
if err = rows.(database.CollectableRows).CollectExactlyOneRow(&user); err != nil {
|
|
return nil, err
|
|
}
|
|
return user.toDomain()
|
|
}
|
|
|
|
func scanUsers(ctx context.Context, client database.QueryExecutor, builder *database.StatementBuilder) ([]*domain.User, error) {
|
|
rows, err := client.Query(ctx, builder.String(), builder.Args()...)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var users []rawUser
|
|
if err = rows.(database.CollectableRows).Collect(&users); err != nil {
|
|
return nil, err
|
|
}
|
|
result := make([]*domain.User, len(users))
|
|
for i, user := range users {
|
|
domainUser, err := user.toDomain()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
result[i] = domainUser
|
|
}
|
|
return result, nil
|
|
}
|
|
|
|
// -------------------------------------------------------------
|
|
// sub repositories
|
|
// -------------------------------------------------------------
|
|
|
|
// Human implements [domain.UserRepository].
|
|
func (u user) Human() domain.HumanUserRepository {
|
|
return userHuman{user: u}
|
|
}
|
|
|
|
// Machine implements [domain.UserRepository].
|
|
func (u user) Machine() domain.MachineUserRepository {
|
|
return userMachine{user: u}
|
|
}
|
|
|
|
func (u user) joinMachineKeys() database.QueryOption {
|
|
return database.WithLeftJoin(
|
|
u.machineKey.qualifiedTableName(),
|
|
database.And(
|
|
database.NewColumnCondition(u.InstanceIDColumn(), u.machineKey.instanceIDColumn()),
|
|
database.NewColumnCondition(u.IDColumn(), u.machineKey.userIDColumn()),
|
|
),
|
|
)
|
|
}
|
|
|
|
func (u user) joinPATs() database.QueryOption {
|
|
return database.WithLeftJoin(
|
|
u.userPersonalAccessToken.qualifiedTableName(),
|
|
database.And(
|
|
database.NewColumnCondition(u.InstanceIDColumn(), u.userPersonalAccessToken.instanceIDColumn()),
|
|
database.NewColumnCondition(u.IDColumn(), u.userPersonalAccessToken.userIDColumn()),
|
|
),
|
|
)
|
|
}
|
|
|
|
func (u user) joinMetadata() database.QueryOption {
|
|
return database.WithLeftJoin(
|
|
u.userMetadata.qualifiedTableName(),
|
|
database.And(
|
|
database.NewColumnCondition(u.InstanceIDColumn(), u.userMetadata.instanceIDColumn()),
|
|
database.NewColumnCondition(u.IDColumn(), u.userMetadata.userIDColumn()),
|
|
),
|
|
)
|
|
}
|
|
|
|
func (u user) joinPasskeys() database.QueryOption {
|
|
return database.WithLeftJoin(
|
|
u.userPasskey.qualifiedTableName(),
|
|
database.And(
|
|
database.NewColumnCondition(u.InstanceIDColumn(), u.userPasskey.instanceIDColumn()),
|
|
database.NewColumnCondition(u.IDColumn(), u.userPasskey.userIDColumn()),
|
|
),
|
|
)
|
|
}
|
|
|
|
func (u user) joinVerifications() database.QueryOption {
|
|
return database.WithLeftJoin(
|
|
u.verification.qualifiedTableName(),
|
|
database.And(
|
|
database.NewColumnCondition(u.InstanceIDColumn(), u.verification.instanceIDColumn()),
|
|
database.NewColumnCondition(u.IDColumn(), u.verification.userIDColumn()),
|
|
),
|
|
)
|
|
}
|
|
|
|
func (u user) joinIdentityProviderLinks() database.QueryOption {
|
|
return database.WithLeftJoin(
|
|
u.userIdentityProviderLink.qualifiedTableName(),
|
|
database.And(
|
|
database.NewColumnCondition(u.InstanceIDColumn(), u.userIdentityProviderLink.instanceIDColumn()),
|
|
database.NewColumnCondition(u.IDColumn(), u.userIdentityProviderLink.userIDColumn()),
|
|
),
|
|
)
|
|
}
|
|
|
|
func (u user) joinLoginNames() database.QueryOption {
|
|
return database.WithLeftJoin(
|
|
u.userLoginName.qualifiedTableName(),
|
|
database.And(
|
|
database.NewColumnCondition(u.InstanceIDColumn(), u.userLoginName.instanceIDColumn()),
|
|
database.NewColumnCondition(u.IDColumn(), u.userLoginName.userIDColumn()),
|
|
),
|
|
)
|
|
}
|