├── .formatter.exs ├── .gitignore ├── .tool-versions ├── .travis.yml ├── LICENSE ├── README.md ├── config ├── config.exs ├── dev.exs └── test.exs ├── lib ├── auth.ex ├── auth │ └── action_code_settings │ │ └── config.ex ├── dynamic_link.ex ├── errors.ex ├── firebase_admin_ex.ex ├── messaging.ex ├── messaging │ ├── android_message │ │ ├── config.ex │ │ └── notification.ex │ ├── apns_message │ │ ├── alert.ex │ │ ├── aps.ex │ │ ├── config.ex │ │ └── payload.ex │ ├── message.ex │ └── web_message │ │ ├── config.ex │ │ └── notification.ex ├── request.ex └── response.ex ├── mix.exs ├── mix.lock └── test ├── firebase_admin_ex_test.exs ├── messaging_test.exs ├── support └── request_mock.ex └── test_helper.exs /.formatter.exs: -------------------------------------------------------------------------------- 1 | [ 2 | inputs: [ 3 | "mix.exs", "{config,lib,test}/**/*.{ex,exs}" 4 | ], 5 | 6 | locals_without_parens: [ 7 | # Formatter tests 8 | assert_format: 2, 9 | assert_format: 3, 10 | assert_same: 1, 11 | assert_same: 2, 12 | 13 | # Errors tests 14 | assert_eval_raise: 3 15 | ] 16 | ] 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Mac 2 | .DS_Store 3 | .env 4 | 5 | # App artifacts 6 | /_build 7 | /db 8 | /deps 9 | /*.ez 10 | /log 11 | /cover 12 | doc/ 13 | 14 | # Generated on crash by the VM 15 | erl_crash.dump 16 | *.ez 17 | *.beam 18 | 19 | # Files matching config/*.secret.exs pattern contain sensitive 20 | # data and you should not commit them into version control. 21 | # 22 | # Alternatively, you may comment the line below and commit the 23 | # secrets files as long as you replace their contents by environment 24 | # variables. 25 | /config/*.secret.exs 26 | *.swp 27 | *.swo 28 | -------------------------------------------------------------------------------- /.tool-versions: -------------------------------------------------------------------------------- 1 | elixir 1.6.6 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: elixir 2 | elixir: '1.6.2' 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Scripbox 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Firebase Admin Elixir SDK 2 | 3 | [![Build Status](https://travis-ci.org/scripbox/firebase-admin-ex.svg?branch=master)](https://travis-ci.org/scripbox/firebase-admin-ex) 4 | 5 | ## Overview 6 | 7 | The Firebase Admin Elixir SDK enables access to Firebase services from privileged environments 8 | (such as servers or cloud) in Elixir. 9 | 10 | For more information, visit the 11 | [Firebase Admin SDK setup guide](https://firebase.google.com/docs/admin/setup/). 12 | 13 | ## Installation 14 | 15 | * Add `firebase_admin_ex` to your list of dependencies in `mix.exs`: 16 | 17 | ```ex 18 | defmodule YourApplication.Mixfile do 19 | use Mix.Project 20 | 21 | # Run "mix help deps" to learn about dependencies. 22 | defp deps do 23 | [ 24 | {:firebase_admin_ex, "~> 0.1.0"} 25 | ] 26 | end 27 | end 28 | ``` 29 | 30 | Next, run `mix deps.get` to pull down the dependencies: 31 | 32 | ```sh 33 | $ mix deps.get 34 | ``` 35 | 36 | Now you can make an API call by obtaining an access token and using the 37 | generated modules. 38 | 39 | ### Obtaining an Access Token 40 | Authentication is typically done through [Application Default Credentials][adc] 41 | which means you do not have to change the code to authenticate as long as 42 | your environment has credentials. 43 | 44 | Start by creating a [Service Account key file][service_account_key_file]. 45 | This file can be used to authenticate to Google Cloud Platform services from any environment. 46 | To use the file, set the `GOOGLE_APPLICATION_CREDENTIALS` environment variable to 47 | the path to the key file. Alternatively you may configure goth (the 48 | the authentication ssyas described at 49 | https://github.com/peburrows/goth#installation 50 | 51 | For example: 52 | 53 | ```sh 54 | $ export GOOGLE_APPLICATION_CREDENTIALS=/path/to/service_account.json 55 | ``` 56 | 57 | If you are deploying to App Engine, Compute Engine, or Container Engine, your 58 | credentials will be available by default. 59 | 60 | ### Usage 61 | 62 | #### Messaging 63 | 64 | * Sending a `WebMessage` 65 | 66 | ```ex 67 | # Get your device registration token 68 | registration_token = "user-device-token" 69 | 70 | # Define message payload attributes 71 | message = FirebaseAdminEx.Messaging.Message.new(%{ 72 | data: %{}, 73 | token: registration_token, 74 | webpush: FirebaseAdminEx.Messaging.WebMessage.Config.new(%{ 75 | headers: %{}, 76 | data: %{}, 77 | title: "notification title", 78 | body: "notification body", 79 | icon: "https://icon.png" 80 | }) 81 | }) 82 | 83 | # Call the Firebase messaging V1 send API 84 | project_id = "YOUR-FIREBASE-PROJECT-ID" 85 | {:ok, response} = FirebaseAdminEx.Messaging.send(project_id, message) 86 | ``` 87 | 88 | * Sending a `AndroidMessage` 89 | 90 | ```ex 91 | # Get your device registration token 92 | registration_token = "user-device-token" 93 | 94 | # Define message payload attributes 95 | message = FirebaseAdminEx.Messaging.Message.new(%{ 96 | data: %{}, 97 | token: registration_token, 98 | android: FirebaseAdminEx.Messaging.AndroidMessage.Config.new(%{ 99 | headers: %{}, 100 | data: %{}, 101 | title: "notification title", 102 | body: "notification body", 103 | icon: "https://icon.png" 104 | }) 105 | }) 106 | 107 | # Call the Firebase messaging V1 send API 108 | project_id = "YOUR-FIREBASE-PROJECT-ID" 109 | {:ok, response} = FirebaseAdminEx.Messaging.send(project_id, message) 110 | ``` 111 | 112 | * Sending a `APNSMessage` 113 | 114 | ```ex 115 | # Get your device registration token 116 | registration_token = "user-device-token" 117 | 118 | # Define message payload attributes 119 | message = FirebaseAdminEx.Messaging.Message.new(%{ 120 | data: %{}, 121 | token: registration_token, 122 | apns: FirebaseAdminEx.Messaging.APNSMessage.Config.new(%{ 123 | headers: %{}, 124 | payload: %{ 125 | aps: %{ 126 | alert: %{ 127 | title: "Message Title", 128 | body: "Message Body" 129 | }, 130 | sound: "default", 131 | "content-available": 1 132 | }, 133 | custom_data: %{} 134 | } 135 | }) 136 | }) 137 | 138 | # Call the Firebase messaging V1 send API 139 | project_id = "YOUR-FIREBASE-PROJECT-ID" 140 | {:ok, response} = FirebaseAdminEx.Messaging.send(project_id, message) 141 | ``` 142 | 143 | #### Authentication Management 144 | 145 | The `FirebaseAdminEx.Auth` module allows for some limited management of the 146 | Firebase Autentication system. It currently supports getting, deleting and creating users. 147 | 148 | * Getting a user by `uid`: 149 | 150 | ```ex 151 | iex(1)> FirebaseAdminEx.Auth.get_user("hYQIfs35Rfa4UMeDaf8lhcmUeTE2") 152 | {:ok, 153 | "{\n \"kind\": \"identitytoolkit#GetAccountInfoResponse\",\n \"users\": [\n {\n \"localId\": \"hYQIfs35Rfa4UMeDaf8lhcmUeTE2\",\n \"providerUserInfo\": [\n {\n \"providerId\": \"phone\",\n \"rawId\": \"+61400000111\",\n \"phoneNumber\": \"+61400000111\"\n }\n ],\n \"lastLoginAt\": \"1543976568000\",\n \"createdAt\": \"1543976568000\",\n \"phoneNumber\": \"+61400000111\"\n }\n ]\n}\n"} 154 | ``` 155 | 156 | * Getting a user by phone number 157 | 158 | ```ex 159 | iex(1)> FirebaseAdminEx.Auth.get_user_by_phone_number("+61400000111") 160 | {:ok, 161 | "{\n \"kind\": \"identitytoolkit#GetAccountInfoResponse\",\n \"users\": [\n {\n \"localId\": \"hYQIfs35Rfa4UMeDaf8lhcmUeTE2\",\n \"providerUserInfo\": [\n {\n \"providerId\": \"phone\",\n \"rawId\": \"+61400000111\",\n \"phoneNumber\": \"+61400000111\"\n }\n ],\n \"lastLoginAt\": \"1543976568000\",\n \"createdAt\": \"1543976568000\",\n \"phoneNumber\": \"+61400000111\"\n }\n ]\n}\n"} 162 | ``` 163 | 164 | * Getting a user by email address 165 | 166 | ```ex 167 | iex(1)> FirebaseAdminEx.Auth.get_user_by_email("user@example.com") 168 | {:ok, 169 | "{\n \"kind\": \"identitytoolkit#GetAccountInfoResponse\",\n \"users\": [\n {\n \"localId\": \"hYQIfs35Rfa4UMeDaf8lhcmUeTE2\",\n \"providerUserInfo\": [\n {\n \"providerId\": \"phone\",\n \"rawId\": \"+61400000111\",\n \"phoneNumber\": \"+61400000111\"\n \"email\": \"user@example.com\"\n }\n ],\n \"lastLoginAt\": \"1543976568000\",\n \"createdAt\": \"1543976568000\",\n \"phoneNumber\": \"+61400000111\"\n }\n ]\n}\n"} 170 | ``` 171 | 172 | * Deleting a user 173 | 174 | ```ex 175 | iex(4)> FirebaseAdminEx.Auth.delete_user("hYQIfs35Rfa4UMeDaf8lhcmUeTE2") 176 | {:ok, "{\n \"kind\": \"identitytoolkit#DeleteAccountResponse\"\n}\n"} 177 | ``` 178 | 179 | * Creating a user 180 | 181 | ```ex 182 | iex(4)> FirebaseAdminEx.Auth.create_email_password_user(%{"email" => "user@email.com", "password" => "hYQIfs35Rfa4UMeDaf8lhcmUeTE2"}) 183 | {:ok, 184 | "{\n \"kind\": \"identitytoolkit#SignupNewUserResponse\",\n \"email\": \"user@email.com\",\n \"localId\": \"s5dggHJyr3fgdgJkLe234G6h6y\"\n}\n"} 185 | ``` 186 | 187 | * Generating the email action link for sign-in flows 188 | 189 | ```ex 190 | # Define ActionCodeSettings 191 | action_code_settings = 192 | FirebaseAdminEx.Auth.ActionCodeSettings.new( 193 | %{ 194 | requestType: "EMAIL_SIGNIN", 195 | email: "user@email.com", 196 | returnOobLink: true, 197 | continueUrl: "www.test.com/sign-in", 198 | canHandleCodeInApp: false, 199 | dynamicLinkDomain: "", 200 | androidPackageName: "", 201 | androidMinimumVersion: "", 202 | androidInstallApp: false, 203 | iOSBundleId: "" 204 | } 205 | ) 206 | client_email = "YOUR-FIREBASE-CLIENT-EMAIL" 207 | project_id = "YOUR-FIREBASE-PROJECT-ID" 208 | iex(4)> FirebaseAdminEx.Auth.generate_sign_in_with_email_link(action_code_settings, client_email, project_id) 209 | {:ok, 210 | "{\n \"kind\": \"identitytoolkit#GetOobConfirmationCodeResponse\",\n \"email\": \"user@email.com\",\n \"oobLink\": \"https://YOUR-FIREBASE-CLIENT.firebaseapp.com/__/auth/action?mode=signIn&oobCode=xcdwelFRvfbtghHjswvw2f3g46hh6j8&apiKey=Fgae35h6j78_vbsddgs34th6h6hhekj97gfj&lang=en&continueUrl=www.test.com/sign-in\"\n}\n"} 211 | ``` 212 | 213 | ## Firebase Documentation 214 | 215 | * [Setup Guide](https://firebase.google.com/docs/admin/setup/) 216 | * [Authentication Guide](https://firebase.google.com/docs/auth/admin/) 217 | * [Cloud Messaging Guide](https://firebase.google.com/docs/cloud-messaging/admin/) 218 | 219 | ## License and Terms 220 | 221 | Your use of Firebase is governed by the 222 | [Terms of Service for Firebase Services](https://firebase.google.com/terms/). 223 | 224 | ## Disclaimer 225 | 226 | This is not an officially supported Google product. 227 | 228 | [adc]: https://cloud.google.com/docs/authentication#getting_credentials_for_server-centric_flow 229 | [service_account_key_file]: https://developers.google.com/identity/protocols/OAuth2ServiceAccount#creatinganaccount 230 | [hex_pm]: https://hex.pm/users/google-cloud 231 | [goth]: https://hex.pm/packages/goth 232 | -------------------------------------------------------------------------------- /config/config.exs: -------------------------------------------------------------------------------- 1 | # This file is responsible for configuring your application 2 | # and its dependencies with the aid of the Mix.Config module. 3 | use Mix.Config 4 | 5 | # This configuration is loaded before any dependency and is restricted 6 | # to this project. If another project depends on this project, this 7 | # file won't be loaded nor affect the parent project. For this reason, 8 | # if you want to provide default values for your application for 9 | # 3rd-party users, it should be done in your "mix.exs" file. 10 | 11 | # You can configure your application as: 12 | # 13 | # config :firebase_admin_ex, key: :value 14 | # 15 | # and access this configuration in your application as: 16 | # 17 | # Application.get_env(:firebase_admin_ex, :key) 18 | # 19 | # You can also configure a 3rd-party app: 20 | # 21 | # config :logger, level: :info 22 | # 23 | 24 | # It is also possible to import configuration files, relative to this 25 | # directory. For example, you can emulate configuration per environment 26 | # by uncommenting the line below and defining dev.exs, test.exs and such. 27 | # Configuration from the imported file will override the ones defined 28 | # here (which is why it is important to import them last). 29 | # 30 | 31 | config :firebase_admin_ex, 32 | default_options: [ 33 | timeout: 5000, 34 | recv_timeout: 2000 35 | ] 36 | 37 | import_config "#{Mix.env()}.exs" 38 | -------------------------------------------------------------------------------- /config/dev.exs: -------------------------------------------------------------------------------- 1 | use Mix.Config 2 | 3 | config :goth, disabled: true 4 | -------------------------------------------------------------------------------- /config/test.exs: -------------------------------------------------------------------------------- 1 | use Mix.Config 2 | 3 | config :goth, disabled: true 4 | -------------------------------------------------------------------------------- /lib/auth.ex: -------------------------------------------------------------------------------- 1 | defmodule FirebaseAdminEx.Auth do 2 | alias FirebaseAdminEx.{Request, Response, Errors} 3 | alias FirebaseAdminEx.Auth.ActionCodeSettings 4 | 5 | @auth_endpoint "https://www.googleapis.com/identitytoolkit/v3/relyingparty/" 6 | @auth_endpoint_account "https://identitytoolkit.googleapis.com/v1/projects/" 7 | @auth_scope "https://www.googleapis.com/auth/cloud-platform" 8 | 9 | @doc """ 10 | Get a user's info by UID 11 | """ 12 | @spec get_user(String.t(), String.t() | nil) :: tuple() 13 | def get_user(uid, client_email \\ nil), do: get_user(:localId, uid, client_email) 14 | 15 | @doc """ 16 | Get a user's info by phone number 17 | """ 18 | @spec get_user_by_phone_number(String.t(), String.t() | nil) :: tuple() 19 | def get_user_by_phone_number(phone_number, client_email \\ nil), 20 | do: get_user(:phone_number, phone_number, client_email) 21 | 22 | @doc """ 23 | Get a user's info by email 24 | """ 25 | @spec get_user_by_email(String.t(), String.t() | nil) :: tuple() 26 | def get_user_by_email(email, client_email \\ nil), 27 | do: get_user(:email, email, client_email) 28 | 29 | defp get_user(key, value, client_email), 30 | do: do_request("getAccountInfo", %{key => value}, client_email) 31 | 32 | @doc """ 33 | Delete an existing user by UID 34 | """ 35 | @spec delete_user(String.t(), String.t() | nil) :: tuple() 36 | def delete_user(uid, client_email \\ nil), 37 | do: do_request("deleteAccount", %{localId: uid}, client_email) 38 | 39 | # TODO: Add other commands: 40 | # list_users 41 | # create_user 42 | # update_user 43 | # import_users 44 | 45 | @doc """ 46 | Create an email/password user 47 | """ 48 | @spec create_email_password_user(map, String.t() | nil) :: tuple() 49 | def create_email_password_user( 50 | %{"email" => email, "password" => password}, 51 | client_email \\ nil 52 | ), 53 | do: 54 | do_request( 55 | "signupNewUser", 56 | %{:email => email, :password => password, :returnSecureToken => true}, 57 | client_email 58 | ) 59 | 60 | @doc """ 61 | Generates the email action link for sign-in flows, using the action code settings provided 62 | """ 63 | @spec generate_sign_in_with_email_link(ActionCodeSettings.t(), String.t(), String.t()) :: tuple() 64 | def generate_sign_in_with_email_link(action_code_settings, client_email, project_id) do 65 | with {:ok, action_code_settings} <- ActionCodeSettings.validate(action_code_settings) do 66 | do_request("accounts:sendOobCode", action_code_settings, client_email, project_id) 67 | end 68 | end 69 | 70 | defp do_request(url_suffix, payload, client_email, project_id) do 71 | with {:ok, response} <- 72 | Request.request( 73 | :post, 74 | "#{@auth_endpoint_account}#{project_id}/#{url_suffix}", 75 | payload, 76 | auth_header(client_email) 77 | ), 78 | {:ok, body} <- Response.parse(response) do 79 | {:ok, body} 80 | else 81 | {:error, error} -> raise Errors.ApiError, Kernel.inspect(error) 82 | end 83 | end 84 | 85 | defp do_request(url_suffix, payload, client_email) do 86 | with {:ok, response} <- 87 | Request.request( 88 | :post, 89 | @auth_endpoint <> url_suffix, 90 | payload, 91 | auth_header(client_email) 92 | ), 93 | {:ok, body} <- Response.parse(response) do 94 | {:ok, body} 95 | else 96 | {:error, error} -> raise Errors.ApiError, Kernel.inspect(error) 97 | end 98 | end 99 | 100 | defp auth_header(nil) do 101 | {:ok, token} = Goth.Token.for_scope(@auth_scope) 102 | 103 | do_auth_header(token.token) 104 | end 105 | 106 | defp auth_header(client_email) do 107 | {:ok, token} = Goth.Token.for_scope({client_email, @auth_scope}) 108 | 109 | do_auth_header(token.token) 110 | end 111 | 112 | defp do_auth_header(token) do 113 | %{"Authorization" => "Bearer #{token}"} 114 | end 115 | end 116 | -------------------------------------------------------------------------------- /lib/auth/action_code_settings/config.ex: -------------------------------------------------------------------------------- 1 | defmodule FirebaseAdminEx.Auth.ActionCodeSettings do 2 | @moduledoc """ 3 | This module is responsible for representing the 4 | attributes of ActionCodeSettings. 5 | """ 6 | 7 | @keys [ 8 | requestType: "", 9 | email: "", 10 | returnOobLink: true, 11 | continueUrl: "", 12 | canHandleCodeInApp: false, 13 | dynamicLinkDomain: "", 14 | androidPackageName: "", 15 | androidMinimumVersion: "", 16 | androidInstallApp: false, 17 | iOSBundleId: "" 18 | ] 19 | 20 | @type t :: %__MODULE__{ 21 | requestType: String.t(), 22 | email: String.t(), 23 | returnOobLink: boolean(), 24 | continueUrl: String.t(), 25 | canHandleCodeInApp: boolean(), 26 | dynamicLinkDomain: String.t(), 27 | androidPackageName: String.t(), 28 | androidMinimumVersion: String.t(), 29 | androidInstallApp: boolean(), 30 | iOSBundleId: String.t() 31 | } 32 | 33 | @derive Jason.Encoder 34 | defstruct @keys 35 | 36 | # Public API 37 | def new(attributes \\ %{}) do 38 | %__MODULE__{ 39 | requestType: Map.get(attributes, :requestType), 40 | email: Map.get(attributes, :email), 41 | returnOobLink: Map.get(attributes, :returnOobLink), 42 | continueUrl: Map.get(attributes, :continueUrl), 43 | canHandleCodeInApp: Map.get(attributes, :canHandleCodeInApp), 44 | dynamicLinkDomain: Map.get(attributes, :dynamicLinkDomain), 45 | androidPackageName: Map.get(attributes, :androidPackageName), 46 | androidMinimumVersion: Map.get(attributes, :androidMinimumVersion), 47 | androidInstallApp: Map.get(attributes, :androidInstallApp), 48 | iOSBundleId: Map.get(attributes, :iOSBundleId) 49 | } 50 | end 51 | 52 | def validate(%__MODULE__{email: nil}), 53 | do: {:error, "[ActionCodeSettings] email is missing"} 54 | 55 | def validate(%__MODULE__{} = action_code_settings), 56 | do: {:ok, action_code_settings} 57 | 58 | def validate(_), do: {:error, "[ActionCodeSettings] Invalid payload"} 59 | end 60 | -------------------------------------------------------------------------------- /lib/dynamic_link.ex: -------------------------------------------------------------------------------- 1 | defmodule FirebaseAdminEx.DynamicLink do 2 | alias FirebaseAdminEx.{Request, Response, Errors} 3 | 4 | @short_link_endpoint "https://firebasedynamiclinks.googleapis.com/v1/shortLinks" 5 | @auth_scope "https://www.googleapis.com/auth/firebase" 6 | 7 | @type suffix_type :: :short | :unguessable 8 | 9 | @doc """ 10 | Generate a short dynamic link based on the supplied parameters or long link. 11 | See https://firebase.google.com/docs/reference/dynamic-links/link-shortener 12 | for the full list of supported parameters. 13 | 14 | Examples: 15 | 16 | iex(1)> FirebaseAdminEx.DynamicLink.get_short_link("https://example.page.link/?link=https://example.com/someresource&apn=com.example.android&amv=3&ibi=com.example.ios&isi=1234567&ius=exampleapp", :short) 17 | {:ok, 18 | "{\n \"shortLink\": \"https://example.page.link/M5Jz\",\n \"previewLink\": \"https://example.page.link/M5Jz?d=1\"\n}\n"} 19 | 20 | 21 | iex(2)> p = %{"domainUriPrefix" => "https://example.page.link", "link" => "https://example.com/abcdef", "iosInfo" => %{"iosBundleId" => "com.exampleco.example", "iosAppStoreId" => "123456789"}} 22 | %{ 23 | "domainUriPrefix" => "https://example.page.link", 24 | "iosInfo" => %{ 25 | "iosAppStoreId" => "123456789", 26 | "iosBundleId" => "com.exampleco.example" 27 | }, 28 | "link" => "https://example.com/abcdef" 29 | } 30 | iex(3)> FirebaseAdminEx.DynamicLink.get_short_link(p, :unguessable) 31 | {:ok, 32 | "{\n \"shortLink\": \"https://example.page.link/uH877tctFJ7mctBF6\",\n \"warning\": [\n {\n \"warningCode\": \"UNRECOGNIZED_PARAM\",\n \"warningMessage\": \"Android app 'com.example' lacks SHA256. AppLinks is not enabled for the app. [https://firebase.google.com/docs/dynamic-links/debug#android-sha256-absent]\"\n }\n ],\n \"previewLink\": \"https://example.page.link/uH877tctFJ7mctBF6?d=1\"\n}\n"} 33 | """ 34 | 35 | @spec short_link(map() | String.t(), suffix_type, String.t() | nil) :: tuple() 36 | def short_link(params, type \\ :unguessable, client_email \\ nil) do 37 | payload = build_payload(params, type) 38 | with {:ok, response} <- 39 | Request.request( 40 | :post, 41 | @short_link_endpoint, 42 | payload, 43 | auth_header(client_email) 44 | ), 45 | {:ok, body} <- Response.parse(response), 46 | {:ok, result} <- Jason.decode(body) do 47 | {:ok, result} 48 | else 49 | {:error, error} -> raise Errors.ApiError, Kernel.inspect(error) 50 | end 51 | end 52 | 53 | defp build_payload(long_link, type) when is_binary(long_link) do 54 | %{ 55 | "longDynamicLink" => long_link, 56 | "suffix" => %{"option" => option(type)} 57 | } 58 | end 59 | 60 | defp build_payload(params, type) when is_map(params)do 61 | %{ 62 | "dynamicLinkInfo" => params, 63 | "suffix" => %{"option" => option(type)} 64 | } 65 | end 66 | 67 | defp option(:short), do: "SHORT" 68 | defp option(:unguessable), do: "UNGUESSABLE" 69 | 70 | defp auth_header(nil) do 71 | {:ok, token} = Goth.Token.for_scope(@auth_scope) 72 | 73 | do_auth_header(token.token) 74 | end 75 | 76 | defp auth_header(client_email) do 77 | {:ok, token} = Goth.Token.for_scope({client_email, @auth_scope}) 78 | 79 | do_auth_header(token.token) 80 | end 81 | 82 | defp do_auth_header(token) do 83 | %{"Authorization" => "Bearer #{token}"} 84 | end 85 | end 86 | -------------------------------------------------------------------------------- /lib/errors.ex: -------------------------------------------------------------------------------- 1 | defmodule FirebaseAdminEx.Errors.ApiError do 2 | defexception [:reason] 3 | 4 | def exception(reason), do: %__MODULE__{reason: reason} 5 | 6 | def message(%__MODULE__{reason: reason}), do: "FirebaseAdminEx::ApiError - #{reason}" 7 | end 8 | 9 | defmodule FirebaseAdminEx.Errors.ApiLimitExceeded do 10 | defexception [:reason] 11 | 12 | def exception(reason), do: %__MODULE__{reason: reason} 13 | 14 | def message(%__MODULE__{reason: reason}), do: "FirebaseAdminEx::ApiLimitExceeded - #{reason}" 15 | end 16 | -------------------------------------------------------------------------------- /lib/firebase_admin_ex.ex: -------------------------------------------------------------------------------- 1 | defmodule FirebaseAdminEx do 2 | @moduledoc """ 3 | Documentation for FirebaseAdminEx. 4 | """ 5 | use Application 6 | 7 | # See http://elixir-lang.org/docs/stable/elixir/Application.html 8 | # for more information on OTP Applications 9 | def start(_type, _args) do 10 | import Supervisor.Spec, warn: false 11 | 12 | children = [ 13 | # Define workers and child supervisors to be supervised 14 | # worker(FirebaseAdminEx.Worker, [arg1, arg2, arg3]), 15 | ] 16 | 17 | # See http://elixir-lang.org/docs/stable/elixir/Supervisor.html 18 | # for other strategies and supported options 19 | opts = [strategy: :one_for_one, name: FirebaseAdminEx.Supervisor] 20 | Supervisor.start_link(children, opts) 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /lib/messaging.ex: -------------------------------------------------------------------------------- 1 | defmodule FirebaseAdminEx.Messaging do 2 | alias FirebaseAdminEx.{Request, Response, Errors} 3 | alias FirebaseAdminEx.Messaging.Message 4 | 5 | @fcm_endpoint "https://fcm.googleapis.com/v1" 6 | @messaging_scope "https://www.googleapis.com/auth/firebase.messaging" 7 | 8 | # Public API 9 | 10 | @doc """ 11 | The send/2 function makes an API call to the 12 | firebase messaging `send` endpoint with the auth token 13 | and message attributes. 14 | """ 15 | @spec send(String.t(), struct()) :: tuple() 16 | def send(client_email, message) do 17 | {:ok, project_id} = Goth.Config.get(client_email, :project_id) 18 | {:ok, token} = Goth.Token.for_scope({client_email, @messaging_scope}) 19 | 20 | send(project_id, token.token, message) 21 | end 22 | 23 | @doc """ 24 | send/3 Is the same as send/2 except the user can supply their own 25 | authentication token 26 | """ 27 | @spec send(String.t(), String.t(), struct()) :: tuple() 28 | def send(project_id, oauth_token, %Message{} = message) do 29 | with {:ok, message} <- Message.validate(message), 30 | {:ok, response} <- 31 | Request.request( 32 | :post, 33 | send_url(project_id), 34 | %{message: message}, 35 | auth_header(oauth_token) 36 | ), 37 | {:ok, body} <- Response.parse(response) do 38 | {:ok, body} 39 | else 40 | {:error, error} -> 41 | raise Errors.ApiError, Kernel.inspect(error) 42 | end 43 | end 44 | 45 | # Private API 46 | defp send_url(project_id) do 47 | "#{@fcm_endpoint}/projects/#{project_id}/messages:send" 48 | end 49 | 50 | defp auth_header(oauth_token) do 51 | %{"Authorization" => "Bearer #{oauth_token}"} 52 | end 53 | end 54 | -------------------------------------------------------------------------------- /lib/messaging/android_message/config.ex: -------------------------------------------------------------------------------- 1 | defmodule FirebaseAdminEx.Messaging.AndroidMessage.Config do 2 | @moduledoc """ 3 | This module is responsible for representing the 4 | attributes of AndroidMessage.Config. 5 | """ 6 | 7 | alias FirebaseAdminEx.Messaging.AndroidMessage.Notification 8 | 9 | @keys [ 10 | collapse_key: "", 11 | priority: "normal", 12 | ttl: "", 13 | restricted_package_name: "", 14 | data: %{}, 15 | notification: %Notification{ 16 | title: "", 17 | body: "" 18 | } 19 | ] 20 | 21 | @type t :: %__MODULE__{ 22 | collapse_key: String.t(), 23 | priority: String.t(), 24 | ttl: String.t(), 25 | restricted_package_name: String.t(), 26 | data: map(), 27 | notification: struct() 28 | } 29 | 30 | @derive Jason.Encoder 31 | defstruct @keys 32 | 33 | # Public API 34 | def new(attributes \\ %{}) do 35 | %__MODULE__{ 36 | collapse_key: Map.get(attributes, :collapse_key), 37 | priority: Map.get(attributes, :priority), 38 | ttl: Map.get(attributes, :ttl), 39 | restricted_package_name: Map.get(attributes, :restricted_package_name), 40 | data: Map.get(attributes, :data, %{}), 41 | notification: Notification.new(attributes) 42 | } 43 | end 44 | 45 | def validate(%__MODULE__{data: _, notification: nil}), 46 | do: {:error, "[AndroidMessage.Config] notification is missing"} 47 | 48 | def validate(%__MODULE__{data: _, notification: notification} = message_config) do 49 | case Notification.validate(notification) do 50 | {:ok, _} -> 51 | {:ok, message_config} 52 | 53 | {:error, error_message} -> 54 | {:error, error_message} 55 | end 56 | end 57 | 58 | def validate(_), do: {:error, "[AndroidMessage.Config] Invalid payload"} 59 | end 60 | -------------------------------------------------------------------------------- /lib/messaging/android_message/notification.ex: -------------------------------------------------------------------------------- 1 | defmodule FirebaseAdminEx.Messaging.AndroidMessage.Notification do 2 | @moduledoc """ 3 | This module is responsible for representing the 4 | attributes of AndroidMessage.Notification. 5 | """ 6 | 7 | @keys [ 8 | title: "", 9 | body: "", 10 | icon: "", 11 | color: "", 12 | sound: "", 13 | tag: "", 14 | click_action: "", 15 | body_loc_key: "", 16 | body_loc_args: [], 17 | title_loc_key: "", 18 | title_loc_args: [] 19 | ] 20 | 21 | @type t :: %__MODULE__{ 22 | title: String.t(), 23 | body: String.t(), 24 | icon: String.t(), 25 | color: String.t(), 26 | sound: String.t(), 27 | tag: String.t(), 28 | click_action: String.t(), 29 | body_loc_key: String.t(), 30 | body_loc_args: List.t(), 31 | title_loc_key: String.t(), 32 | title_loc_args: List.t() 33 | } 34 | 35 | @derive Jason.Encoder 36 | defstruct @keys 37 | 38 | # Public API 39 | 40 | def new(attributes \\ %{}) do 41 | %__MODULE__{ 42 | title: Map.get(attributes, :title), 43 | body: Map.get(attributes, :body), 44 | icon: Map.get(attributes, :icon), 45 | color: Map.get(attributes, :color), 46 | sound: Map.get(attributes, :sound), 47 | tag: Map.get(attributes, :tag), 48 | click_action: Map.get(attributes, :click_action), 49 | body_loc_key: Map.get(attributes, :body_loc_key), 50 | body_loc_args: Map.get(attributes, :body_loc_args), 51 | title_loc_key: Map.get(attributes, :title_loc_key), 52 | title_loc_args: Map.get(attributes, :title_loc_args) 53 | } 54 | end 55 | 56 | def validate(%__MODULE__{title: nil, body: _}), 57 | do: {:error, "[AndroidMessage.Notification] title is missing"} 58 | 59 | def validate(%__MODULE__{title: _, body: nil}), 60 | do: {:error, "[AndroidMessage.Notification] body is missing"} 61 | 62 | def validate(%__MODULE__{title: _, body: _} = message), do: {:ok, message} 63 | def validate(_), do: {:error, "[AndroidMessage.Notification] Invalid payload"} 64 | end 65 | -------------------------------------------------------------------------------- /lib/messaging/apns_message/alert.ex: -------------------------------------------------------------------------------- 1 | defmodule FirebaseAdminEx.Messaging.APNSMessage.Alert do 2 | @moduledoc """ 3 | This module is responsible for representing the 4 | attributes of APNSMessage.Alert. 5 | """ 6 | 7 | @keys [ 8 | title: "", 9 | body: "", 10 | "loc-key": "", 11 | "loc-args": [], 12 | "title-loc-key": "", 13 | "title-loc-args": [], 14 | "action-loc-key": "", 15 | "launch-image": "" 16 | ] 17 | 18 | @type t :: %__MODULE__{ 19 | title: String.t(), 20 | body: String.t(), 21 | "loc-key": String.t(), 22 | "loc-args": List.t(), 23 | "title-loc-key": String.t(), 24 | "title-loc-args": List.t(), 25 | "action-loc-key": String.t(), 26 | "launch-image": String.t() 27 | } 28 | 29 | @derive Jason.Encoder 30 | defstruct @keys 31 | 32 | # Public API 33 | 34 | def new(attributes \\ %{}) do 35 | %__MODULE__{ 36 | title: Map.get(attributes, :title), 37 | body: Map.get(attributes, :body), 38 | "loc-key": Map.get(attributes, :"loc-key", ""), 39 | "loc-args": Map.get(attributes, :"loc-args", []), 40 | "title-loc-key": Map.get(attributes, :"title-loc-key", ""), 41 | "title-loc-args": Map.get(attributes, :"title-loc-args", []), 42 | "action-loc-key": Map.get(attributes, :"action-loc-key"), 43 | "launch-image": Map.get(attributes, :"launch-image", "") 44 | } 45 | end 46 | 47 | def validate(%__MODULE__{"title-loc-key": "", "title-loc-args": title_loc_args}) 48 | when length(title_loc_args) > 0 do 49 | {:error, "[APNSMessage.Alert] title-loc-key is required when specifying title-loc-args"} 50 | end 51 | 52 | def validate(%__MODULE__{"loc-key": "", "loc-args": loc_args}) 53 | when length(loc_args) > 0 do 54 | {:error, "[APNSMessage.Alert] loc-key is required when specifying loc-args"} 55 | end 56 | 57 | def validate(%__MODULE__{} = message), do: {:ok, message} 58 | def validate(_), do: {:error, "[APNSMessage.Alert] Invalid payload"} 59 | end 60 | -------------------------------------------------------------------------------- /lib/messaging/apns_message/aps.ex: -------------------------------------------------------------------------------- 1 | defmodule FirebaseAdminEx.Messaging.APNSMessage.Aps do 2 | @moduledoc """ 3 | This module is responsible for representing the 4 | attributes of APNSMessage.Aps. 5 | """ 6 | 7 | alias FirebaseAdminEx.Messaging.APNSMessage.Alert 8 | 9 | @keys [ 10 | alert_string: "", 11 | alert: %Alert{}, 12 | badge: 0, 13 | sound: "", 14 | category: "", 15 | "content-available": 0 16 | ] 17 | 18 | @type t :: %__MODULE__{ 19 | alert_string: String.t(), 20 | alert: struct(), 21 | badge: integer(), 22 | sound: String.t(), 23 | category: String.t(), 24 | "content-available": integer() 25 | } 26 | 27 | @derive Jason.Encoder 28 | defstruct @keys 29 | 30 | # Public API 31 | 32 | def new(attributes \\ %{}) do 33 | %__MODULE__{ 34 | alert_string: Map.get(attributes, :alert_string), 35 | alert: Alert.new(Map.get(attributes, :alert)), 36 | badge: Map.get(attributes, :badge, 0), 37 | sound: Map.get(attributes, :sound), 38 | category: Map.get(attributes, :category, ""), 39 | "content-available": Map.get(attributes, :"content-available") 40 | } 41 | end 42 | 43 | def validate(%__MODULE__{alert_string: nil, alert: nil} = message), do: {:ok, message} 44 | 45 | def validate(%__MODULE__{alert_string: nil, alert: alert} = message_config) do 46 | case Alert.validate(alert) do 47 | {:ok, _} -> 48 | {:ok, message_config} 49 | 50 | {:error, error_message} -> 51 | {:error, error_message} 52 | end 53 | end 54 | 55 | def validate(%__MODULE__{alert_string: _, alert: nil} = message), do: {:ok, message} 56 | 57 | def validate(%__MODULE__{alert_string: _, alert: _}), 58 | do: {:error, "[APNSMessage.Aps] Multiple alert specifications"} 59 | 60 | def validate(_), do: {:error, "[APNSMessage.Aps] Invalid payload"} 61 | end 62 | -------------------------------------------------------------------------------- /lib/messaging/apns_message/config.ex: -------------------------------------------------------------------------------- 1 | defmodule FirebaseAdminEx.Messaging.APNSMessage.Config do 2 | @moduledoc """ 3 | This module is responsible for representing the 4 | attributes of APNSMessage.Config. 5 | """ 6 | 7 | alias FirebaseAdminEx.Messaging.APNSMessage.Payload 8 | 9 | @keys [ 10 | headers: %{}, 11 | payload: %Payload{ 12 | aps: %{}, 13 | custom_data: %{} 14 | } 15 | ] 16 | 17 | @type t :: %__MODULE__{ 18 | headers: map(), 19 | payload: struct() 20 | } 21 | 22 | @derive Jason.Encoder 23 | defstruct @keys 24 | 25 | # Public API 26 | def new(attributes \\ %{}) do 27 | %__MODULE__{ 28 | headers: Map.get(attributes, :headers, %{}), 29 | payload: Payload.new(Map.get(attributes, :payload)) 30 | } 31 | end 32 | 33 | def validate(%__MODULE__{headers: _, payload: nil}), 34 | do: {:error, "[APNSMessage.Config] payload is missing"} 35 | 36 | def validate(%__MODULE__{headers: _, payload: payload} = message_config) do 37 | case Payload.validate(payload) do 38 | {:ok, _} -> 39 | {:ok, message_config} 40 | 41 | {:error, error_message} -> 42 | {:error, error_message} 43 | end 44 | end 45 | 46 | def validate(_), do: {:error, "[APNSMessage.Config] Invalid payload"} 47 | end 48 | -------------------------------------------------------------------------------- /lib/messaging/apns_message/payload.ex: -------------------------------------------------------------------------------- 1 | defmodule FirebaseAdminEx.Messaging.APNSMessage.Payload do 2 | @moduledoc """ 3 | This module is responsible for representing the 4 | attributes of APNSMessage.Payload. 5 | """ 6 | 7 | alias FirebaseAdminEx.Messaging.APNSMessage.Aps 8 | 9 | @keys [ 10 | aps: %{}, 11 | custom_data: %{} 12 | ] 13 | 14 | @type t :: %__MODULE__{ 15 | aps: map(), 16 | custom_data: map() 17 | } 18 | 19 | @derive Jason.Encoder 20 | defstruct @keys 21 | 22 | # Public API 23 | 24 | def new(attributes \\ %{}) do 25 | %__MODULE__{ 26 | aps: Aps.new(Map.get(attributes, :aps)), 27 | custom_data: Map.get(attributes, :custom_data) 28 | } 29 | end 30 | 31 | def validate(%__MODULE__{aps: nil} = message), do: {:ok, message} 32 | 33 | def validate(%__MODULE__{aps: aps} = message_config) do 34 | case Aps.validate(aps) do 35 | {:ok, _} -> 36 | {:ok, message_config} 37 | 38 | {:error, error_message} -> 39 | {:error, error_message} 40 | end 41 | end 42 | 43 | def validate(_), do: {:error, "[APNSMessage.Payload] Invalid payload"} 44 | end 45 | -------------------------------------------------------------------------------- /lib/messaging/message.ex: -------------------------------------------------------------------------------- 1 | defmodule FirebaseAdminEx.Messaging.Message do 2 | @moduledoc """ 3 | This module is responsible for representing the 4 | attributes of FirebaseAdminEx.Message. 5 | """ 6 | 7 | alias __MODULE__ 8 | alias FirebaseAdminEx.Messaging.WebMessage.Config, as: WebMessageConfig 9 | alias FirebaseAdminEx.Messaging.AndroidMessage.Config, as: AndroidMessageConfig 10 | alias FirebaseAdminEx.Messaging.APNSMessage.Config, as: APNSMessageConfig 11 | 12 | @keys [ 13 | data: %{}, 14 | notification: %{}, 15 | webpush: nil, 16 | android: nil, 17 | apns: nil, 18 | token: "" 19 | ] 20 | 21 | @type t :: %__MODULE__{ 22 | data: map(), 23 | notification: map(), 24 | webpush: struct(), 25 | android: struct(), 26 | apns: struct(), 27 | token: String.t() 28 | } 29 | 30 | @derive Jason.Encoder 31 | defstruct @keys 32 | 33 | # Public API 34 | def new(%{token: token, webpush: webpush} = attributes) do 35 | %Message{ 36 | data: Map.get(attributes, :data, %{}), 37 | notification: Map.get(attributes, :notification, %{}), 38 | webpush: webpush, 39 | token: token 40 | } 41 | end 42 | 43 | def new(%{token: token, android: android} = attributes) do 44 | %Message{ 45 | data: Map.get(attributes, :data, %{}), 46 | notification: Map.get(attributes, :notification, %{}), 47 | android: android, 48 | token: token 49 | } 50 | end 51 | 52 | def new(%{token: token, apns: apns} = attributes) do 53 | %Message{ 54 | data: Map.get(attributes, :data, %{}), 55 | notification: Map.get(attributes, :notification, %{}), 56 | apns: apns, 57 | token: token 58 | } 59 | end 60 | 61 | def new(%{token: token} = attributes) do 62 | %Message{ 63 | data: Map.get(attributes, :data, %{}), 64 | notification: Map.get(attributes, :notification, %{}), 65 | token: token 66 | } 67 | end 68 | 69 | def validate(%Message{data: _, token: nil}), do: {:error, "[Message] token is missing"} 70 | 71 | def validate(%Message{data: _, token: _, webpush: nil, android: nil, apns: nil} = message), 72 | do: {:ok, message} 73 | 74 | def validate(%Message{data: _, token: _, webpush: web_message_config} = message) 75 | when web_message_config != nil do 76 | case WebMessageConfig.validate(web_message_config) do 77 | {:ok, _} -> 78 | {:ok, message} 79 | 80 | {:error, error_message} -> 81 | {:error, error_message} 82 | end 83 | end 84 | 85 | def validate(%Message{data: _, token: _, android: android_message_config} = message) 86 | when android_message_config != nil do 87 | case AndroidMessageConfig.validate(android_message_config) do 88 | {:ok, _} -> 89 | {:ok, message} 90 | 91 | {:error, error_message} -> 92 | {:error, error_message} 93 | end 94 | end 95 | 96 | def validate(%Message{data: _, token: _, apns: apns_message_config} = message) 97 | when apns_message_config != nil do 98 | case APNSMessageConfig.validate(apns_message_config) do 99 | {:ok, _} -> 100 | {:ok, message} 101 | 102 | {:error, error_message} -> 103 | {:error, error_message} 104 | end 105 | end 106 | 107 | def validate(_), do: {:error, "[Message] Invalid payload"} 108 | end 109 | -------------------------------------------------------------------------------- /lib/messaging/web_message/config.ex: -------------------------------------------------------------------------------- 1 | defmodule FirebaseAdminEx.Messaging.WebMessage.Config do 2 | @moduledoc """ 3 | This module is responsible for representing the 4 | attributes of WebMessage.Config. 5 | """ 6 | 7 | alias FirebaseAdminEx.Messaging.WebMessage.Notification 8 | 9 | @keys [ 10 | headers: %{}, 11 | data: %{}, 12 | notification: %Notification{ 13 | title: "", 14 | body: "" 15 | } 16 | ] 17 | 18 | @type t :: %__MODULE__{ 19 | headers: map(), 20 | data: map(), 21 | notification: struct() 22 | } 23 | 24 | @derive Jason.Encoder 25 | defstruct @keys 26 | 27 | # Public API 28 | 29 | def new(attributes \\ %{}) do 30 | %__MODULE__{ 31 | headers: Map.get(attributes, :headers, %{}), 32 | data: Map.get(attributes, :data, %{}), 33 | notification: Notification.new(attributes) 34 | } 35 | end 36 | 37 | def validate(%__MODULE__{headers: _, data: _, notification: nil}), 38 | do: {:error, "[WebMessage.Config] notification is missing"} 39 | 40 | def validate(%__MODULE__{headers: _, data: _, notification: notification} = message_config) do 41 | case Notification.validate(notification) do 42 | {:ok, _} -> 43 | {:ok, message_config} 44 | 45 | {:error, error_message} -> 46 | {:error, error_message} 47 | end 48 | end 49 | 50 | def validate(_), do: {:error, "[WebMessage.Config] Invalid payload"} 51 | end 52 | -------------------------------------------------------------------------------- /lib/messaging/web_message/notification.ex: -------------------------------------------------------------------------------- 1 | defmodule FirebaseAdminEx.Messaging.WebMessage.Notification do 2 | @moduledoc """ 3 | This module is responsible for representing the 4 | attributes of WebMessage.Notification. 5 | """ 6 | 7 | @keys [ 8 | title: "", 9 | body: "", 10 | icon: "" 11 | ] 12 | 13 | @type t :: %__MODULE__{ 14 | title: String.t(), 15 | body: String.t(), 16 | icon: String.t() 17 | } 18 | 19 | @derive Jason.Encoder 20 | defstruct @keys 21 | 22 | # Public API 23 | 24 | def new(attributes \\ %{}) do 25 | %__MODULE__{ 26 | title: Map.get(attributes, :title), 27 | body: Map.get(attributes, :body), 28 | icon: Map.get(attributes, :icon) 29 | } 30 | end 31 | 32 | def validate(%__MODULE__{title: nil, body: _, icon: _}), 33 | do: {:error, "[WebMessage.Notification] title is missing"} 34 | 35 | def validate(%__MODULE__{title: _, body: nil, icon: _}), 36 | do: {:error, "[WebMessage.Notification] body is missing"} 37 | 38 | def validate(%__MODULE__{title: _, body: _, icon: _} = message), do: {:ok, message} 39 | def validate(_), do: {:error, "[WebMessage.Notification] Invalid payload"} 40 | end 41 | -------------------------------------------------------------------------------- /lib/request.ex: -------------------------------------------------------------------------------- 1 | defmodule FirebaseAdminEx.Request do 2 | @default_headers %{"Content-Type" => "application/json"} 3 | @default_options Application.get_env(:firebase_admin_ex, :default_options, []) 4 | 5 | def request(method, url, data, headers \\ %{}) do 6 | method 7 | |> HTTPoison.request( 8 | url, 9 | process_request_body(data), 10 | process_request_headers(headers), 11 | @default_options 12 | ) 13 | end 14 | 15 | # Override the base headers with any passed in. 16 | def process_request_headers(headers) when is_map(headers) do 17 | Map.merge(@default_headers, headers) 18 | |> Enum.into([]) 19 | end 20 | 21 | def process_request_headers(_), do: @default_headers 22 | 23 | defp process_request_body(body) when is_map(body) do 24 | Jason.encode!(body) 25 | end 26 | 27 | defp process_request_body(body), do: body 28 | end 29 | -------------------------------------------------------------------------------- /lib/response.ex: -------------------------------------------------------------------------------- 1 | defmodule FirebaseAdminEx.Response do 2 | def parse(%HTTPoison.Response{} = response) do 3 | case response do 4 | %HTTPoison.Response{status_code: 200, body: body} -> 5 | {:ok, body} 6 | 7 | %HTTPoison.Response{status_code: status_code, body: body} -> 8 | error_message = Jason.decode!(body) |> Map.get("error", %{}) |> Map.get("message") 9 | {:error, "#{status_code} - #{error_message}"} 10 | end 11 | end 12 | 13 | def parse(_response) do 14 | {:error, "Invalid response"} 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /mix.exs: -------------------------------------------------------------------------------- 1 | defmodule FirebaseAdminEx.MixProject do 2 | use Mix.Project 3 | 4 | def project do 5 | [ 6 | app: :firebase_admin_ex, 7 | version: "0.2.0", 8 | elixir: "~> 1.6", 9 | start_permanent: Mix.env() == :prod, 10 | deps: deps(), 11 | description: description(), 12 | package: package(), 13 | source_url: "https://github.com/scripbox/firebase-admin-ex", 14 | homepage_url: "https://github.com/scripbox/firebase-admin-ex" 15 | ] 16 | end 17 | 18 | # Run "mix help compile.app" to learn about applications. 19 | def application do 20 | [ 21 | extra_applications: [:logger] 22 | ] 23 | end 24 | 25 | # Run "mix help deps" to learn about dependencies. 26 | defp deps do 27 | [ 28 | {:httpoison, "~> 1.5"}, 29 | {:jason, "~> 1.1"}, 30 | {:mock, "~> 0.3.3", only: :test}, 31 | {:goth, "~> 1.1"}, 32 | {:ex_doc, "~> 0.21.2", only: :dev, runtime: false} 33 | ] 34 | end 35 | 36 | defp description() do 37 | "The Firebase Admin Elixir SDK" 38 | end 39 | 40 | defp package do 41 | [ 42 | licenses: ["Apache 2.0"], 43 | links: %{"GitHub" => "https://github.com/scripbox/firebase-admin-ex"} 44 | ] 45 | end 46 | end 47 | -------------------------------------------------------------------------------- /mix.lock: -------------------------------------------------------------------------------- 1 | %{ 2 | "base64url": {:hex, :base64url, "0.0.1", "36a90125f5948e3afd7be97662a1504b934dd5dac78451ca6e9abf85a10286be", [:rebar], [], "hexpm"}, 3 | "certifi": {:hex, :certifi, "2.5.1", "867ce347f7c7d78563450a18a6a28a8090331e77fa02380b4a21962a65d36ee5", [:rebar3], [{:parse_trans, "~>3.3", [hex: :parse_trans, repo: "hexpm", optional: false]}], "hexpm"}, 4 | "earmark": {:hex, :earmark, "1.4.0", "397e750b879df18198afc66505ca87ecf6a96645545585899f6185178433cc09", [:mix], [], "hexpm"}, 5 | "ex_doc": {:hex, :ex_doc, "0.21.2", "caca5bc28ed7b3bdc0b662f8afe2bee1eedb5c3cf7b322feeeb7c6ebbde089d6", [:mix], [{:earmark, "~> 1.3.3 or ~> 1.4", [hex: :earmark, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}], "hexpm"}, 6 | "goth": {:hex, :goth, "1.1.0", "85977656822e54217bc0472666f1ce15dc3921495ef5f4f0774ef15503bae207", [:mix], [{:httpoison, "~> 0.11 or ~> 1.0", [hex: :httpoison, repo: "hexpm", optional: false]}, {:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: false]}, {:joken, "~> 2.0", [hex: :joken, repo: "hexpm", optional: false]}], "hexpm"}, 7 | "hackney": {:hex, :hackney, "1.15.1", "9f8f471c844b8ce395f7b6d8398139e26ddca9ebc171a8b91342ee15a19963f4", [:rebar3], [{:certifi, "2.5.1", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "6.0.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "1.0.1", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~>1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "1.1.4", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}], "hexpm"}, 8 | "httpoison": {:hex, :httpoison, "1.5.1", "0f55b5b673b03c5c327dac7015a67cb571b99b631acc0bc1b0b98dcd6b9f2104", [:mix], [{:hackney, "~> 1.8", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm"}, 9 | "idna": {:hex, :idna, "6.0.0", "689c46cbcdf3524c44d5f3dde8001f364cd7608a99556d8fbd8239a5798d4c10", [:rebar3], [{:unicode_util_compat, "0.4.1", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm"}, 10 | "jason": {:hex, :jason, "1.1.2", "b03dedea67a99223a2eaf9f1264ce37154564de899fd3d8b9a21b1a6fd64afe7", [:mix], [{:decimal, "~> 1.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm"}, 11 | "joken": {:hex, :joken, "2.1.0", "bf21a73105d82649f617c5e59a7f8919aa47013d2519ebcc39d998d8d12adda9", [:mix], [{:jose, "~> 1.9", [hex: :jose, repo: "hexpm", optional: false]}], "hexpm"}, 12 | "jose": {:hex, :jose, "1.9.0", "4167c5f6d06ffaebffd15cdb8da61a108445ef5e85ab8f5a7ad926fdf3ada154", [:mix, :rebar3], [{:base64url, "~> 0.0.1", [hex: :base64url, repo: "hexpm", optional: false]}], "hexpm"}, 13 | "makeup": {:hex, :makeup, "1.0.0", "671df94cf5a594b739ce03b0d0316aa64312cee2574b6a44becb83cd90fb05dc", [:mix], [{:nimble_parsec, "~> 0.5.0", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm"}, 14 | "makeup_elixir": {:hex, :makeup_elixir, "0.14.0", "cf8b7c66ad1cff4c14679698d532f0b5d45a3968ffbcbfd590339cb57742f1ae", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm"}, 15 | "meck": {:hex, :meck, "0.8.13", "ffedb39f99b0b99703b8601c6f17c7f76313ee12de6b646e671e3188401f7866", [:rebar3], [], "hexpm"}, 16 | "metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm"}, 17 | "mimerl": {:hex, :mimerl, "1.2.0", "67e2d3f571088d5cfd3e550c383094b47159f3eee8ffa08e64106cdf5e981be3", [:rebar3], [], "hexpm"}, 18 | "mock": {:hex, :mock, "0.3.3", "42a433794b1291a9cf1525c6d26b38e039e0d3a360732b5e467bfc77ef26c914", [:mix], [{:meck, "~> 0.8.13", [hex: :meck, repo: "hexpm", optional: false]}], "hexpm"}, 19 | "nimble_parsec": {:hex, :nimble_parsec, "0.5.1", "c90796ecee0289dbb5ad16d3ad06f957b0cd1199769641c961cfe0b97db190e0", [:mix], [], "hexpm"}, 20 | "parse_trans": {:hex, :parse_trans, "3.3.0", "09765507a3c7590a784615cfd421d101aec25098d50b89d7aa1d66646bc571c1", [:rebar3], [], "hexpm"}, 21 | "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.4", "f0eafff810d2041e93f915ef59899c923f4568f4585904d010387ed74988e77b", [:make, :mix, :rebar3], [], "hexpm"}, 22 | "unicode_util_compat": {:hex, :unicode_util_compat, "0.4.1", "d869e4c68901dd9531385bb0c8c40444ebf624e60b6962d95952775cac5e90cd", [:rebar3], [], "hexpm"}, 23 | } 24 | -------------------------------------------------------------------------------- /test/firebase_admin_ex_test.exs: -------------------------------------------------------------------------------- 1 | defmodule FirebaseAdminExTest do 2 | use ExUnit.Case 3 | 4 | import Mock 5 | 6 | alias FirebaseAdminEx.Request 7 | alias FirebaseAdminEx.RequestMock 8 | alias FirebaseAdminEx.Auth 9 | alias FirebaseAdminEx.Auth.ActionCodeSettings 10 | 11 | @project_id "FIREBASE-PROJECT-ID" 12 | @client_email "FIREBASE-CLIENT-EMAIL" 13 | 14 | defmacro with_request_mock(block) do 15 | quote do 16 | with_mocks([ 17 | { 18 | Request, 19 | [], 20 | [ 21 | request: fn method, url, body, headers -> RequestMock.post(url, body, headers) end 22 | ] 23 | }, 24 | { 25 | Goth.Token, 26 | [], 27 | [ 28 | for_scope: fn _auth_scope -> {:ok, %{token: "token_test"}} end 29 | ] 30 | } 31 | ]) do 32 | unquote(block) 33 | end 34 | end 35 | end 36 | 37 | test "should createa a new email/password user" do 38 | with_request_mock do 39 | 40 | attributes = %{ 41 | "email" => "user@email.com", 42 | "password" => :crypto.strong_rand_bytes(256) |> Base.url_encode64() |> binary_part(0, 256) 43 | } 44 | 45 | {:ok, _response} = Auth.create_email_password_user(attributes, @client_email) 46 | end 47 | end 48 | 49 | test "should generates the email action link for sign-in flows, using valid action code settings" do 50 | with_request_mock do 51 | 52 | action_code_settings = 53 | ActionCodeSettings.new( 54 | %{ 55 | requestType: "EMAIL_SIGNIN", 56 | email: "user@email.com", 57 | returnOobLink: true, 58 | continueUrl: "www.test.com", 59 | canHandleCodeInApp: false, 60 | dynamicLinkDomain: "", 61 | androidPackageName: "", 62 | androidMinimumVersion: "", 63 | androidInstallApp: false, 64 | iOSBundleId: "" 65 | } 66 | ) 67 | 68 | {:ok, _response} = Auth.generate_sign_in_with_email_link(action_code_settings, @client_email, @project_id) 69 | end 70 | end 71 | 72 | test "should not generates the email action link for sign-in flows, using invalid action code settings" do 73 | with_request_mock do 74 | 75 | action_code_settings = 76 | ActionCodeSettings.new( 77 | %{ 78 | requestType: "EMAIL_SIGNIN", 79 | returnOobLink: true, 80 | continueUrl: "www.test.com" 81 | } 82 | ) 83 | 84 | {:error, "[ActionCodeSettings] email is missing"} == Auth.generate_sign_in_with_email_link(action_code_settings, @client_email, @project_id) 85 | end 86 | end 87 | 88 | end 89 | -------------------------------------------------------------------------------- /test/messaging_test.exs: -------------------------------------------------------------------------------- 1 | defmodule FirebaseAdminEx.MessagingTest do 2 | use ExUnit.Case 3 | import Mock 4 | 5 | alias FirebaseAdminEx.Request 6 | alias FirebaseAdminEx.Messaging 7 | alias FirebaseAdminEx.RequestMock 8 | alias FirebaseAdminEx.Messaging.Message 9 | alias FirebaseAdminEx.Messaging.WebMessage.Config, as: WebMessageConfig 10 | alias FirebaseAdminEx.Messaging.AndroidMessage.Config, as: AndroidMessageConfig 11 | alias FirebaseAdminEx.Messaging.APNSMessage.Config, as: APNSMessageConfig 12 | 13 | @project_id "FIREBASE-PROJECT-ID" 14 | 15 | defmacro with_request_mock(block) do 16 | quote do 17 | with_mock Request, 18 | request: fn method, url, body, headers -> RequestMock.post(url, body, headers) end do 19 | unquote(block) 20 | end 21 | end 22 | end 23 | 24 | describe "send/2" do 25 | test "[WebPush] returns response with valid message and oauth_token" do 26 | with_request_mock do 27 | oauth_token = "oauth token" 28 | 29 | message = 30 | Message.new(%{ 31 | data: %{}, 32 | token: "registration-token", 33 | webpush: 34 | WebMessageConfig.new(%{ 35 | headers: %{}, 36 | data: %{}, 37 | title: "notification title", 38 | body: "notification body", 39 | icon: "https://icon.png" 40 | }) 41 | }) 42 | 43 | {:ok, _response} = Messaging.send(@project_id, oauth_token, message) 44 | end 45 | end 46 | 47 | test "[Android] returns response with valid message and oauth_token" do 48 | with_request_mock do 49 | oauth_token = "oauth token" 50 | 51 | message = 52 | Message.new(%{ 53 | data: %{}, 54 | token: "registration-token", 55 | android: 56 | AndroidMessageConfig.new(%{ 57 | headers: %{}, 58 | data: %{}, 59 | title: "notification title", 60 | body: "notification body", 61 | icon: "https://icon.png" 62 | }) 63 | }) 64 | 65 | {:ok, _response} = Messaging.send(@project_id, oauth_token, message) 66 | end 67 | end 68 | 69 | test "[APNS] returns response with valid message and oauth_token" do 70 | with_request_mock do 71 | oauth_token = "oauth token" 72 | 73 | message = 74 | Message.new(%{ 75 | data: %{}, 76 | token: "registration-token", 77 | apns: 78 | APNSMessageConfig.new(%{ 79 | headers: %{}, 80 | payload: %{ 81 | aps: %{ 82 | alert: %{ 83 | title: "Message Title", 84 | body: "Message Body" 85 | }, 86 | sound: "default", 87 | "content-available": 1 88 | }, 89 | custom_data: %{} 90 | } 91 | }) 92 | }) 93 | 94 | {:ok, _response} = Messaging.send(@project_id, oauth_token, message) 95 | end 96 | end 97 | 98 | test "[DATA] returns response with valid message and oauth_token" do 99 | with_request_mock do 100 | oauth_token = "oauth token" 101 | 102 | message = 103 | Message.new(%{ 104 | data: %{ 105 | key_1: "value 1", 106 | key_2: "value 2" 107 | }, 108 | token: "registration-token" 109 | }) 110 | 111 | {:ok, _response} = Messaging.send(@project_id, oauth_token, message) 112 | end 113 | end 114 | end 115 | end 116 | -------------------------------------------------------------------------------- /test/support/request_mock.ex: -------------------------------------------------------------------------------- 1 | defmodule FirebaseAdminEx.RequestMock do 2 | # Public API 3 | def post(_url, _body, _headers) do 4 | {:ok, successful_response()} 5 | end 6 | 7 | # Private API 8 | defp successful_response do 9 | %HTTPoison.Response{ 10 | body: 11 | "{\n \"name\": \"projects/YOUR-FIREBASE-PROJECT-ID/messages/0:1523208634968690%cc9b4facf9fd7ecd\"\n}\n", 12 | headers: [ 13 | {"Content-Type", "application/json; charset=UTF-8"}, 14 | {"Vary", "X-Origin"}, 15 | {"Vary", "Referer"}, 16 | {"Date", "Sun, 08 Apr 2018 17:30:34 GMT"}, 17 | {"Server", "ESF"}, 18 | {"Cache-Control", "private"}, 19 | {"X-XSS-Protection", "1; mode=block"}, 20 | {"X-Frame-Options", "SAMEORIGIN"}, 21 | {"X-Content-Type-Options", "nosniff"}, 22 | {"Alt-Svc", 23 | "hq=\":443\"; ma=2592000; quic=51303432; quic=51303431; quic=51303339; quic=51303335,quic=\":443\"; ma=2592000; v=\"42,41,39,35\""}, 24 | {"Accept-Ranges", "none"}, 25 | {"Vary", "Origin,Accept-Encoding"}, 26 | {"Transfer-Encoding", "chunked"} 27 | ], 28 | request_url: 29 | "https://fcm.googleapis.com/v1/projects/YOUR-FIREBASE-PROJECT-ID/messages:send", 30 | status_code: 200 31 | } 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start() 2 | files = Path.wildcard("./test/support/*.ex") 3 | 4 | Enum.each(files, fn file -> 5 | Code.require_file(file) 6 | end) 7 | --------------------------------------------------------------------------------