firebase-admin 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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