umami-ruby 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,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