Packages

Social posts module for PhoenixKit — user posts, threaded comments, tags, boards, likes/dislikes, media, mentions, and scheduling

Current section

Files

Jump to
phoenix_kit_posts lib phoenix_kit_posts schemas post_view.ex
Raw

lib/phoenix_kit_posts/schemas/post_view.ex

defmodule PhoenixKitPosts.PostView do
@moduledoc """
Schema for post view tracking (analytics).
Tracks when posts are viewed for analytics purposes. Supports session-based
deduplication to avoid counting multiple views from the same visitor.
## Fields
- `post_uuid` - Reference to the viewed post
- `user_uuid` - Reference to the viewer (nullable for guests)
- `ip_address` - Hashed IP for privacy
- `user_agent_hash` - Hashed browser fingerprint
- `session_id` - Session identifier for deduplication
- `viewed_at` - Timestamp of view
## Examples
# Logged-in user view
%PostView{
post_uuid: "018e3c4a-9f6b-7890-abcd-ef1234567890",
user_uuid: "018e3c4a-1234-5678-abcd-ef1234567890",
ip_address: "hashed_ip",
user_agent_hash: "hashed_ua",
session_id: "session_abc123",
viewed_at: ~U[2025-01-01 12:00:00Z]
}
# Guest view
%PostView{
post_uuid: "018e3c4a-9f6b-7890-abcd-ef1234567890",
user_uuid: nil,
ip_address: "hashed_ip",
user_agent_hash: "hashed_ua",
session_id: "session_xyz789",
viewed_at: ~U[2025-01-01 13:30:00Z]
}
"""
use Ecto.Schema
import Ecto.Changeset
alias PhoenixKit.Utils.Date, as: UtilsDate
@primary_key {:uuid, UUIDv7, autogenerate: true}
@type t :: %__MODULE__{
uuid: UUIDv7.t() | nil,
post_uuid: UUIDv7.t(),
user_uuid: UUIDv7.t() | nil,
ip_address: String.t() | nil,
user_agent_hash: String.t() | nil,
session_id: String.t() | nil,
viewed_at: DateTime.t(),
post: PhoenixKitPosts.Post.t() | Ecto.Association.NotLoaded.t(),
user: PhoenixKit.Users.Auth.User.t() | Ecto.Association.NotLoaded.t() | nil,
inserted_at: DateTime.t() | nil,
updated_at: DateTime.t() | nil
}
schema "phoenix_kit_post_views" do
field(:ip_address, :string)
field(:user_agent_hash, :string)
field(:session_id, :string)
field(:viewed_at, :utc_datetime)
belongs_to(:post, PhoenixKitPosts.Post,
foreign_key: :post_uuid,
references: :uuid,
type: UUIDv7
)
belongs_to(:user, PhoenixKit.Users.Auth.User,
foreign_key: :user_uuid,
references: :uuid,
type: UUIDv7
)
timestamps(type: :utc_datetime)
end
@doc """
Changeset for creating a post view record.
## Required Fields
- `post_uuid` - Reference to post
- `viewed_at` - Timestamp of view
## Validation Rules
- viewed_at must not be in the future
"""
def changeset(view, attrs) do
view
|> cast(attrs, [
:post_uuid,
:user_uuid,
:ip_address,
:user_agent_hash,
:session_id,
:viewed_at
])
|> validate_required([:post_uuid, :viewed_at])
|> validate_viewed_at_not_future()
|> foreign_key_constraint(:post_uuid)
|> foreign_key_constraint(:user_uuid)
end
@doc """
Hash an IP address for privacy.
"""
def hash_ip(ip_address) when is_binary(ip_address) do
salt = Application.get_env(:phoenix_kit_posts, :hash_salt, "phoenix_kit_posts_default_salt")
:crypto.hash(:sha256, salt <> ip_address) |> Base.encode16(case: :lower)
end
@doc """
Hash a user agent for privacy.
"""
def hash_user_agent(user_agent) when is_binary(user_agent) do
salt = Application.get_env(:phoenix_kit_posts, :hash_salt, "phoenix_kit_posts_default_salt")
:crypto.hash(:sha256, salt <> user_agent) |> Base.encode16(case: :lower)
end
# Private Functions
defp validate_viewed_at_not_future(changeset) do
viewed_at = get_field(changeset, :viewed_at)
if viewed_at && DateTime.compare(viewed_at, UtilsDate.utc_now()) == :gt do
add_error(changeset, :viewed_at, "cannot be in the future")
else
changeset
end
end
end