devise-api 0.1.3 → 0.3.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.
data/README.md CHANGED
@@ -1,202 +1,388 @@
1
+ # devise-api
2
+
3
+ Token-based API authentication for [Devise](https://github.com/heartcombo/devise). Opaque access + refresh tokens, one `devise :api` module, zero Warden strategies to write.
4
+
1
5
  [![Gem Version](https://badge.fury.io/rb/devise-api.svg)](https://badge.fury.io/rb/devise-api)
2
6
  ![test](https://github.com/nejdetkadir/devise-api/actions/workflows/test.yml/badge.svg?branch=main)
3
7
  ![rubocop](https://github.com/nejdetkadir/devise-api/actions/workflows/rubocop.yml/badge.svg?branch=main)
4
8
  [![Ruby Style Guide](https://img.shields.io/badge/code_style-rubocop-brightgreen.svg)](https://github.com/rubocop/rubocop)
5
9
  ![Ruby Version](https://img.shields.io/badge/ruby_version->=_2.7.0-blue.svg)
10
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
11
+
12
+ `devise-api` is a Rails engine that plugs into Devise's own extension mechanism. Add `:api` to your Devise model and you get sign-up, sign-in, token refresh, revocation, and an authenticated info endpoint — plus controller helpers (`authenticate_devise_api_token!`, `current_devise_api_user`) for protecting the rest of your API.
13
+
14
+ **Highlights**
15
+
16
+ - 🔑 **Opaque access + refresh tokens** stored in your database — revocable at any time, no JWT invalidation headaches
17
+ - 🔁 **Refresh token rotation** with reuse detection (a replayed rotated token revokes the whole token family)
18
+ - 🧩 **Plays well with Devise modules** — `lockable`, `confirmable`, `trackable` are detected and honored automatically
19
+ - ⚙️ **Fully configurable** — token TTLs and generators, paranoid mode, header/params extraction, per-action callbacks, and swappable base classes for the token model and controller
20
+ - 🧱 **Service objects built on dry-monads** — every endpoint delegates to a composable, overridable service
21
+ - 📚 **Documented for humans and AI agents** — [`docs/`](docs/README.md) holds contractual architecture, API, and configuration references
22
+
23
+ ## Table of contents
24
+
25
+ - [How it works](#how-it-works)
26
+ - [Requirements](#requirements)
27
+ - [Quick start](#quick-start)
28
+ - [Endpoints](#endpoints)
29
+ - [Protecting your own endpoints](#protecting-your-own-endpoints)
30
+ - [Response payloads](#response-payloads)
31
+ - [Configuration](#configuration)
32
+ - [Security checklist](#security-checklist)
33
+ - [Devise module compatibility](#devise-module-compatibility)
34
+ - [Customization](#customization)
35
+ - [Documentation](#documentation)
36
+ - [Development](#development)
37
+ - [Contributing](#contributing)
38
+ - [License](#license)
39
+
40
+ ## How it works
41
+
42
+ A client signs in once, then uses a short-lived access token per request and a longer-lived refresh token to get new access tokens without re-sending credentials:
43
+
44
+ ```mermaid
45
+ sequenceDiagram
46
+ autonumber
47
+ participant Client
48
+ participant API as Your Rails API
49
+ participant DB as devise_api_tokens
50
+
51
+ Client->>API: POST /users/tokens/sign_in (email + password)
52
+ API->>DB: create token pair
53
+ API-->>Client: 200 { token, refresh_token, expires_in, resource_owner }
54
+
55
+ loop While access token is valid
56
+ Client->>API: GET /your/endpoints (Authorization: Bearer <access token>)
57
+ API-->>Client: 200 your data
58
+ end
59
+
60
+ Client->>API: GET /your/endpoints (expired access token)
61
+ API-->>Client: 401 { "error": "expired_token" }
62
+
63
+ Client->>API: POST /users/tokens/refresh (Authorization: Bearer <refresh token>)
64
+ API->>DB: mint new pair (rotation: revoke presented token)
65
+ API-->>Client: 200 { token, refresh_token, ... }
66
+
67
+ Client->>API: POST /users/tokens/revoke (Authorization: Bearer <access token>)
68
+ API->>DB: mark revoked
69
+ API-->>Client: 204 No Content
70
+ ```
6
71
 
7
- # Devise API
8
- The devise-api gem is a convenient way to add authentication to your Ruby on Rails application using the devise gem. It provides support for access tokens and refresh tokens, which allow you to authenticate API requests and keep the user's session active for a longer period of time on the client side. It can be installed by adding the gem to your Gemfile, running migrations, and adding the :api module to your devise model. The gem is fully configurable, allowing you to set things like token expiration times and token generators.
72
+ Tokens are opaque random strings (`Devise.friendly_token` by default) persisted in a `devise_api_tokens` table with a polymorphic `resource_owner`, so one table serves any number of Devise scopes (`User`, `Customer`, …). A token is **active** only while it is neither expired nor revoked:
73
+
74
+ ```mermaid
75
+ stateDiagram-v2
76
+ [*] --> Active: sign_up / sign_in / refresh
77
+ Active --> Expired: access_token.expires_in elapses
78
+ Active --> Revoked: POST /tokens/revoke
79
+ Active --> Revoked: rotation on refresh
80
+ Expired --> [*]: refresh (mints a new pair)
81
+ Revoked --> [*]
82
+ note right of Revoked
83
+ Reuse detection: presenting a rotated
84
+ refresh token again revokes the
85
+ entire token family
86
+ end note
87
+ ```
88
+
89
+ For the full component map and request lifecycle diagrams, see [docs/architecture.md](docs/architecture.md).
9
90
 
10
- Here's how it works:
91
+ ## Requirements
11
92
 
12
- - When a user logs in to your Rails application, the `devise-api` gem generates an access token and a refresh token.
13
- - The access token is included in the API request headers and is used to authenticate the user on each subsequent request.
14
- - The refresh token is stored on the client side (e.g. in a browser cookie or on a mobile device) and is used to obtain a new access token when the original access token expires.
15
- - This allows the user to remain logged in and make API requests without having to constantly re-enter their login credentials.
93
+ | Dependency | Version |
94
+ |---|---|
95
+ | Ruby | >= 2.7 |
96
+ | Rails | >= 6.0 |
97
+ | Devise | >= 4.7.2 |
16
98
 
17
- Overall, the `devise-api` gem is a useful tool for adding secure authentication to your Ruby on Rails application.
99
+ ## Quick start
18
100
 
19
- ## Installation
101
+ **1. Install the gem**
20
102
 
21
- Install the gem and add to the application's Gemfile by executing:
22
103
  ```bash
23
- $ bundle add devise-api
104
+ bundle add devise-api
24
105
  ```
25
106
 
26
- Or add the following line to the application's Gemfile:
107
+ Or track `main` from your Gemfile:
108
+
27
109
  ```ruby
28
110
  gem 'devise-api', github: 'nejdetkadir/devise-api', branch: 'main'
29
111
  ```
30
112
 
31
- If bundler is not being used to manage dependencies, install the gem by executing:
32
- ```bash
33
- gem install devise-api
34
- ```
113
+ **2. Generate the migration and locales**
35
114
 
36
- After that, you need to generate relevant migrations and locales by executing:
37
115
  ```bash
38
- $ rails generate devise_api:install
116
+ rails generate devise_api:install
117
+ rails db:migrate
39
118
  ```
40
119
 
41
- This will introduce two changes:
42
- - Locale files in `config/locales/devise_api.en.yml`
43
- - Migration file in `db/migrate` to create devise api tokens table
120
+ This copies a migration for the `devise_api_tokens` table and the locale file `config/locales/devise_api.en.yml` into your app.
44
121
 
45
- Now you're ready to run the migrations:
46
- ```bash
47
- $ rails db:migrate
48
- ```
122
+ **3. Add the `:api` module to your Devise model**
49
123
 
50
- Finally, you need to add `:api` module to your devise model. For example:
51
124
  ```ruby
52
125
  class User < ApplicationRecord
53
- devise :database_authenticatable,
54
- :registerable,
126
+ devise :database_authenticatable,
127
+ :registerable,
55
128
  :recoverable,
56
129
  :rememberable,
57
130
  :validatable,
58
- :api # <--- Add this module
131
+ :api # <--- add this
132
+ end
133
+ ```
134
+
135
+ That's it — your existing `devise_for :users` in `config/routes.rb` now draws the token endpoints automatically.
136
+
137
+ **4. Try it**
138
+
139
+ ```bash
140
+ curl -X POST http://localhost:3000/users/tokens/sign_in \
141
+ -H 'Content-Type: application/json' \
142
+ -d '{ "email": "test@example.com", "password": "123456" }'
143
+ ```
144
+
145
+ ```json
146
+ {
147
+ "token": "ACCESS_TOKEN",
148
+ "refresh_token": "REFRESH_TOKEN",
149
+ "expires_in": 3600,
150
+ "token_type": "Bearer",
151
+ "resource_owner": { "id": 1, "email": "test@example.com", "created_at": "...", "updated_at": "..." }
152
+ }
153
+ ```
154
+
155
+ ## Endpoints
156
+
157
+ Drawn under `/<scope>/tokens` for every Devise scope whose model includes `:api` (examples use `devise_for :users`):
158
+
159
+ | Verb | Path | Purpose | Auth |
160
+ |---|---|---|---|
161
+ | `POST` | `/users/tokens/sign_up` | Register and get a token pair | — |
162
+ | `POST` | `/users/tokens/sign_in` | Authenticate and get a token pair | — |
163
+ | `POST` | `/users/tokens/refresh` | Exchange a refresh token for a new pair | refresh token |
164
+ | `POST` | `/users/tokens/revoke` | Revoke the presented token | access token |
165
+ | `GET` | `/users/tokens/info` | Current resource owner details | access token |
166
+
167
+ All tokens — including the refresh token for `/refresh` — travel in the same slot: the `Authorization: Bearer <token>` header and/or an `access_token` param, depending on `authorization.location` (see [Configuration](#configuration)).
168
+
169
+ ```bash
170
+ # Sign up
171
+ curl -X POST http://localhost:3000/users/tokens/sign_up \
172
+ -H 'Content-Type: application/json' \
173
+ -d '{ "email": "test@example.com", "password": "123456" }'
174
+
175
+ # Refresh (note: the REFRESH token goes in the Authorization header)
176
+ curl -X POST http://localhost:3000/users/tokens/refresh \
177
+ -H 'Authorization: Bearer REFRESH_TOKEN'
178
+
179
+ # Revoke
180
+ curl -X POST http://localhost:3000/users/tokens/revoke \
181
+ -H 'Authorization: Bearer ACCESS_TOKEN'
182
+
183
+ # Info
184
+ curl http://localhost:3000/users/tokens/info \
185
+ -H 'Authorization: Bearer ACCESS_TOKEN'
186
+ ```
187
+
188
+ Route paths and the controller are customizable through the standard `devise_for` options:
189
+
190
+ ```ruby
191
+ # config/routes.rb
192
+ Rails.application.routes.draw do
193
+ devise_for :customers, controllers: { tokens: 'customers/api/tokens' }
194
+ end
195
+ ```
196
+
197
+ ## Protecting your own endpoints
198
+
199
+ The gem mixes three helpers into **every** controller:
200
+
201
+ | Helper | Returns |
202
+ |---|---|
203
+ | `authenticate_devise_api_token!` | Renders a 401 error response unless a valid, active access token is presented |
204
+ | `current_devise_api_token` | The active `Devise::Api::Token` (or `nil`) |
205
+ | `current_devise_api_user` | The token's resource owner (or `nil`) — works for any scope, despite the name |
206
+
207
+ ```ruby
208
+ # app/controllers/api/v1/orders_controller.rb
209
+ class Api::V1::OrdersController < ApplicationController
210
+ skip_before_action :verify_authenticity_token, raise: false
211
+ before_action :authenticate_devise_api_token!
212
+
213
+ def index
214
+ render json: current_devise_api_user.orders, status: :ok
215
+ end
216
+
217
+ def show
218
+ order = current_devise_api_token.resource_owner.orders.find(params[:id])
219
+ render json: order, status: :ok
220
+ end
59
221
  end
60
222
  ```
61
223
 
62
- Your user model is now ready to use `devise-api` gem. It will draw routes for token authenticatable and token refreshable.
224
+ ## Response payloads
225
+
226
+ **Success** (`sign_in` 200, `sign_up` 201, `refresh` 200):
227
+
228
+ ```json
229
+ {
230
+ "token": "...",
231
+ "refresh_token": "...",
232
+ "expires_in": 3600,
233
+ "token_type": "Bearer",
234
+ "resource_owner": { "id": 1, "email": "...", "created_at": "...", "updated_at": "..." }
235
+ }
236
+ ```
237
+
238
+ `info` returns just the `resource_owner` object; `revoke` returns `204 No Content`.
63
239
 
64
- | Prefix | Verb | URI Pattern | Controller#Action |
65
- |--------|------|------------|--------------------------|
66
- | revoke_user_tokens | POST | /users/tokens/revoke | devise/api/tokens#revoke |
67
- | refresh_user_tokens | POST | /users/tokens/refresh | devise/api/tokens#refresh |
68
- | sign_up_user_tokens | POST | /users/tokens/sign_up | devise/api/tokens#sign_up |
69
- | sign_in_user_tokens | POST | /users/tokens/sign_in | devise/api/tokens#sign_in |
70
- | info_user_tokens | GET | /users/tokens/info | devise/api/tokens#info |
240
+ **Errors** are consistent JSON with a symbolic type and human-readable descriptions (translated via i18n):
71
241
 
72
- ### You can look up the [example requests](#example-api-requests).
242
+ ```json
243
+ {
244
+ "error": "expired_token",
245
+ "error_description": ["Your token has expired. Please sign in again."]
246
+ }
247
+ ```
248
+
249
+ Common error types: `invalid_authentication` (401), `invalid_token` (401), `expired_token` (401), `revoked_token` (401), `expired_refresh_token` (401), `invalid_refresh_token` (400), `sign_up_disabled` (400), `resource_owner_create_error` (422). The complete catalog — every type, status, and trigger — lives in [docs/api-reference.md](docs/api-reference.md).
73
250
 
74
251
  ## Configuration
75
252
 
76
- `devise-api` is a full configurable gem. You can configure it to your needs. Here is a basic usage example:
253
+ Everything is configured on a single global inside `Devise.setup`. All values shown are the defaults:
77
254
 
78
255
  ```ruby
79
256
  # config/initializers/devise.rb
80
257
  Devise.setup do |config|
81
258
  config.api.configure do |api|
82
- # Access Token
259
+ # Access token
83
260
  api.access_token.expires_in = 1.hour
84
261
  api.access_token.expires_in_infinite = ->(_resource_owner) { false }
85
262
  api.access_token.generator = ->(_resource_owner) { Devise.friendly_token(60) }
86
263
 
87
-
88
- # Refresh Token
264
+ # Refresh token
89
265
  api.refresh_token.enabled = true
90
266
  api.refresh_token.expires_in = 1.week
91
- api.refresh_token.generator = ->(_resource_owner) { Devise.friendly_token(60) }
92
267
  api.refresh_token.expires_in_infinite = ->(_resource_owner) { false }
268
+ api.refresh_token.generator = ->(_resource_owner) { Devise.friendly_token(60) }
269
+ api.refresh_token.rotation_enabled = false # recommended: true (see Security checklist)
93
270
 
94
271
  # Sign up
95
272
  api.sign_up.enabled = true
273
+ api.sign_up.extra_fields = [] # e.g. %i[first_name last_name] — writable at sign-up AND echoed in responses
274
+
275
+ # Error responses
276
+ api.error_response.verbose_account_state = true # false hides lockable/confirmable details from errors
277
+ api.paranoid = false # true makes unknown accounts indistinguishable from wrong passwords
96
278
 
97
- # Authorization
279
+ # Token extraction
98
280
  api.authorization.key = 'Authorization'
99
281
  api.authorization.scheme = 'Bearer'
100
- api.authorization.location = :both # :header or :params or :both
282
+ api.authorization.location = :both # :header, :params, or :both (params win)
101
283
  api.authorization.params_key = 'access_token'
102
284
 
103
-
104
- # Base classes
285
+ # Base classes (string names, constantized lazily — point at your own subclasses)
105
286
  api.base_token_model = 'Devise::Api::Token'
106
287
  api.base_controller = '::DeviseController'
107
288
 
108
-
109
- # After successful callbacks
110
- api.after_successful_sign_in = ->(_resource_owner, _token, _request) { }
111
- api.after_successful_sign_up = ->(_resource_owner, _token, _request) { }
112
- api.after_successful_refresh = ->(_resource_owner, _token, _request) { }
113
- api.after_successful_revoke = ->(_resource_owner, _token, _request) { }
114
-
115
-
116
- # Before callbacks
117
- api.before_sign_in = ->(_params, _request, _resource_class) { }
118
- api.before_sign_up = ->(_params, _request, _resource_class) { }
119
- api.before_refresh = ->(_params, _request, _resource_class) { }
120
- api.before_revoke = ->(_params, _request, _resource_class) { }
289
+ # Lifecycle hooks (all default to no-ops)
290
+ api.before_sign_in = ->(params, request, resource_class) {}
291
+ api.before_sign_up = ->(params, request, resource_class) {}
292
+ api.before_refresh = ->(token, request) {}
293
+ api.before_revoke = ->(token, request) {}
294
+ api.after_successful_sign_in = ->(resource_owner, token, request) {}
295
+ api.after_successful_sign_up = ->(resource_owner, token, request) {}
296
+ api.after_successful_refresh = ->(resource_owner, token, request) {}
297
+ api.after_successful_revoke = ->(resource_owner, token, request) {}
121
298
  end
122
299
  end
123
300
  ```
124
301
 
125
- ## Routes
302
+ Settings are read at use time (never cached at boot), so changes take effect immediately — handy in tests. The full reference with types, defaults, and exactly which code consumes each setting is in [docs/configuration.md](docs/configuration.md).
126
303
 
127
- You can configure the tokens routes with the orginally `devise_for` method. For example:
128
- ```ruby
129
- # config/routes.rb
130
- Rails.application.routes.draw do
131
- devise_for :customers,
132
- controllers: { tokens: 'customers/api/tokens' }
133
- end
134
- ```
304
+ ## Security checklist
305
+
306
+ Recommended production settings and guardrails:
307
+
308
+ - ✅ **Send tokens in the `Authorization` header only.** The default `authorization.location = :both` also accepts tokens as query/body params, and URLs leak into server logs, browser history, and `Referer` headers. Set `api.authorization.location = :header` unless you need params support.
309
+ - **Enable refresh token rotation** (`api.refresh_token.rotation_enabled = true`). Each refresh then revokes the presented refresh token, and replaying a rotated token revokes the entire token family (reuse detection).
310
+ - ✅ **Enable paranoid mode** (`api.paranoid = true`) to prevent account enumeration — unknown emails and wrong passwords return the same generic `invalid_authentication` error.
311
+ - ✅ **Rate limit the token endpoints.** The gem does not throttle `sign_in`/`sign_up`/`refresh`; put [rack-attack](https://github.com/rack/rack-attack) or an equivalent in front of them. Devise `lockable` only slows per-account brute force.
312
+ - ⚠️ **Audit `sign_up.extra_fields`.** Every listed field is mass-assignable at sign-up **and** echoed in every token/info response — never list privileged fields like `:role` or `:admin`.
313
+ - ⚠️ **Keep token values out of logs.** The gem adds `access_token`, `refresh_token`, and `previous_refresh_token` to `filter_parameters` and filters the token model's `#inspect`, but raw SQL logging (e.g. debug log level in production) can still print token values.
314
+
315
+ The full threat-model review is in [docs/analysis/security-review.md](docs/analysis/security-review.md).
135
316
 
136
- ## Usage
137
- `devise-api` module works with `:lockable` and `:confirmable` modules. It also works with `:trackable` module.
317
+ ## Devise module compatibility
138
318
 
139
- `devise-api` provides a set of controllers and helpers to help you implement authentication in your Rails application. Here's a quick overview of the available controllers and helpers:
319
+ `devise-api` feature-detects the other modules on your model and adapts:
140
320
 
141
- - [Devise::Api::TokensController](https://github.com/nejdetkadir/devise-api/tree/main/app/controllers/devise/api/tokens_controller.rb) - This controller is responsible for generating access tokens and refresh tokens. It also provides actions for refreshing access tokens and revoking refresh tokens.
321
+ | Module | Behavior |
322
+ |---|---|
323
+ | `trackable` | `sign_in`/`sign_up` update the tracked fields (sign-in count, IPs, timestamps) |
324
+ | `lockable` | Failed sign-ins increment `failed_attempts`; lock state is reported in the error payload (unless paranoid/quiet); a successful sign-in resets the counter |
325
+ | `confirmable` | Unconfirmed users can sign **up** (they get tokens plus a `confirmable` notice in the response) but cannot sign **in** until confirmed |
142
326
 
143
- - [Devise::Api::Token](https://github.com/nejdetkadir/devise-api/tree/main/lib/devise/api/token.rb) - This model is responsible for storing access tokens and refresh tokens in the database.
327
+ ## Customization
144
328
 
145
- - [Devise::Api::Responses::ErrorResponse](https://github.com/nejdetkadir/devise-api/tree/main/lib/devise/api/responses/error_response.rb) - This class is responsible for generating error responses. It also provides a set of error types and helpers to help you implement error responses in your Rails application.
329
+ ### Override the responses
146
330
 
147
- - [Devise::Api::Responses::TokenResponse](https://github.com/nejdetkadir/devise-api/tree/main/lib/devise/api/responses/token_response.rb) - This class is responsible for generating token responses. It also provides actions for generating access tokens and refresh tokens for each action.
331
+ Prepend a decorator module to `TokenResponse` or `ErrorResponse`:
148
332
 
149
- ## Overriding Responses
150
- You can prepend your decorators to the response classes to override the default responses. For example:
151
333
  ```ruby
152
334
  # app/lib/devise/api/responses/token_response_decorator.rb
153
335
  module Devise::Api::Responses::TokenResponseDecorator
154
336
  def body
155
- return default_body.merge({ roles: resource_owner.roles })
337
+ default_body.merge({ roles: resource_owner.roles })
156
338
  end
157
339
  end
158
340
  ```
159
341
 
160
- Then you need to prepend your decorator to the response class. For example:
161
-
162
342
  ```ruby
163
343
  # config/initializers/devise.rb
164
- Devise.setup do |config|
165
- end
344
+ require 'devise/api/responses/token_response_decorator'
166
345
 
167
346
  Devise::Api::Responses::TokenResponse.prepend Devise::Api::Responses::TokenResponseDecorator
168
347
  ```
169
348
 
170
- ## Using helpers
171
- `devise-api` provides a set of helpers to help you implement authentication in your Rails application. Here's a quick overview of the available helpers:
349
+ ### Swap the base classes
172
350
 
173
- Example:
174
- ```ruby
175
- # app/controllers/api/v1/orders_controller.rb
176
- class Api::V1::OrdersController < YourBaseController
177
- skip_before_action :verify_authenticity_token, raise: false
178
- before_action :authenticate_devise_api_token!
351
+ `base_token_model` and `base_controller` are stored as class *names* and resolved lazily, so you can subclass without load-order problems:
179
352
 
180
- def index
181
- render json: current_devise_api_user.orders, status: :ok
182
- end
353
+ ```ruby
354
+ # app/models/api_token.rb
355
+ class ApiToken < Devise::Api::Token
356
+ belongs_to :organization, optional: true
357
+ end
358
+ ```
183
359
 
184
- def show
185
- devise_api_token = current_devise_api_token
186
- render json: devise_api_token.resource_owner.orders.find(params[:id]), status: :ok
360
+ ```ruby
361
+ # config/initializers/devise.rb
362
+ Devise.setup do |config|
363
+ config.api.configure do |api|
364
+ api.base_token_model = 'ApiToken'
365
+ api.base_controller = 'Api::BaseController'
187
366
  end
188
367
  end
189
368
  ```
190
369
 
191
- ## Using devise base services
192
- `devise-api` provides a set of base services to help you implement authentication in your Rails application. Here's a quick overview of the available services:
370
+ ### Hook into the lifecycle
193
371
 
194
- - [Devise::Api::BaseService](https://github.com/nejdetkadir/devise-api/tree/main/app/services/devise/api/base_service.rb) - This service is useful for creating and updating resources. It is inherited by the following gems.
195
- - [dry-monads](https://dry-rb.org/gems/dry-monads)
196
- - [dry-types](https://dry-rb.org/gems/dry-types)
197
- - [dry-initializer](https://dry-rb.org/gems/dry-initializer)
372
+ The `before_*` / `after_successful_*` callbacks (see [Configuration](#configuration)) are handy for audit logging, analytics, or sending welcome emails:
373
+
374
+ ```ruby
375
+ api.after_successful_sign_up = lambda { |resource_owner, _token, _request|
376
+ WelcomeMailer.with(user: resource_owner).welcome.deliver_later
377
+ }
378
+ ```
379
+
380
+ `before_*` return values are ignored — raise, or use a `before_action` in a subclassed controller, if you need to halt a request.
381
+
382
+ ### Build your own services
383
+
384
+ Every endpoint delegates to a service object built on [dry-monads](https://dry-rb.org/gems/dry-monads), [dry-types](https://dry-rb.org/gems/dry-types), and [dry-initializer](https://dry-rb.org/gems/dry-initializer). Inherit from `Devise::Api::BaseService` to compose your own:
198
385
 
199
- You can create a service by inheriting the `Devise::Api::BaseService` class. For example:
200
386
  ```ruby
201
387
  # app/services/devise/api/tokens_service/v2/create.rb
202
388
  module Devise::Api::TokensService::V2
@@ -205,85 +391,60 @@ module Devise::Api::TokensService::V2
205
391
  option :resource_class, type: Types::Class, reader: true
206
392
 
207
393
  def call
208
- ...
209
-
394
+ # ...
210
395
  Success(resource)
211
396
  end
212
397
  end
213
398
  end
214
399
  ```
215
400
 
216
- Then you can call the service in your controller. For example:
217
401
  ```ruby
218
402
  # app/controllers/api/v1/tokens_controller.rb
219
- class Api::V1::TokensController < YourBaseController
220
- skip_before_action :verify_authenticity_token, raise: false
403
+ def create
404
+ result = Devise::Api::TokensService::V2::Create.new(params: params, resource_class: Customer).call
221
405
 
222
- def create
223
- service = Devise::Api::TokensService::V2::Create.call(params: params, resource_class: Customer || resource_class)
224
- if service.success?
225
- render json: service.success, status: :created
226
- else
227
- render json: service.failure, status: :unprocessable_entity
228
- end
406
+ if result.success?
407
+ render json: result.success, status: :created
408
+ else
409
+ render json: result.failure, status: :unprocessable_entity
229
410
  end
230
411
  end
231
412
  ```
232
413
 
233
- ## Example API requests
234
-
235
- ### Sign in
236
- ```curl
237
- curl --location --request POST 'http://127.0.0.1:3000/users/tokens/sign_in' \
238
- --header 'Content-Type: application/json' \
239
- --data-raw '{
240
- "email": "test@development.com",
241
- "password": "123456"
242
- }'
243
- ```
414
+ Service contracts (inputs, success/failure values, composition) are documented in [docs/services.md](docs/services.md), and all supported customization points in [docs/extending.md](docs/extending.md).
244
415
 
245
- ### Sign up
246
- ```curl
247
- curl --location --request POST 'http://127.0.0.1:3000/users/tokens/sign_up' \
248
- --header 'Content-Type: application/json' \
249
- --data-raw '{
250
- "email": "test@development.com",
251
- "password": "123456"
252
- }'
253
- ```
254
-
255
- ### Refresh token
256
- ```curl
257
- curl --location --request POST 'http://127.0.0.1:3000/users/tokens/refresh' \
258
- --header 'Authorization: Bearer <refresh_token>'
259
- ```
416
+ ## Documentation
260
417
 
261
- ### Revoke
262
- ```curl
263
- curl --location --request POST 'http://127.0.0.1:3000/users/tokens/revoke' \
264
- --header 'Authorization: Bearer <access_token>'
265
- ```
418
+ The [`docs/`](docs/README.md) directory is the source of truth for how the gem works internally — written for contributors **and** AI coding agents, and kept in sync with the code by convention:
266
419
 
267
- ### Info
268
- ```curl
269
- curl --location --request GET 'http://127.0.0.1:3000/users/tokens/info' \
270
- --header 'Authorization: Bearer <access_token>'
271
- ```
420
+ | Document | Contents |
421
+ |---|---|
422
+ | [architecture.md](docs/architecture.md) | Component map, boot sequence, request lifecycle (with diagrams) |
423
+ | [api-reference.md](docs/api-reference.md) | Every endpoint, payload, and the full error catalog |
424
+ | [configuration.md](docs/configuration.md) | Every setting: type, default, and where it is consumed |
425
+ | [data-model.md](docs/data-model.md) | `devise_api_tokens` schema, token state machine, refresh chains |
426
+ | [services.md](docs/services.md) | Service-object contracts and composition |
427
+ | [extending.md](docs/extending.md) | Supported customization points |
428
+ | [testing.md](docs/testing.md) / [development.md](docs/development.md) | Test layout, dummy app, CI, release process |
429
+ | [analysis/](docs/analysis/security-review.md) | Security review and vetted known-issues backlog |
272
430
 
273
431
  ## Development
274
432
 
275
- After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake rspec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
433
+ ```bash
434
+ bin/setup # install dependencies
435
+ bundle exec rake # what CI runs: RSpec + RuboCop
436
+ bundle exec rake rspec # tests only
437
+ bin/console # interactive prompt
438
+ ```
276
439
 
277
- To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
440
+ Tests run against the dummy Rails app in `spec/dummy`. To install the gem locally run `bundle exec rake install`; to release, bump `version.rb` and run `bundle exec rake release`.
278
441
 
279
442
  ## Contributing
280
443
 
281
- Bug reports and pull requests are welcome on GitHub at https://github.com/nejdetkadir/devise-api. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [code of conduct](https://github.com/nejdetkadir/devise-api/blob/main/CODE_OF_CONDUCT.md).
444
+ Bug reports and pull requests are welcome on [GitHub](https://github.com/nejdetkadir/devise-api). Please read [docs/README.md](docs/README.md) for the ground rules (docs are contractual behavior changes must update the matching document) and check the [known-issues backlog](docs/analysis/known-issues.md) before "fixing" surprising code.
445
+
446
+ This project is intended to be a safe, welcoming space for collaboration; contributors are expected to follow the [code of conduct](CODE_OF_CONDUCT.md).
282
447
 
283
448
  ## License
284
449
 
285
450
  The gem is available as open source under the terms of the [MIT License](LICENSE).
286
-
287
- ## Code of Conduct
288
-
289
- Everyone interacting in the Devise::Api project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/nejdetkadir/devise-api/blob/main/CODE_OF_CONDUCT.md).
data/Rakefile CHANGED
@@ -5,6 +5,12 @@ require 'rspec/core/rake_task'
5
5
 
6
6
  RSpec::Core::RakeTask.new(:rspec)
7
7
 
8
+ # Full-suite runs enforce the SimpleCov minimum; single-file `rspec` runs do not.
9
+ task :enforce_coverage do
10
+ ENV['ENFORCE_COVERAGE'] = '1'
11
+ end
12
+ task rspec: :enforce_coverage
13
+
8
14
  require 'rubocop/rake_task'
9
15
 
10
16
  RuboCop::RakeTask.new