Files
zitadel/backend/v3/storage/database/repository/user_metadata.go
T
5df8a97f9d feat(repository): implement user repository (#10967)
## Summary

This PR introduces the relational user repository for backend v3,
including schema, repository APIs, reducers/projections, and tests for
both human and machine users.

## What changed

* Added user-focused relational migrations (users, verifications,
metadata, PATs, machine keys, passkeys, identity provider links) and
related FK wiring.
* Implemented/expanded repository domain contracts and storage
implementations for human and machine user flows.
* Added support for machine secret handling and aligned access token
type modeling.
* Extended user domain models (verifications, password/email/phone/TOTP
state, passkeys, metadata, invite handling, identity provider links).
* Updated projection/reduction coverage and integration tests for key
user lifecycle events.
* Regenerated and updated mocks/enums plus supporting database
change/column utilities.

## Notes for reviewers

* This PR is large because it establishes the foundational relational
user storage layer and related test scaffolding. Sorry for that.

## Closes

Closes #10218
Closes #10224
Closes #10225
Closes #10209
Closes #10802

---------

Co-authored-by: Iraq Jaber <[email protected]>
Co-authored-by: Copilot <[email protected]>
Co-authored-by: Marco A. <[email protected]>
Co-authored-by: Tim Möhlmann <[email protected]>
2026-02-23 16:03:47 +01:00

122 lines
4.3 KiB
Go

package repository
import (
"strings"
"github.com/zitadel/zitadel/backend/v3/domain"
"github.com/zitadel/zitadel/backend/v3/storage/database"
)
type userMetadata struct{}
func (userMetadata) qualifiedTableName() string {
return "zitadel.user_metadata"
}
func (userMetadata) unqualifiedTableName() string {
return "user_metadata"
}
// -------------------------------------------------------------
// changes
// -------------------------------------------------------------
// SetMetadata implements [domain.UserRepository].
func (u userMetadata) SetMetadata(metadata ...*domain.Metadata) database.Change {
return database.NewCTEChange(
func(builder *database.StatementBuilder) {
builder.WriteString("INSERT INTO zitadel.user_metadata(instance_id, user_id, key, value, created_at, updated_at)")
builder.WriteString(" SELECT existing_user.instance_id, existing_user.id, md.key::TEXT, md.value::BYTEA, md.created_at::TIMESTAMPTZ, md.updated_at::TIMESTAMPTZ")
builder.WriteString(" FROM existing_user CROSS JOIN (VALUES ")
for i, md := range metadata {
if i > 0 {
builder.WriteString(", ")
}
builder.WriteRune('(')
// The complex placeholder handling is needed to append the type casts
// if we use WriteArgs directly, pgx resolves all args as TEXT instead of the correct type
placeholders := make([]string, 4)
placeholders[0] = builder.AppendArg(md.Key)
placeholders[1] = builder.AppendArg(md.Value) + "::BYTEA"
var createdAt, updatedAt any = database.NowInstruction, database.NullInstruction
if !md.CreatedAt.IsZero() {
createdAt = md.CreatedAt
}
if !md.UpdatedAt.IsZero() {
updatedAt = md.UpdatedAt
}
placeholders[2] = builder.AppendArg(createdAt) + "::TIMESTAMPTZ"
placeholders[3] = builder.AppendArg(updatedAt) + "::TIMESTAMPTZ"
builder.WriteString(strings.Join(placeholders, ", "))
builder.WriteRune(')')
}
builder.WriteString(") AS md(key, value, created_at, updated_at) ON CONFLICT (")
database.Columns{
u.instanceIDColumn(),
u.userIDColumn(),
u.keyColumn(),
}.WriteUnqualified(builder)
builder.WriteString(") DO UPDATE SET value = EXCLUDED.value, updated_at = EXCLUDED.updated_at WHERE ")
u.valueColumn().WriteQualified(builder)
builder.WriteString(" IS DISTINCT FROM EXCLUDED.value")
},
nil,
)
}
// RemoveMetadata implements [domain.UserRepository].
func (u userMetadata) RemoveMetadata(condition database.Condition) database.Change {
return database.NewCTEChange(
func(builder *database.StatementBuilder) {
builder.WriteString("DELETE FROM zitadel.user_metadata USING ")
builder.WriteString(existingUser.unqualifiedTableName())
writeCondition(builder, database.And(
database.NewColumnCondition(existingUser.InstanceIDColumn(), u.instanceIDColumn()),
database.NewColumnCondition(existingUser.IDColumn(), u.userIDColumn()),
condition,
))
},
nil,
)
}
// -------------------------------------------------------------
// conditions
// -------------------------------------------------------------
func (u userMetadata) MetadataConditions() domain.UserMetadataConditions {
return u
}
// MetadataKeyCondition implements [domain.UserMetadataConditions].
func (u userMetadata) MetadataKeyCondition(op database.TextOperation, key string) database.Condition {
return database.NewTextCondition(u.keyColumn(), op, key)
}
// MetadataValueCondition implements [domain.UserMetadataConditions].
func (u userMetadata) MetadataValueCondition(op database.BytesOperation, value []byte) database.Condition {
return database.NewBytesCondition[[]byte](database.SHA256Column(u.valueColumn()), op, database.SHA256Value(value))
}
var _ domain.UserMetadataConditions = (*userMetadata)(nil)
// -------------------------------------------------------------
// columns
// -------------------------------------------------------------
func (u userMetadata) instanceIDColumn() database.Column {
return database.NewColumn(u.unqualifiedTableName(), "instance_id")
}
func (u userMetadata) userIDColumn() database.Column {
return database.NewColumn(u.unqualifiedTableName(), "user_id")
}
func (u userMetadata) keyColumn() database.Column {
return database.NewColumn(u.unqualifiedTableName(), "key")
}
func (u userMetadata) valueColumn() database.Column {
return database.NewColumn(u.unqualifiedTableName(), "value")
}