defmodule Carve.HashIds do @moduledoc """ Carve.HashIds provides functionality for encoding and decoding IDs using HashIds. This module is responsible for: - Configuring the HashIds settings - Encoding integer IDs into hashed strings - Decoding hashed strings back into integer IDs - Handling entity-specific salting for added security It uses the Hashids library internally and provides a convenient API for Carve users. """ @alphabet "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_" @default_min_length 4 @default_salt "1207:Rumi" @doc """ Configures the HashIds settings. This function sets up the salt and minimum length for HashIds encoding. It stores these settings in persistent term storage for efficient access. ## Parameters - `opts`: A keyword list of configuration options. - `:salt` - The salt used for hashing (default: #{@default_salt}) - `:min_length` - The minimum length of generated hashes (default: #{@default_min_length}) ## Example iex> Carve.HashIds.configure(salt: "my_custom_salt", min_length: 8) :ok """ def configure(opts) do salt = Keyword.get(opts, :salt, @default_salt) min_length = Keyword.get(opts, :min_length, @default_min_length) :persistent_term.put(:carve_hash_ids_salt, salt) :persistent_term.put(:carve_hash_ids_min_length, min_length) end @doc """ Encodes an integer ID for a given type into a hashed string. ## Parameters - `type`: An atom representing the entity type (e.g., :user, :post) - `id`: The integer ID to be encoded ## Returns A string representing the encoded ID. ## Example iex> Carve.HashIds.encode(:user, 123) "Xk9Lp2Rr4m" """ def encode(type, id) when is_atom(type) and is_integer(id) do hashids = provider() entity_salt = entity_salt(type) Hashids.encode(hashids, [id, entity_salt]) end @doc """ Decodes a hashed string back into an integer ID for a given type. ## Parameters - `type`: An atom representing the entity type (e.g., :user, :post) - `hash`: The hashed string to be decoded ## Returns `{:ok, id}` if decoding is successful, where `id` is the original integer ID. `{:error, :invalid_entity_type}` if the decoded salt doesn't match the given type. `{:error, reason}` for other decoding errors. ## Example iex> Carve.HashIds.decode(:user, "Xk9Lp2Rr4m") {:ok, 123} """ def decode(type, hash) when is_atom(type) and is_binary(hash) do hashids = provider() entity_salt = entity_salt(type) case Hashids.decode(hashids, hash) do {:ok, [id, ^entity_salt]} -> {:ok, id} {:ok, _} -> {:error, :invalid_entity_type} {:error, _} = error -> error end end @doc """ Decodes a hashed string without specifying the entity type. This function attempts to decode the hash without verifying the entity type. ## Parameters - `hash`: The hashed string to be decoded ## Returns `{:ok, id}` if decoding is successful, where `id` is the original integer ID. `{:error, reason}` for decoding errors. ## Example iex> Carve.HashIds.decode("Xk9Lp2Rr4m") {:ok, 123} """ def decode(hash) when is_binary(hash) do hashids = provider() case Hashids.decode(hashids, hash) do {:ok, [id | _]} -> {:ok, id} {:error, _} = error -> error end end defp entity_salt(type) do type |> Atom.to_string() |> :erlang.phash2() end defp provider do salt = :persistent_term.get(:carve_hash_ids_salt) min_length = :persistent_term.get(:carve_hash_ids_min_length) Hashids.new(salt: salt, min_len: min_length, alphabet: @alphabet) end end