Current section
Files
Jump to
Current section
Files
lib/ttlock_client/oauth.ex
defmodule TTlockClient.OAuth do
@moduledoc """
TTLock OAuth2 client for authentication and token management.
This module handles:
- Getting access tokens using Resource Owner Password Credentials
- Refreshing expired access tokens
- Password hashing (MD5) for TTLock API requirements
## Usage
# Get access token
{:ok, response} = TTlockClient.OAuth.get_access_token(
client_id: "your_client_id",
client_secret: "your_client_secret",
username: "+8618966498228",
password: "your_password"
)
# Refresh token
{:ok, response} = TTlockClient.OAuth.refresh_token(
client_id: "your_client_id",
client_secret: "your_client_secret",
refresh_token: "existing_refresh_token"
)
"""
@base_url "https://euapi.ttlock.com"
@token_endpoint "/oauth2/token"
@type oauth_config :: [
client_id: String.t(),
client_secret: String.t(),
username: String.t(),
password: String.t()
]
@type refresh_config :: [
client_id: String.t(),
client_secret: String.t(),
refresh_token: String.t()
]
@type token_response :: %{
access_token: String.t(),
uid: integer(),
refresh_token: String.t(),
expires_in: integer()
}
@type refresh_response :: %{
access_token: String.t(),
refresh_token: String.t(),
expires_in: integer()
}
@doc """
Get an access token using TTLock app account credentials.
## Parameters
- `client_id`: Application clientId from TTLock Create Application page
- `client_secret`: Application clientSecret from TTLock Create Application page
- `username`: TTLock app account username (or prefixed username from User Register API)
- `password`: Plain text password (will be MD5 hashed automatically)
## Returns
- `{:ok, token_response}` on success
- `{:error, reason}` on failure
## Example
{:ok, %{
"access_token" => "39caac89b0b51c980aa61ad4264b693b",
"uid" => 2340,
"refresh_token" => "1bd2a21a7df889630f444364813738d7",
"expires_in" => 7776000
}} = TTlockClient.OAuth.get_access_token(
client_id: "your_client_id",
client_secret: "your_client_secret",
username: "+8618966498228",
password: "your_password"
)
"""
@spec get_access_token(oauth_config()) :: {:ok, token_response()} | {:error, term()}
def get_access_token(opts \\ []) do
client_id = get_config_value(opts, :client_id)
client_secret = get_config_value(opts, :client_secret)
username = get_config_value(opts, :username)
password = get_config_value(opts, :password)
hashed_password = hash_password(password)
params = [
{"clientId", client_id},
{"clientSecret", client_secret},
{"username", username},
{"password", hashed_password}
]
make_token_request(params)
end
@doc """
Refresh an expired access token using a refresh token.
## Parameters
- `client_id`: Application clientId
- `client_secret`: Application clientSecret
- `refresh_token`: Refresh token obtained from previous get_access_token call
## Returns
- `{:ok, refresh_response}` on success
- `{:error, reason}` on failure
## Example
{:ok, %{
"access_token" => "39caac89b0b51c980aa61ad4264b693b",
"refresh_token" => "1bd2a21a7df889630f444364813738d7",
"expires_in" => 7776000
}} = TTlockClient.OAuth.refresh_token(
client_id: "your_client_id",
client_secret: "your_client_secret",
refresh_token: "existing_refresh_token"
)
"""
@spec refresh_token(refresh_config()) :: {:ok, refresh_response()} | {:error, term()}
def refresh_token(opts \\ []) do
client_id = get_config_value(opts, :client_id)
client_secret = get_config_value(opts, :client_secret)
refresh_token = Keyword.fetch!(opts, :refresh_token)
params = [
{"clientId", client_id},
{"clientSecret", client_secret},
{"grant_type", "refresh_token"},
{"refresh_token", refresh_token}
]
make_token_request(params)
end
@doc """
Hash a password using MD5 as required by TTLock API.
Returns a 32-character lowercase MD5 hash.
## Example
iex> TTlockClient.OAuth.hash_password("123456")
"e10adc3949ba59abbe56e057f20f883e"
"""
@spec hash_password(String.t()) :: String.t()
def hash_password(password) when is_binary(password) do
:crypto.hash(:md5, password)
|> Base.encode16(case: :lower)
end
# Private functions
defp get_config_value(opts, key) do
case Keyword.get(opts, key) do
nil ->
case Application.get_env(:ex_ttlock, key) do
nil -> raise ArgumentError, "#{key} is required either as option or in config"
value -> value
end
value -> value
end
end
defp make_token_request(params) do
url = @base_url <> @token_endpoint
body = URI.encode_query(params)
headers = [{"Content-Type", "application/x-www-form-urlencoded"}]
case HTTPoison.post(url, body, headers) do
{:ok, %HTTPoison.Response{status_code: 200, body: response_body}} ->
case Jason.decode(response_body) do
{:ok, data} -> {:ok, data}
{:error, _} = error -> error
end
{:ok, %HTTPoison.Response{status_code: status_code, body: response_body}} ->
case Jason.decode(response_body) do
{:ok, error_data} -> {:error, {status_code, error_data}}
{:error, _} -> {:error, {status_code, response_body}}
end
{:error, %HTTPoison.Error{} = error} ->
{:error, error}
end
end
end