2025-04-29 06:03:47 +02:00
|
|
|
package repository
|
|
|
|
|
|
|
|
import (
|
2025-05-08 07:42:53 +02:00
|
|
|
"context"
|
|
|
|
"time"
|
|
|
|
|
2025-04-29 06:03:47 +02:00
|
|
|
"github.com/zitadel/zitadel/backend/v3/domain"
|
|
|
|
"github.com/zitadel/zitadel/backend/v3/storage/database"
|
|
|
|
)
|
|
|
|
|
2025-05-08 07:42:53 +02:00
|
|
|
const queryUserStmt = `SELECT instance_id, org_id, id, username, type, created_at, updated_at, deleted_at,` +
|
|
|
|
` first_name, last_name, email_address, email_verified_at, phone_number, phone_verified_at, description` +
|
|
|
|
` FROM users_view`
|
|
|
|
|
2025-04-29 06:03:47 +02:00
|
|
|
type user struct {
|
2025-05-08 07:42:53 +02:00
|
|
|
repository
|
|
|
|
}
|
|
|
|
|
|
|
|
func UserRepository(client database.QueryExecutor) domain.UserRepository {
|
|
|
|
return &user{
|
|
|
|
repository: repository{
|
|
|
|
client: client,
|
|
|
|
},
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
var _ domain.UserRepository = (*user)(nil)
|
|
|
|
|
|
|
|
// -------------------------------------------------------------
|
|
|
|
// repository
|
|
|
|
// -------------------------------------------------------------
|
|
|
|
|
|
|
|
// Human implements [domain.UserRepository].
|
|
|
|
func (u *user) Human() domain.HumanRepository {
|
|
|
|
return &userHuman{user: u}
|
2025-04-29 06:03:47 +02:00
|
|
|
}
|
|
|
|
|
2025-05-08 07:42:53 +02:00
|
|
|
// Machine implements [domain.UserRepository].
|
|
|
|
func (u *user) Machine() domain.MachineRepository {
|
|
|
|
return &userMachine{user: u}
|
2025-04-29 06:03:47 +02:00
|
|
|
}
|
|
|
|
|
2025-05-08 07:42:53 +02:00
|
|
|
// List implements [domain.UserRepository].
|
|
|
|
func (u *user) List(ctx context.Context, opts ...database.QueryOption) (users []*domain.User, err error) {
|
|
|
|
options := new(database.QueryOpts)
|
|
|
|
for _, opt := range opts {
|
|
|
|
opt(options)
|
2025-04-29 06:03:47 +02:00
|
|
|
}
|
2025-05-08 07:42:53 +02:00
|
|
|
|
2025-06-17 09:46:01 +02:00
|
|
|
builder := database.StatementBuilder{}
|
|
|
|
builder.WriteString(queryUserStmt)
|
|
|
|
options.WriteCondition(&builder)
|
|
|
|
options.WriteOrderBy(&builder)
|
|
|
|
options.WriteLimit(&builder)
|
|
|
|
options.WriteOffset(&builder)
|
|
|
|
|
|
|
|
rows, err := u.client.Query(ctx, builder.String(), builder.Args()...)
|
2025-05-08 07:42:53 +02:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
defer func() {
|
|
|
|
closeErr := rows.Close()
|
|
|
|
if err != nil {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
err = closeErr
|
|
|
|
}()
|
|
|
|
for rows.Next() {
|
|
|
|
user, err := scanUser(rows)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
users = append(users, user)
|
|
|
|
}
|
|
|
|
if err := rows.Err(); err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
return users, nil
|
2025-04-29 06:03:47 +02:00
|
|
|
}
|
|
|
|
|
2025-05-08 07:42:53 +02:00
|
|
|
// Get implements [domain.UserRepository].
|
|
|
|
func (u *user) Get(ctx context.Context, opts ...database.QueryOption) (*domain.User, error) {
|
|
|
|
options := new(database.QueryOpts)
|
|
|
|
for _, opt := range opts {
|
|
|
|
opt(options)
|
2025-04-29 06:03:47 +02:00
|
|
|
}
|
2025-05-08 07:42:53 +02:00
|
|
|
|
2025-06-17 09:46:01 +02:00
|
|
|
builder := database.StatementBuilder{}
|
|
|
|
builder.WriteString(queryUserStmt)
|
|
|
|
options.WriteCondition(&builder)
|
|
|
|
options.WriteOrderBy(&builder)
|
|
|
|
options.WriteLimit(&builder)
|
|
|
|
options.WriteOffset(&builder)
|
2025-05-08 07:42:53 +02:00
|
|
|
|
2025-06-17 09:46:01 +02:00
|
|
|
return scanUser(u.client.QueryRow(ctx, builder.String(), builder.Args()...))
|
2025-04-29 06:03:47 +02:00
|
|
|
}
|
|
|
|
|
2025-05-08 07:42:53 +02:00
|
|
|
const (
|
|
|
|
createHumanStmt = `INSERT INTO human_users (instance_id, org_id, user_id, username, first_name, last_name, email_address, email_verified_at, phone_number, phone_verified_at)` +
|
|
|
|
` VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)` +
|
|
|
|
` RETURNING created_at, updated_at`
|
|
|
|
createMachineStmt = `INSERT INTO user_machines (instance_id, org_id, user_id, username, description)` +
|
|
|
|
` VALUES ($1, $2, $3, $4, $5)` +
|
|
|
|
` RETURNING created_at, updated_at`
|
|
|
|
)
|
|
|
|
|
|
|
|
// Create implements [domain.UserRepository].
|
|
|
|
func (u *user) Create(ctx context.Context, user *domain.User) error {
|
2025-06-17 09:46:01 +02:00
|
|
|
builder := database.StatementBuilder{}
|
|
|
|
builder.AppendArgs(user.InstanceID, user.OrgID, user.ID, user.Username, user.Traits.Type())
|
2025-05-08 07:42:53 +02:00
|
|
|
switch trait := user.Traits.(type) {
|
|
|
|
case *domain.Human:
|
2025-06-17 09:46:01 +02:00
|
|
|
builder.WriteString(createHumanStmt)
|
|
|
|
builder.AppendArgs(trait.FirstName, trait.LastName, trait.Email.Address, trait.Email.VerifiedAt, trait.Phone.Number, trait.Phone.VerifiedAt)
|
2025-05-08 07:42:53 +02:00
|
|
|
case *domain.Machine:
|
2025-06-17 09:46:01 +02:00
|
|
|
builder.WriteString(createMachineStmt)
|
|
|
|
builder.AppendArgs(trait.Description)
|
2025-04-29 06:03:47 +02:00
|
|
|
}
|
2025-06-17 09:46:01 +02:00
|
|
|
return u.client.QueryRow(ctx, builder.String(), builder.Args()...).Scan(&user.CreatedAt, &user.UpdatedAt)
|
2025-05-08 07:42:53 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
// Delete implements [domain.UserRepository].
|
|
|
|
func (u *user) Delete(ctx context.Context, condition database.Condition) error {
|
2025-06-17 09:46:01 +02:00
|
|
|
builder := database.StatementBuilder{}
|
|
|
|
builder.WriteString("DELETE FROM users")
|
|
|
|
u.writeCondition(builder, condition)
|
|
|
|
_, err := u.client.Exec(ctx, builder.String(), builder.Args()...)
|
|
|
|
return err
|
2025-05-08 07:42:53 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
// -------------------------------------------------------------
|
|
|
|
// changes
|
|
|
|
// -------------------------------------------------------------
|
|
|
|
|
|
|
|
// SetUsername implements [domain.userChanges].
|
|
|
|
func (u user) SetUsername(username string) database.Change {
|
|
|
|
return database.NewChange(u.UsernameColumn(), username)
|
|
|
|
}
|
|
|
|
|
|
|
|
// -------------------------------------------------------------
|
|
|
|
// conditions
|
|
|
|
// -------------------------------------------------------------
|
|
|
|
|
|
|
|
// InstanceIDCondition implements [domain.userConditions].
|
|
|
|
func (u user) InstanceIDCondition(instanceID string) database.Condition {
|
|
|
|
return database.NewTextCondition(u.InstanceIDColumn(), database.TextOperationEqual, instanceID)
|
|
|
|
}
|
|
|
|
|
|
|
|
// OrgIDCondition implements [domain.userConditions].
|
|
|
|
func (u user) OrgIDCondition(orgID string) database.Condition {
|
|
|
|
return database.NewTextCondition(u.OrgIDColumn(), database.TextOperationEqual, orgID)
|
|
|
|
}
|
|
|
|
|
|
|
|
// IDCondition implements [domain.userConditions].
|
|
|
|
func (u user) IDCondition(userID string) database.Condition {
|
|
|
|
return database.NewTextCondition(u.IDColumn(), database.TextOperationEqual, userID)
|
|
|
|
}
|
|
|
|
|
|
|
|
// UsernameCondition implements [domain.userConditions].
|
|
|
|
func (u user) UsernameCondition(op database.TextOperation, username string) database.Condition {
|
|
|
|
return database.NewTextCondition(u.UsernameColumn(), op, username)
|
|
|
|
}
|
|
|
|
|
|
|
|
// CreatedAtCondition implements [domain.userConditions].
|
|
|
|
func (u user) CreatedAtCondition(op database.NumberOperation, createdAt time.Time) database.Condition {
|
|
|
|
return database.NewNumberCondition(u.CreatedAtColumn(), op, createdAt)
|
2025-04-29 06:03:47 +02:00
|
|
|
}
|
|
|
|
|
2025-05-08 07:42:53 +02:00
|
|
|
// UpdatedAtCondition implements [domain.userConditions].
|
|
|
|
func (u user) UpdatedAtCondition(op database.NumberOperation, updatedAt time.Time) database.Condition {
|
|
|
|
return database.NewNumberCondition(u.UpdatedAtColumn(), op, updatedAt)
|
|
|
|
}
|
|
|
|
|
|
|
|
// DeletedCondition implements [domain.userConditions].
|
|
|
|
func (u user) DeletedCondition(isDeleted bool) database.Condition {
|
|
|
|
if isDeleted {
|
|
|
|
return database.IsNotNull(u.DeletedAtColumn())
|
|
|
|
}
|
|
|
|
return database.IsNull(u.DeletedAtColumn())
|
|
|
|
}
|
|
|
|
|
|
|
|
// DeletedAtCondition implements [domain.userConditions].
|
|
|
|
func (u user) DeletedAtCondition(op database.NumberOperation, deletedAt time.Time) database.Condition {
|
|
|
|
return database.NewNumberCondition(u.DeletedAtColumn(), op, deletedAt)
|
|
|
|
}
|
|
|
|
|
|
|
|
// -------------------------------------------------------------
|
|
|
|
// columns
|
|
|
|
// -------------------------------------------------------------
|
|
|
|
|
|
|
|
// InstanceIDColumn implements [domain.userColumns].
|
|
|
|
func (user) InstanceIDColumn() database.Column {
|
|
|
|
return database.NewColumn("instance_id")
|
|
|
|
}
|
|
|
|
|
|
|
|
// OrgIDColumn implements [domain.userColumns].
|
|
|
|
func (user) OrgIDColumn() database.Column {
|
|
|
|
return database.NewColumn("org_id")
|
|
|
|
}
|
|
|
|
|
|
|
|
// IDColumn implements [domain.userColumns].
|
|
|
|
func (user) IDColumn() database.Column {
|
|
|
|
return database.NewColumn("id")
|
|
|
|
}
|
|
|
|
|
|
|
|
// UsernameColumn implements [domain.userColumns].
|
|
|
|
func (user) UsernameColumn() database.Column {
|
|
|
|
return database.NewIgnoreCaseColumn("username", "_lower")
|
|
|
|
}
|
|
|
|
|
|
|
|
// FirstNameColumn implements [domain.userColumns].
|
|
|
|
func (user) CreatedAtColumn() database.Column {
|
|
|
|
return database.NewColumn("created_at")
|
|
|
|
}
|
|
|
|
|
|
|
|
// UpdatedAtColumn implements [domain.userColumns].
|
|
|
|
func (user) UpdatedAtColumn() database.Column {
|
|
|
|
return database.NewColumn("updated_at")
|
|
|
|
}
|
|
|
|
|
|
|
|
// DeletedAtColumn implements [domain.userColumns].
|
|
|
|
func (user) DeletedAtColumn() database.Column {
|
|
|
|
return database.NewColumn("deleted_at")
|
|
|
|
}
|
|
|
|
|
2025-06-17 09:46:01 +02:00
|
|
|
func (u *user) writeCondition(
|
|
|
|
builder database.StatementBuilder,
|
|
|
|
condition database.Condition,
|
|
|
|
) {
|
2025-05-08 07:42:53 +02:00
|
|
|
if condition == nil {
|
|
|
|
return
|
|
|
|
}
|
2025-06-17 09:46:01 +02:00
|
|
|
builder.WriteString(" WHERE ")
|
|
|
|
condition.Write(&builder)
|
2025-05-08 07:42:53 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
func (u user) columns() database.Columns {
|
|
|
|
return database.Columns{
|
|
|
|
u.InstanceIDColumn(),
|
|
|
|
u.OrgIDColumn(),
|
|
|
|
u.IDColumn(),
|
|
|
|
u.UsernameColumn(),
|
|
|
|
u.CreatedAtColumn(),
|
|
|
|
u.UpdatedAtColumn(),
|
|
|
|
u.DeletedAtColumn(),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func scanUser(scanner database.Scanner) (*domain.User, error) {
|
|
|
|
var (
|
|
|
|
user domain.User
|
|
|
|
human domain.Human
|
|
|
|
email domain.Email
|
|
|
|
phone domain.Phone
|
|
|
|
machine domain.Machine
|
|
|
|
typ domain.UserType
|
|
|
|
)
|
|
|
|
err := scanner.Scan(
|
|
|
|
&user.InstanceID,
|
|
|
|
&user.OrgID,
|
|
|
|
&user.ID,
|
|
|
|
&user.Username,
|
|
|
|
&typ,
|
|
|
|
&user.CreatedAt,
|
|
|
|
&user.UpdatedAt,
|
|
|
|
&user.DeletedAt,
|
|
|
|
&human.FirstName,
|
|
|
|
&human.LastName,
|
|
|
|
&email.Address,
|
|
|
|
&email.VerifiedAt,
|
|
|
|
&phone.Number,
|
|
|
|
&phone.VerifiedAt,
|
|
|
|
&machine.Description,
|
|
|
|
)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
switch typ {
|
|
|
|
case domain.UserTypeHuman:
|
|
|
|
if email.Address != "" {
|
|
|
|
human.Email = &email
|
|
|
|
}
|
|
|
|
if phone.Number != "" {
|
|
|
|
human.Phone = &phone
|
|
|
|
}
|
|
|
|
user.Traits = &human
|
|
|
|
case domain.UserTypeMachine:
|
|
|
|
user.Traits = &machine
|
|
|
|
}
|
|
|
|
|
|
|
|
return &user, nil
|
|
|
|
}
|