umami-ruby 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,599 @@
1
+ require "faraday"
2
+ require "json"
3
+
4
+ module Umami
5
+ # The Client class provides methods to interact with the Umami API.
6
+ #
7
+ # @see https://umami.is/docs/api Umami API Documentation
8
+ class Client
9
+ attr_reader :uri_base, :request_timeout
10
+
11
+ # Initialize a new Umami API client
12
+ #
13
+ # @param options [Hash] options to create a client with.
14
+ # @option options [String] :uri_base The base URI for the Umami API
15
+ # @option options [Integer] :request_timeout Request timeout in seconds
16
+ # @option options [String] :access_token Access token for authentication
17
+ # @option options [String] :username Username for authentication (only for self-hosted instances)
18
+ # @option options [String] :password Password for authentication (only for self-hosted instances)
19
+ def initialize(options = {})
20
+ @config = options[:config] || Umami.configuration
21
+ @uri_base = options[:uri_base] || @config.uri_base
22
+ @request_timeout = options[:request_timeout] || @config.request_timeout
23
+ @access_token = options[:access_token] || @config.access_token
24
+ @username = options[:username] || @config.username
25
+ @password = options[:password] || @config.password
26
+
27
+ validate_client_options
28
+
29
+ authenticate if @access_token.nil?
30
+ end
31
+
32
+ # Check if the client is configured for Umami Cloud
33
+ #
34
+ # @return [Boolean] true if using Umami Cloud, false otherwise
35
+ def cloud?
36
+ @uri_base == Umami::Configuration::UMAMI_CLOUD_URL
37
+ end
38
+
39
+ # Check if the client is configured for a self-hosted Umami instance
40
+ #
41
+ # @return [Boolean] true if using a self-hosted instance, false otherwise
42
+ def self_hosted?
43
+ !cloud?
44
+ end
45
+
46
+ # Verify the authentication token
47
+ #
48
+ # @return [Hash] Token verification result
49
+ # @see https://umami.is/docs/api/authentication#post-/api/auth/verify
50
+ def verify_token
51
+ get("/api/auth/verify")
52
+ end
53
+
54
+ # Authentication endpoints
55
+
56
+ # Authenticate with the Umami API using username and password
57
+ #
58
+ # This method is called automatically when initializing the client if an access token is not provided.
59
+ # It sets the @access_token instance variable upon successful authentication.
60
+ #
61
+ # @raise [Umami::AuthenticationError] if username or password is missing, or if authentication fails
62
+ # @return [void]
63
+ # @see https://umami.is/docs/api/authentication#post-/api/auth/login
64
+ def authenticate
65
+ raise Umami::AuthenticationError, "Username and password are required for authentication" if @username.nil? || @password.nil?
66
+
67
+ response = connection.post("/api/auth/login") do |req|
68
+ req.body = { username: @username, password: @password }.to_json
69
+ end
70
+
71
+ data = JSON.parse(response.body)
72
+ @access_token = data["token"]
73
+ rescue Faraday::Error, JSON::ParserError => e
74
+ raise Umami::AuthenticationError, "Authentication failed: #{e.message}"
75
+ end
76
+
77
+ # -------- Users endpoints --------
78
+
79
+ # Create a new user
80
+ #
81
+ # @param username [String] The user's username
82
+ # @param password [String] The user's password
83
+ # @param role [String] The user's role ('admin' or 'user')
84
+ # @return [Hash] Created user details
85
+ # @see https://umami.is/docs/api/users-api#post-/api/users
86
+ def create_user(username, password, role)
87
+ post("/api/users", { username: username, password: password, role: role })
88
+ end
89
+
90
+ # Get all users (admin access required)
91
+ #
92
+ # @return [Array<Hash>] List of all users
93
+ # @see https://umami.is/docs/api/users-api#get-/api/admin/users
94
+ def users
95
+ get("/api/admin/users")
96
+ end
97
+
98
+ # Get a user by ID
99
+ #
100
+ # @param user_id [String] The user's ID
101
+ # @return [Hash] User details
102
+ # @see https://umami.is/docs/api/users-api#get-/api/users/:userid
103
+ def user(user_id)
104
+ get("/api/users/#{user_id}")
105
+ end
106
+
107
+ # Update a user
108
+ #
109
+ # @param user_id [String] The user's ID
110
+ # @param params [Hash] User parameters to update
111
+ # @option params [String] :username The user's new username
112
+ # @option params [String] :password The user's new password
113
+ # @option params [String] :role The user's new role
114
+ # @return [Hash] Updated user details
115
+ # @see https://umami.is/docs/api/users-api#post-/api/users/:userid
116
+ def update_user(user_id, params = {})
117
+ post("/api/users/#{user_id}", params)
118
+ end
119
+
120
+ # Delete a user
121
+ #
122
+ # @param user_id [String] The user's ID
123
+ # @return [String] Confirmation message
124
+ # @see https://umami.is/docs/api/users-api#delete-/api/users/:userid
125
+ def delete_user(user_id)
126
+ delete("/api/users/#{user_id}")
127
+ end
128
+
129
+ # Get all websites for a user
130
+ #
131
+ # @param user_id [String] The user's ID
132
+ # @param params [Hash] Query parameters
133
+ # @option params [String] :query Search text
134
+ # @option params [Integer] :page Page number
135
+ # @option params [Integer] :pageSize Number of results per page
136
+ # @option params [String] :orderBy Column to order by
137
+ # @return [Array<Hash>] List of user's websites
138
+ # @see https://umami.is/docs/api/users-api#get-/api/users/:userid/websites
139
+ def user_websites(user_id, params = {})
140
+ get("/api/users/#{user_id}/websites", params)
141
+ end
142
+
143
+ # Get all teams for a user
144
+ #
145
+ # @param user_id [String] The user's ID
146
+ # @param params [Hash] Query parameters
147
+ # @option params [String] :query Search text
148
+ # @option params [Integer] :page Page number
149
+ # @option params [Integer] :pageSize Number of results per page
150
+ # @option params [String] :orderBy Column to order by
151
+ # @return [Array<Hash>] List of user's teams
152
+ # @see https://umami.is/docs/api/users-api#get-/api/users/:userid/teams
153
+ def user_teams(user_id, params = {})
154
+ get("/api/users/#{user_id}/teams", params)
155
+ end
156
+
157
+ # -------- Teams endpoints --------
158
+
159
+ # Create a new team
160
+ #
161
+ # @param name [String] The team's name
162
+ # @return [Hash] Created team details
163
+ # @see https://umami.is/docs/api/teams-api#post-/api/teams
164
+ def create_team(name)
165
+ post("/api/teams", { name: name })
166
+ end
167
+
168
+ # Get all teams
169
+ #
170
+ # @param params [Hash] Query parameters
171
+ # @option params [String] :query Search text
172
+ # @option params [Integer] :page Page number
173
+ # @option params [Integer] :pageSize Number of results per page
174
+ # @option params [String] :orderBy Column to order by
175
+ # @return [Array<Hash>] List of teams
176
+ # @see https://umami.is/docs/api/teams-api#get-/api/teams
177
+ def teams(params = {})
178
+ get("/api/teams", params)
179
+ end
180
+
181
+ # Join a team
182
+ #
183
+ # @param access_code [String] The team's access code
184
+ # @return [Hash] Joined team details
185
+ # @see https://umami.is/docs/api/teams-api#post-/api/teams/join
186
+ def join_team(access_code)
187
+ post("/api/teams/join", { accessCode: access_code })
188
+ end
189
+
190
+ # Get a team by ID
191
+ #
192
+ # @param team_id [String] The team's ID
193
+ # @return [Hash] Team details
194
+ # @see https://umami.is/docs/api/teams-api#get-/api/teams/:teamid
195
+ def team(team_id)
196
+ get("/api/teams/#{team_id}")
197
+ end
198
+
199
+ # Update a team
200
+ #
201
+ # @param team_id [String] The team's ID
202
+ # @param params [Hash] Team parameters to update
203
+ # @option params [String] :name The team's new name
204
+ # @option params [String] :accessCode The team's new access code
205
+ # @return [Hash] Updated team details
206
+ # @see https://umami.is/docs/api/teams-api#post-/api/teams/:teamid
207
+ def update_team(team_id, params = {})
208
+ post("/api/teams/#{team_id}", params)
209
+ end
210
+
211
+ # Delete a team
212
+ #
213
+ # @param team_id [String] The team's ID
214
+ # @return [String] Confirmation message
215
+ # @see https://umami.is/docs/api/teams-api#delete-/api/teams/:teamid
216
+ def delete_team(team_id)
217
+ delete("/api/teams/#{team_id}")
218
+ end
219
+
220
+ # Get all users in a team
221
+ #
222
+ # @param team_id [String] The team's ID
223
+ # @param params [Hash] Query parameters
224
+ # @option params [String] :query Search text
225
+ # @option params [Integer] :page Page number
226
+ # @option params [Integer] :pageSize Number of results per page
227
+ # @option params [String] :orderBy Column to order by
228
+ # @return [Array<Hash>] List of team users
229
+ # @see https://umami.is/docs/api/teams-api#get-/api/teams/:teamid/users
230
+ def team_users(team_id, params = {})
231
+ get("/api/teams/#{team_id}/users", params)
232
+ end
233
+
234
+ # Add a user to a team
235
+ #
236
+ # @param team_id [String] The team's ID
237
+ # @param user_id [String] The user's ID
238
+ # @param role [String] The user's role in the team
239
+ # @return [Hash] Added team user details
240
+ # @see https://umami.is/docs/api/teams-api#post-/api/teams/:teamid/users
241
+ def add_team_user(team_id, user_id, role)
242
+ post("/api/teams/#{team_id}/users", { userId: user_id, role: role })
243
+ end
244
+
245
+ # Get a user in a team
246
+ #
247
+ # @param team_id [String] The team's ID
248
+ # @param user_id [String] The user's ID
249
+ # @return [Hash] Team user details
250
+ # @see https://umami.is/docs/api/teams-api#get-/api/teams/:teamid/users/:userid
251
+ def team_user(team_id, user_id)
252
+ get("/api/teams/#{team_id}/users/#{user_id}")
253
+ end
254
+
255
+ # Update a user's role in a team
256
+ #
257
+ # @param team_id [String] The team's ID
258
+ # @param user_id [String] The user's ID
259
+ # @param role [String] The user's new role
260
+ # @return [String] Confirmation message
261
+ # @see https://umami.is/docs/api/teams-api#post-/api/teams/:teamid/users/:userid
262
+ def update_team_user(team_id, user_id, role)
263
+ post("/api/teams/#{team_id}/users/#{user_id}", { role: role })
264
+ end
265
+
266
+ # Remove a user from a team
267
+ #
268
+ # @param team_id [String] The team's ID
269
+ # @param user_id [String] The user's ID
270
+ # @return [String] Confirmation message
271
+ # @see https://umami.is/docs/api/teams-api#delete-/api/teams/:teamid/users/:userid
272
+ def delete_team_user(team_id, user_id)
273
+ delete("/api/teams/#{team_id}/users/#{user_id}")
274
+ end
275
+
276
+ # Get all websites for a team
277
+ #
278
+ # @param team_id [String] The team's ID
279
+ # @param params [Hash] Query parameters
280
+ # @option params [String] :query Search text
281
+ # @option params [Integer] :page Page number
282
+ # @option params [Integer] :pageSize Number of results per page
283
+ # @option params [String] :orderBy Column to order by
284
+ # @return [Array<Hash>] List of team websites
285
+ # @see https://umami.is/docs/api/teams-api#get-/api/teams/:teamid/websites
286
+ def team_websites(team_id, params = {})
287
+ get("/api/teams/#{team_id}/websites", params)
288
+ end
289
+
290
+ # -------- Websites endpoints --------
291
+
292
+ # Get all websites
293
+ #
294
+ # @param params [Hash] Query parameters
295
+ # @option params [String] :query Search text
296
+ # @option params [Integer] :page Page number
297
+ # @option params [Integer] :pageSize Number of results per page
298
+ # @option params [String] :orderBy Column to order by
299
+ # @return [Array<Hash>] List of websites
300
+ # @see https://umami.is/docs/api/websites-api#get-/api/websites
301
+ def websites(params = {})
302
+ get("/api/websites", params)
303
+ end
304
+
305
+ # Create a new website
306
+ #
307
+ # @param params [Hash] Website parameters
308
+ # @option params [String] :domain The full domain of the tracked website
309
+ # @option params [String] :name The name of the website in Umami
310
+ # @option params [String] :shareId A unique string to enable a share url (optional)
311
+ # @option params [String] :teamId The ID of the team the website will be created under (optional)
312
+ # @return [Hash] Created website details
313
+ # @see https://umami.is/docs/api/websites-api#post-/api/websites
314
+ def create_website(params = {})
315
+ post("/api/websites", params)
316
+ end
317
+
318
+ # Get a website by ID
319
+ #
320
+ # @param id [String] The website's ID
321
+ # @return [Hash] Website details
322
+ # @see https://umami.is/docs/api/websites-api#get-/api/websites/:websiteid
323
+ def website(id)
324
+ get("/api/websites/#{id}")
325
+ end
326
+
327
+ # Update a website
328
+ #
329
+ # @param website_id [String] The website's ID
330
+ # @param params [Hash] Website parameters to update
331
+ # @option params [String] :name The name of the website in Umami
332
+ # @option params [String] :domain The full domain of the tracked website
333
+ # @option params [String] :shareId A unique string to enable a share url
334
+ # @return [Hash] Updated website details
335
+ # @see https://umami.is/docs/api/websites-api#post-/api/websites/:websiteid
336
+ def update_website(website_id, params = {})
337
+ post("/api/websites/#{website_id}", params)
338
+ end
339
+
340
+ # Delete a website
341
+ #
342
+ # @param website_id [String] The website's ID
343
+ # @return [String] Confirmation message
344
+ # @see https://umami.is/docs/api/websites-api#delete-/api/websites/:websiteid
345
+ def delete_website(website_id)
346
+ delete("/api/websites/#{website_id}")
347
+ end
348
+
349
+ # Reset a website's data
350
+ #
351
+ # @param website_id [String] The website's ID
352
+ # @return [String] Confirmation message
353
+ # @see https://umami.is/docs/api/websites-api#post-/api/websites/:websiteid/reset
354
+ def reset_website(website_id)
355
+ post("/api/websites/#{website_id}/reset")
356
+ end
357
+
358
+ # -------- Website stats endpoints --------
359
+
360
+ # Get website statistics
361
+ #
362
+ # @param id [String] The website's ID
363
+ # @param params [Hash] Query parameters
364
+ # @option params [Integer] :startAt Timestamp (in ms) of starting date
365
+ # @option params [Integer] :endAt Timestamp (in ms) of end date
366
+ # @option params [String] :url Name of URL
367
+ # @option params [String] :referrer Name of referrer
368
+ # @option params [String] :title Name of page title
369
+ # @option params [String] :query Name of query
370
+ # @option params [String] :event Name of event
371
+ # @option params [String] :os Name of operating system
372
+ # @option params [String] :browser Name of browser
373
+ # @option params [String] :device Name of device
374
+ # @option params [String] :country Name of country
375
+ # @option params [String] :region Name of region/state/province
376
+ # @option params [String] :city Name of city
377
+ # @return [Hash] Website statistics
378
+ # @see https://umami.is/docs/api/website-stats#get-/api/websites/:websiteid/stats
379
+ def website_stats(id, params = {})
380
+ get("/api/websites/#{id}/stats", params)
381
+ end
382
+
383
+ # Get active visitors for a website
384
+ #
385
+ # @param id [String] The website's ID
386
+ # @return [Hash] Number of active visitors
387
+ # @see https://umami.is/docs/api/website-stats#get-/api/websites/:websiteid/active
388
+ def website_active_visitors(id)
389
+ get("/api/websites/#{id}/active")
390
+ end
391
+
392
+ # Get website events
393
+ #
394
+ # @param id [String] The website's ID
395
+ # @param params [Hash] Query parameters
396
+ # @option params [Integer] :startAt Timestamp (in ms) of starting date
397
+ # @option params [Integer] :endAt Timestamp (in ms) of end date
398
+ # @option params [String] :unit Time unit (year | month | hour | day)
399
+ # @option params [String] :timezone Timezone (ex. America/Los_Angeles)
400
+ # @option params [String] :url Name of URL
401
+ # @return [Array<Hash>] Website events
402
+ # @see https://umami.is/docs/api/website-stats#get-/api/websites/:websiteid/events
403
+ def website_events(id, params = {})
404
+ get("/api/websites/#{id}/events", params)
405
+ end
406
+
407
+ # Get website pageviews
408
+ #
409
+ # @param id [String] The website's ID
410
+ # @param params [Hash] Query parameters
411
+ # @option params [Integer] :startAt Timestamp (in ms) of starting date
412
+ # @option params [Integer] :endAt Timestamp (in ms) of end date
413
+ # @option params [String] :unit Time unit (year | month | hour | day)
414
+ # @option params [String] :timezone Timezone (ex. America/Los_Angeles)
415
+ # @option params [String] :url Name of URL
416
+ # @option params [String] :referrer Name of referrer
417
+ # @option params [String] :title Name of page title
418
+ # @option params [String] :os Name of operating system
419
+ # @option params [String] :browser Name of browser
420
+ # @option params [String] :device Name of device
421
+ # @option params [String] :country Name of country
422
+ # @option params [String] :region Name of region/state/province
423
+ # @option params [String] :city Name of city
424
+ # @return [Hash] Website pageviews and sessions
425
+ # @see https://umami.is/docs/api/website-stats#get-/api/websites/:websiteid/pageviews
426
+ def website_pageviews(id, params = {})
427
+ get("/api/websites/#{id}/pageviews", params)
428
+ end
429
+
430
+ # Get website metrics
431
+ #
432
+ # @param id [String] The website's ID
433
+ # @param params [Hash] Query parameters
434
+ # @option params [Integer] :startAt Timestamp (in ms) of starting date
435
+ # @option params [Integer] :endAt Timestamp (in ms) of end date
436
+ # @option params [String] :type Metrics type (url | referrer | browser | os | device | country | event)
437
+ # @option params [String] :url Name of URL
438
+ # @option params [String] :referrer Name of referrer
439
+ # @option params [String] :title Name of page title
440
+ # @option params [String] :query Name of query
441
+ # @option params [String] :event Name of event
442
+ # @option params [String] :os Name of operating system
443
+ # @option params [String] :browser Name of browser
444
+ # @option params [String] :device Name of device
445
+ # @option params [String] :country Name of country
446
+ # @option params [String] :region Name of region/state/province
447
+ # @option params [String] :city Name of city
448
+ # @option params [String] :language Name of language
449
+ # @option params [Integer] :limit Number of results to return (default: 500)
450
+ # @return [Array<Hash>] Website metrics
451
+ # @see https://umami.is/docs/api/website-stats#get-/api/websites/:websiteid/metrics
452
+ def website_metrics(id, params = {})
453
+ get("/api/websites/#{id}/metrics", params)
454
+ end
455
+
456
+ # Get event data events
457
+ #
458
+ # @param website_id [String] The website's ID
459
+ # @param params [Hash] Query parameters
460
+ # @option params [Integer] :startAt Timestamp (in ms) of starting date
461
+ # @option params [Integer] :endAt Timestamp (in ms) of end date
462
+ # @option params [String] :event Event Name filter
463
+ # @return [Array<Hash>] Event data events
464
+ # @see https://umami.is/docs/api/event-data#get-/api/event-data/events
465
+ def event_data_events(website_id, params = {})
466
+ get("/api/event-data/events", params.merge(websiteId: website_id))
467
+ end
468
+
469
+
470
+ # -------- Event data endpoints --------
471
+
472
+ # Get event data fields
473
+ #
474
+ # @param website_id [String] The website's ID
475
+ # @param params [Hash] Query parameters
476
+ # @option params [Integer] :startAt Timestamp (in ms) of starting date
477
+ # @option params [Integer] :endAt Timestamp (in ms) of end date
478
+ # @return [Array<Hash>] Event data fields
479
+ # @see https://umami.is/docs/api/event-data#get-/api/event-data/fields
480
+ def event_data_fields(website_id, params = {})
481
+ get("/api/event-data/fields", params.merge(websiteId: website_id))
482
+ end
483
+
484
+ # Get event data stats
485
+ #
486
+ # @param website_id [String] The website's ID
487
+ # @param params [Hash] Query parameters
488
+ # @option params [Integer] :startAt Timestamp (in ms) of starting date
489
+ # @option params [Integer] :endAt Timestamp (in ms) of end date
490
+ # @return [Array<Hash>] Event data stats
491
+ # @see https://umami.is/docs/api/event-data#get-/api/event-data/stats
492
+ def event_data_stats(website_id, params = {})
493
+ get("/api/event-data/stats", params.merge(websiteId: website_id))
494
+ end
495
+
496
+ # -------- Sending stats endpoint --------
497
+
498
+ # Send an event
499
+ #
500
+ # @param payload [Hash] Event payload
501
+ # @option payload [String] :hostname Name of host
502
+ # @option payload [String] :language Language of visitor (ex. "en-US")
503
+ # @option payload [String] :referrer Referrer URL
504
+ # @option payload [String] :screen Screen resolution (ex. "1920x1080")
505
+ # @option payload [String] :title Page title
506
+ # @option payload [String] :url Page URL
507
+ # @option payload [String] :website Website ID
508
+ # @option payload [String] :name Name of the event
509
+ # @option payload [Hash] :data Additional data for the event
510
+ # @return [Hash] Response from the server
511
+ # @see https://umami.is/docs/api/sending-stats
512
+ def send_event(payload)
513
+ post("/api/send", { type: "event", payload: payload })
514
+ end
515
+
516
+
517
+ private
518
+
519
+ def authenticate
520
+ raise Umami::AuthenticationError, "Username and password are required for authentication" if @username.nil? || @password.nil?
521
+
522
+ response = connection.post("/api/auth/login") do |req|
523
+ req.body = { username: @username, password: @password }.to_json
524
+ end
525
+
526
+ data = JSON.parse(response.body)
527
+ @access_token = data["token"]
528
+ rescue Faraday::Error, JSON::ParserError => e
529
+ raise Umami::AuthenticationError, "Authentication failed: #{e.message}"
530
+ end
531
+
532
+ def get(path, params = {})
533
+ response = connection.get(path, params)
534
+ JSON.parse(response.body)
535
+ rescue Faraday::Error => e
536
+ handle_error(e)
537
+ end
538
+
539
+ def post(path, body = {})
540
+ response = connection.post(path, body.to_json)
541
+ JSON.parse(response.body)
542
+ rescue Faraday::Error => e
543
+ handle_error(e)
544
+ end
545
+
546
+ def delete(path)
547
+ response = connection.delete(path)
548
+ response.body == "ok" ? "ok" : JSON.parse(response.body)
549
+ rescue Faraday::Error => e
550
+ handle_error(e)
551
+ end
552
+
553
+ def connection
554
+ @connection ||= Faraday.new(url: uri_base) do |faraday|
555
+ faraday.request :json
556
+ faraday.response :raise_error
557
+ faraday.adapter Faraday.default_adapter
558
+ faraday.headers["Authorization"] = "Bearer #{@access_token}" if @access_token
559
+ faraday.options.timeout = request_timeout
560
+ end
561
+ end
562
+
563
+ def handle_error(error)
564
+ case error
565
+ when Faraday::ResourceNotFound
566
+ raise Umami::NotFoundError, "Resource not found: #{error.message}"
567
+ when Faraday::ClientError
568
+ raise Umami::ClientError, "Client error: #{error.message}"
569
+ when Faraday::ServerError
570
+ raise Umami::ServerError, "Server error: #{error.message}"
571
+ else
572
+ raise Umami::APIError, "API request failed: #{error.message}"
573
+ end
574
+ end
575
+
576
+ def validate_client_options
577
+ if @access_token && @uri_base.nil?
578
+ @uri_base = Umami::Configuration::UMAMI_CLOUD_URL
579
+ Umami.logger.info "No URI base provided with access token. Using Umami Cloud URL: #{@uri_base}"
580
+ end
581
+
582
+ raise Umami::ConfigurationError, "URI base is required for self-hosted instances" if @uri_base.nil? && !@access_token
583
+
584
+ if cloud? && (@username || @password)
585
+ raise Umami::ConfigurationError, "Username/password authentication is not supported for Umami Cloud"
586
+ end
587
+
588
+ if @access_token && (@username || @password)
589
+ Umami.logger.warn "Both access token and credentials provided. Access token will be used."
590
+ @username = nil
591
+ @password = nil
592
+ end
593
+
594
+ if @uri_base != Umami::Configuration::UMAMI_CLOUD_URL && !@access_token && !@username && !@password
595
+ raise Umami::ConfigurationError, "Authentication is required for self-hosted instances"
596
+ end
597
+ end
598
+ end
599
+ end
@@ -0,0 +1,63 @@
1
+ module Umami
2
+ class Configuration
3
+ UMAMI_CLOUD_URL = "https://api.umami.is".freeze
4
+
5
+ attr_reader :uri_base, :request_timeout, :access_token, :username, :password
6
+
7
+ def initialize
8
+ @uri_base = nil
9
+ @request_timeout = 120
10
+ @access_token = nil
11
+ @username = nil
12
+ @password = nil
13
+ end
14
+
15
+ def uri_base=(url)
16
+ @uri_base = url&.chomp('/')
17
+ validate_configuration
18
+ end
19
+
20
+ def access_token=(token)
21
+ @access_token = token
22
+ @username = nil
23
+ @password = nil
24
+ validate_configuration
25
+ end
26
+
27
+ def credentials=(creds)
28
+ raise Umami::ConfigurationError, "Both username and password are required" unless creds[:username] && creds[:password]
29
+
30
+ @username = creds[:username]
31
+ @password = creds[:password]
32
+ @access_token = nil
33
+ validate_configuration
34
+ end
35
+
36
+ def cloud?
37
+ @access_token && @uri_base.nil?
38
+ end
39
+
40
+ private
41
+
42
+ def validate_configuration
43
+ if cloud?
44
+ @uri_base = UMAMI_CLOUD_URL
45
+ Umami.logger.info "Using Umami Cloud (#{UMAMI_CLOUD_URL})"
46
+ end
47
+
48
+ if @uri_base == UMAMI_CLOUD_URL && (@username || @password)
49
+ raise Umami::ConfigurationError, "Username/password authentication is not supported for Umami Cloud"
50
+ end
51
+
52
+ if @access_token && (@username || @password)
53
+ Umami.logger.warn "Both access token and credentials provided. Access token will be used."
54
+ @username = nil
55
+ @password = nil
56
+ end
57
+
58
+ if @uri_base && @uri_base != UMAMI_CLOUD_URL && !@access_token && !@username && !@password
59
+ raise Umami::ConfigurationError, "Authentication is required for self-hosted instances"
60
+ end
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,22 @@
1
+ module Umami
2
+ # Base error class for Umami-related errors
3
+ class Error < StandardError; end
4
+
5
+ # Error raised when there's a configuration issue
6
+ class ConfigurationError < Error; end
7
+
8
+ # Error raised when authentication fails
9
+ class AuthenticationError < Error; end
10
+
11
+ # Base error class for API-related errors
12
+ class APIError < Error; end
13
+
14
+ # Error raised when a resource is not found
15
+ class NotFoundError < APIError; end
16
+
17
+ # Error raised for client-side errors (4xx status codes)
18
+ class ClientError < APIError; end
19
+
20
+ # Error raised for server-side errors (5xx status codes)
21
+ class ServerError < APIError; end
22
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Umami
4
+ VERSION = "0.1.0"
5
+ end