package authentik import ( "context" "fmt" "net/url" "strings" ) type akUser struct { PK int `json:"pk"` UUID string `json:"uuid"` Attributes map[string]any `json:"attributes"` } const avatarAttributeKey = "avatar" // FindUserByUUID returns the Authentik core user for an OIDC subject (uuid). func (c *Client) FindUserByUUID(ctx context.Context, userUUID string) (*akUser, bool, error) { userUUID = strings.TrimSpace(userUUID) if userUUID == "" { return nil, false, nil } q := url.Values{} q.Set("uuid", userUUID) var out listResponse[akUser] if err := c.getJSON(ctx, "/api/v3/core/users/?"+q.Encode(), &out); err != nil { return nil, false, err } if len(out.Results) == 0 { return nil, false, nil } user := out.Results[0] if user.Attributes == nil { user.Attributes = map[string]any{} } return &user, true, nil } // GetUserAvatarAttribute reads attributes.avatar from Authentik. func (c *Client) GetUserAvatarAttribute(ctx context.Context, userUUID string) (string, error) { user, found, err := c.FindUserByUUID(ctx, userUUID) if err != nil || !found { return "", err } return avatarAttributeString(user.Attributes[avatarAttributeKey]), nil } // SetUserAvatarAttribute writes attributes.avatar on the Authentik user. func (c *Client) SetUserAvatarAttribute(ctx context.Context, userUUID, avatarURL string) error { user, found, err := c.FindUserByUUID(ctx, userUUID) if err != nil { return err } if !found { return fmt.Errorf("authentik user not found: %s", userUUID) } attrs := cloneAttributes(user.Attributes) avatarURL = strings.TrimSpace(avatarURL) if avatarURL == "" { delete(attrs, avatarAttributeKey) } else { attrs[avatarAttributeKey] = avatarURL } return c.patchJSON(ctx, fmt.Sprintf("/api/v3/core/users/%d/", user.PK), map[string]any{ "attributes": attrs, }) } func cloneAttributes(src map[string]any) map[string]any { if len(src) == 0 { return map[string]any{} } dst := make(map[string]any, len(src)) for k, v := range src { dst[k] = v } return dst } func avatarAttributeString(raw any) string { switch v := raw.(type) { case string: return strings.TrimSpace(v) case []byte: return strings.TrimSpace(string(v)) default: if raw == nil { return "" } return strings.TrimSpace(fmt.Sprint(raw)) } }