firebase-admin 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,11 @@
1
+ module FirebaseAdmin
2
+ # Wrapper for the Firebase Admin REST API
3
+ #
4
+ # @note All methods have been separated into modules and follow the same grouping used in http://???
5
+ # @see http://???
6
+ class Client < API
7
+ Dir[File.expand_path('client/*.rb', __dir__)].sort.each { |f| require f }
8
+
9
+ include FirebaseAdmin::Client::Accounts
10
+ end
11
+ end
@@ -0,0 +1,159 @@
1
+ require 'jwt'
2
+
3
+ module FirebaseAdmin
4
+ class Client
5
+ # Defines methods related to accounts
6
+ module Accounts
7
+ # Create a new account
8
+ #
9
+ # @param params [Hash] A customizable set of params
10
+ # @option options [String] :email
11
+ # @option options [String] :password
12
+ # @option options [String] :phoneNumber
13
+ # @option options [String] :displayName
14
+ # @option options [String] :photoUrl
15
+ # @option options [String] :customAttributes
16
+ # @option customAttributes [String] :roles
17
+ #
18
+ # @return [Resource]
19
+ # @see https://firebase.google.com/docs/reference/rest/auth#section-create-email-password
20
+ #
21
+ # @example
22
+ # FirebaseAdmin.create_account(
23
+ # :email => "lebron@lakers.com",
24
+ # :password => "super-secret",
25
+ # :phoneNumber => "+5555555555",
26
+ # :displayName => "LeBron James",
27
+ # :photoUrl => "http://www.example.com/photo.jpg",
28
+ # :customAttributes => {
29
+ # :roles => ['admin']
30
+ # }
31
+ # )
32
+ def create_account(params)
33
+ post("v1/projects/#{project_id}/accounts", params)
34
+ end
35
+
36
+ # Update an existing account
37
+ #
38
+ # @param params [Hash] A customizable set of params
39
+ # @option options [String] :email
40
+ # @option options [String] :password
41
+ # @option options [String] :phoneNumber
42
+ # @option options [String] :displayName
43
+ # @option options [String] :photoUrl
44
+ # @option options [String] :customAttributes
45
+ # @option customAttributes [String] :roles
46
+ #
47
+ # @return [Resource]
48
+ # @see https://firebase.google.com/docs/reference/rest/auth
49
+ #
50
+ # @example
51
+ # FirebaseAdmin.update_account(
52
+ # email: "lebron@lakers.com",
53
+ # password: "super-secret",
54
+ # phoneNumber: "+5555555555",
55
+ # displayName: "LeBron James",
56
+ # photoUrl: "http://www.example.com/photo.jpg",
57
+ # localId: "1234",
58
+ # customAttributes: {
59
+ # :roles => ['admin']
60
+ # }
61
+ # )
62
+ def update_account(params)
63
+ post("v1/projects/#{project_id}/accounts:update", params)
64
+ end
65
+
66
+ # Sign in with a password
67
+ #
68
+ # @param params [Hash] A customizable set of params
69
+ # @option options [String] :email
70
+ # @option options [String] :password
71
+ #
72
+ # @return [Resource]
73
+ # @see https://firebase.google.com/docs/reference/rest/auth
74
+ #
75
+ # @example
76
+ # FirebaseAdmin.sign_in_with_password(
77
+ # email: "lebron@lakers.com",
78
+ # password: "super-secret"
79
+ # )
80
+ def sign_in_with_password(params)
81
+ post('v1/accounts:signInWithPassword', params)
82
+ end
83
+
84
+ # Sign in with custom token
85
+ #
86
+ # @param token [String] A custom token
87
+ #
88
+ # @return [Resource] with idToken
89
+ #
90
+ # @example
91
+ # FirebaseAdmin.sign_in_with_custom_token("...")
92
+ def sign_in_with_custom_token(token)
93
+ post('v1/accounts:signInWithCustomToken', { token: token, returnSecureToken: true })
94
+ end
95
+
96
+ # Create a custom JWT token for a UID
97
+ #
98
+ # @param uid [String] The uid of a user
99
+ #
100
+ # @return [String]
101
+ # @see https://firebase.google.com/docs/reference/rest/auth
102
+ #
103
+ # @example
104
+ # FirebaseAdmin.create_custom_token('...')
105
+ def create_custom_token(uid)
106
+ credentials = default_credentials
107
+
108
+ service_account_email = credentials.fetch('client_email', ENV['GOOGLE_CLIENT_EMAIL'])
109
+ private_key = OpenSSL::PKey::RSA.new credentials.fetch('private_key', ENV['GOOGLE_PRIVATE_KEY'])
110
+
111
+ now_seconds = Time.now.to_i
112
+ payload = {
113
+ iss: service_account_email,
114
+ sub: service_account_email,
115
+ aud: 'https://identitytoolkit.googleapis.com/google.identity.identitytoolkit.v1.IdentityToolkit',
116
+ iat: now_seconds,
117
+ exp: now_seconds + (60 * 60), # Maximum expiration time is one hour
118
+ uid: uid
119
+ }
120
+ JWT.encode(payload, private_key, 'RS256')
121
+ end
122
+
123
+ # Get user by email/phone/uid
124
+ #
125
+ # @param key [String] either :email or :phone
126
+ # @param value [String] the value to search for
127
+ #
128
+ # @return [Resource]
129
+ #
130
+ # @example
131
+ # FirebaseAdmin.get_user_by(:email, "lebron@lakers.com")
132
+ def get_user_by(key, value)
133
+ params = {}
134
+ params[key] = Array(value)
135
+ response = post('v1/accounts:lookup', params)
136
+ (response[:users] || []).first
137
+ end
138
+
139
+ # Reset emulator
140
+ #
141
+ # @return [Resource]
142
+ # @see https://firebase.google.com/docs/reference/rest/auth
143
+ #
144
+ # @example
145
+ # FirebaseAdmin.reset()
146
+ def reset
147
+ delete("emulator/v1/projects/#{project_id}/accounts")
148
+ end
149
+
150
+ private
151
+
152
+ def default_credentials
153
+ return {} if ENV['GOOGLE_APPLICATION_CREDENTIALS'].nil?
154
+
155
+ JSON.parse(File.read(ENV['GOOGLE_APPLICATION_CREDENTIALS']))
156
+ end
157
+ end
158
+ end
159
+ end
@@ -0,0 +1,78 @@
1
+ require 'faraday'
2
+ require File.expand_path('version', __dir__)
3
+
4
+ module FirebaseAdmin
5
+ # Defines constants and methods related to configuration
6
+ module Configuration
7
+ # An array of valid keys in the options hash when configuring a {FirebaseAdmin::API}
8
+ VALID_OPTIONS_KEYS = %i[
9
+ access_token
10
+ adapter
11
+ connection_options
12
+ endpoint
13
+ user_agent
14
+ project_id
15
+ loud_logger
16
+ ].freeze
17
+
18
+ # By default, don't set a user access token
19
+ DEFAULT_ACCESS_TOKEN = 'owner'.freeze
20
+
21
+ # The adapter that will be used to connect if none is set
22
+ #
23
+ # @note The default faraday adapter is Net::HTTP.
24
+ DEFAULT_ADAPTER = Faraday.default_adapter
25
+
26
+ # By default, don't set any connection options
27
+ DEFAULT_CONNECTION_OPTIONS = {}.freeze
28
+
29
+ # The endpoint that will be used to connect if none is set
30
+ #
31
+ # @note There is no reason to use any other endpoint at this time
32
+ DEFAULT_ENDPOINT = 'https://identitytoolkit.googleapis.com/'.freeze
33
+
34
+ # The response format appended to the path and sent in the 'Accept' header if none is set
35
+ #
36
+ # @note JSON is the only available format at this time
37
+ DEFAULT_FORMAT = :json
38
+
39
+ # The user agent that will be sent to the API endpoint if none is set
40
+ DEFAULT_USER_AGENT = "Firebase Admin Ruby Gem #{FirebaseAdmin::VERSION}".freeze
41
+
42
+ DEFAULT_PROJECT_ID = ''.freeze
43
+
44
+ # By default, don't turn on loud logging
45
+ DEFAULT_LOUD_LOGGER = nil
46
+
47
+ # @private
48
+ attr_accessor(*VALID_OPTIONS_KEYS)
49
+
50
+ # When this module is extended, set all configuration options to their default values
51
+ def self.extended(base)
52
+ base.reset
53
+ end
54
+
55
+ # Convenience method to allow configuration options to be set in a block
56
+ def configure
57
+ yield self
58
+ end
59
+
60
+ # Create a hash of options and their values
61
+ def options
62
+ VALID_OPTIONS_KEYS.inject({}) do |option, key|
63
+ option.merge!(key => send(key))
64
+ end
65
+ end
66
+
67
+ # Reset all configuration options to defaults
68
+ def reset
69
+ self.access_token = DEFAULT_ACCESS_TOKEN
70
+ self.adapter = DEFAULT_ADAPTER
71
+ self.connection_options = DEFAULT_CONNECTION_OPTIONS
72
+ self.endpoint = DEFAULT_ENDPOINT
73
+ self.user_agent = DEFAULT_USER_AGENT
74
+ self.project_id = DEFAULT_PROJECT_ID
75
+ self.loud_logger = DEFAULT_LOUD_LOGGER
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,30 @@
1
+ require 'faraday_middleware'
2
+ Dir[File.expand_path('../faraday/*.rb', __dir__)].sort.each { |f| require f }
3
+
4
+ module FirebaseAdmin
5
+ # @private
6
+ module Connection
7
+ private
8
+
9
+ def connection
10
+ options = {
11
+ headers: {
12
+ 'Content-Type' => 'application/json; charset=utf-8',
13
+ 'Accept' => 'application/json; charset=utf-8',
14
+ 'User-Agent' => user_agent
15
+ },
16
+ url: endpoint
17
+ }.merge(connection_options)
18
+
19
+ Faraday::Connection.new(options) do |connection|
20
+ connection.authorization :Bearer, access_token if access_token
21
+ connection.use Faraday::Request::UrlEncoded
22
+ connection.use FaradayMiddleware::Mashify
23
+ connection.use Faraday::Response::ParseJson
24
+ connection.use FaradayMiddleware::RaiseHttpException
25
+ connection.use FaradayMiddleware::LoudLogger if loud_logger
26
+ connection.adapter(adapter)
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,28 @@
1
+ module FirebaseAdmin
2
+ # Custom error class for rescuing from all Firebase errors
3
+ class Error < StandardError; end
4
+
5
+ # Raised when Firebase returns the HTTP status code 400
6
+ class BadRequest < Error; end
7
+
8
+ # Raised when Firebase returns the HTTP status code 404
9
+ class NotFound < Error; end
10
+
11
+ # Raised when Firebase returns the HTTP status code 429
12
+ class TooManyRequests < Error; end
13
+
14
+ # Raised when Firebase returns the HTTP status code 500
15
+ class InternalServerError < Error; end
16
+
17
+ # Raised when Firebase returns the HTTP status code 502
18
+ class BadGateway < Error; end
19
+
20
+ # Raised when Firebase returns the HTTP status code 503
21
+ class ServiceUnavailable < Error; end
22
+
23
+ # Raised when Firebase returns the HTTP status code 504
24
+ class GatewayTimeout < Error; end
25
+
26
+ # Raised when Firebase returns the HTTP status code 429
27
+ class RateLimitExceeded < Error; end
28
+ end
@@ -0,0 +1,44 @@
1
+ require 'addressable/uri'
2
+ require 'json'
3
+
4
+ module FirebaseAdmin
5
+ # Defines HTTP request methods
6
+ module Request
7
+ # Perform an HTTP GET request
8
+ def get(path, options = {})
9
+ request(:get, path, options)
10
+ end
11
+
12
+ # Perform an HTTP POST request
13
+ def post(path, options = {})
14
+ request(:post, path, options)
15
+ end
16
+
17
+ # Perform an HTTP PUT request
18
+ def put(path, options = {})
19
+ request(:put, path, options)
20
+ end
21
+
22
+ # Perform an HTTP DELETE request
23
+ def delete(path, options = {})
24
+ request(:delete, path, options)
25
+ end
26
+
27
+ private
28
+
29
+ # Perform an HTTP request
30
+ def request(method, path, options)
31
+ response = connection.send(method) do |request|
32
+ case method
33
+ when :post, :put
34
+ request.path = Addressable::URI.escape(path)
35
+ request.body = options.to_json unless options.empty?
36
+
37
+ else
38
+ request.url(Addressable::URI.escape(path), options)
39
+ end
40
+ end
41
+ response.body
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,3 @@
1
+ module FirebaseAdmin
2
+ VERSION = '0.1.0'.freeze unless defined?(::FirebaseAdmin::VERSION)
3
+ end
@@ -0,0 +1,73 @@
1
+ # frozen_string_literal: true
2
+
3
+ require File.expand_path('../spec_helper', __dir__)
4
+
5
+ describe Faraday::Response do
6
+ before do
7
+ @client = FirebaseAdmin::Client.new(project_id: 'test-project')
8
+ end
9
+
10
+ {
11
+ 400 => FirebaseAdmin::BadRequest,
12
+ 404 => FirebaseAdmin::NotFound,
13
+ 429 => FirebaseAdmin::TooManyRequests,
14
+ 500 => FirebaseAdmin::InternalServerError,
15
+ 503 => FirebaseAdmin::ServiceUnavailable
16
+ }.each do |status, exception|
17
+ context "when HTTP status is #{status}" do
18
+ before do
19
+ stub_post('v1/projects/test-project/accounts')
20
+ .to_return(status: status)
21
+ end
22
+
23
+ it "should raise #{exception.name} error" do
24
+ expect do
25
+ @client.create_account({})
26
+ end.to raise_error(exception)
27
+ end
28
+ end
29
+ end
30
+
31
+ context 'when a 400 is raised' do
32
+ before do
33
+ stub_post('v1/projects/test-project/accounts')
34
+ .to_return(body: fixture('400_error.json'), status: 400)
35
+ end
36
+
37
+ it 'should return the body error message' do
38
+ expect do
39
+ @client.create_account({})
40
+ end.to raise_error(FirebaseAdmin::BadRequest, /INVALID_PHONE_NUMBER : Invalid format\./)
41
+ end
42
+ end
43
+
44
+ context 'when a 502 is raised with an HTML response' do
45
+ before do
46
+ stub_post('v1/projects/test-project/accounts').to_return(
47
+ body: fixture('bad_gateway.html'),
48
+ status: 502
49
+ )
50
+ end
51
+
52
+ it 'should raise an FirebaseAdmin::BadGateway' do
53
+ expect do
54
+ @client.create_account({})
55
+ end.to raise_error(FirebaseAdmin::BadGateway)
56
+ end
57
+ end
58
+
59
+ context 'when a 504 is raised with an HTML response' do
60
+ before do
61
+ stub_post('v1/projects/test-project/accounts').to_return(
62
+ body: fixture('gateway_timeout.html'),
63
+ status: 504
64
+ )
65
+ end
66
+
67
+ it 'should raise an FirebaseAdmin::GatewayTimeout' do
68
+ expect do
69
+ @client.create_account({})
70
+ end.to raise_error(FirebaseAdmin::GatewayTimeout)
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,134 @@
1
+ require File.expand_path('../spec_helper', __dir__)
2
+
3
+ describe FirebaseAdmin::API do
4
+ before do
5
+ @keys = FirebaseAdmin::Configuration::VALID_OPTIONS_KEYS
6
+ end
7
+
8
+ context 'with module configuration' do
9
+ before do
10
+ FirebaseAdmin.configure do |config|
11
+ @keys.each do |key|
12
+ config.send("#{key}=", key)
13
+ end
14
+ end
15
+ end
16
+
17
+ after do
18
+ FirebaseAdmin.reset
19
+ end
20
+
21
+ it 'should inherit module configuration' do
22
+ api = FirebaseAdmin::API.new
23
+ @keys.each do |key|
24
+ expect(api.send(key)).to eq(key)
25
+ end
26
+ end
27
+
28
+ context 'with class configuration' do
29
+ before do
30
+ @configuration = {
31
+ access_token: 'AT',
32
+ adapter: :typhoeus,
33
+ connection_options: { ssl: { verify: true } },
34
+ endpoint: 'http://tumblr.com/',
35
+ user_agent: 'Custom User Agent',
36
+ project_id: 'test-project',
37
+ loud_logger: true
38
+ }
39
+ end
40
+
41
+ context 'during initialization' do
42
+ it 'should override module configuration' do
43
+ api = FirebaseAdmin::API.new(@configuration)
44
+ @keys.each do |key|
45
+ expect(api.send(key)).to eq(@configuration[key])
46
+ end
47
+ end
48
+ end
49
+
50
+ context 'after initialization' do
51
+ let(:api) { FirebaseAdmin::API.new }
52
+
53
+ before do
54
+ @configuration.each do |key, value|
55
+ api.send("#{key}=", value)
56
+ end
57
+ end
58
+
59
+ it 'should override module configuration after initialization' do
60
+ @keys.each do |key|
61
+ expect(api.send(key)).to eq(@configuration[key])
62
+ end
63
+ end
64
+
65
+ describe '#connection' do
66
+ it 'should use the connection_options' do
67
+ expect(Faraday::Connection).to receive(:new).with(include(ssl: { verify: true }))
68
+ api.send(:connection)
69
+ end
70
+ end
71
+ end
72
+ end
73
+ end
74
+
75
+ describe '#config' do
76
+ subject { FirebaseAdmin::API.new }
77
+
78
+ let(:config) do
79
+ c = {}
80
+ @keys.each { |key| c[key] = key }
81
+ c
82
+ end
83
+
84
+ it 'returns a hash representing the configuration' do
85
+ @keys.each do |key|
86
+ subject.send("#{key}=", key)
87
+ end
88
+ expect(subject.config).to eq(config)
89
+ end
90
+ end
91
+
92
+ describe 'loud_logger param' do
93
+ before do
94
+ @client = FirebaseAdmin::Client.new(project_id: 'test-project', loud_logger: true)
95
+ end
96
+
97
+ context 'outputs to STDOUT with faraday logs when enabled' do
98
+ before do
99
+ stub_post('v1/projects/test-project/accounts')
100
+ .to_return(body: fixture('create_account.json'), headers: { content_type: 'application/json; charset=utf-8' })
101
+ end
102
+
103
+ it 'should return the body error message' do
104
+ output = capture_output do
105
+ @client.create_account({})
106
+ end
107
+
108
+ expect(output).to include 'INFO -- : Started POST request to: '
109
+ expect(output).to include 'DEBUG -- : Response Headers:'
110
+ expect(output).to include "User-Agent : Firebase Admin Ruby Gem #{FirebaseAdmin::VERSION}"
111
+ end
112
+ end
113
+
114
+ context 'shows STDOUT output when errors occur' do
115
+ before do
116
+ stub_post('v1/projects/test-project/accounts')
117
+ .to_return(body: '{"error":{"message": "Bad words are bad."}}', status: 400)
118
+ end
119
+
120
+ it 'should return the body error message' do
121
+ output = capture_output do
122
+ @client.create_account({})
123
+ rescue StandardError
124
+ nil
125
+ end
126
+
127
+ expect(output).to include 'INFO -- : Started POST request to: '
128
+ expect(output).to include 'DEBUG -- : Response Headers:'
129
+ expect(output).to include "User-Agent : Firebase Admin Ruby Gem #{FirebaseAdmin::VERSION}"
130
+ expect(output).to include '{"error":{"message": "Bad words are bad."}}'
131
+ end
132
+ end
133
+ end
134
+ end