devise-api 0.2.0 → 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,205 +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" }
6
62
 
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.
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
+ ```
71
+
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
+ ```
9
88
 
10
- Here's how it works:
89
+ For the full component map and request lifecycle diagrams, see [docs/architecture.md](docs/architecture.md).
11
90
 
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.
91
+ ## Requirements
16
92
 
17
- Overall, the `devise-api` gem is a useful tool for adding secure authentication to your Ruby on Rails application.
93
+ | Dependency | Version |
94
+ |---|---|
95
+ | Ruby | >= 2.7 |
96
+ | Rails | >= 6.0 |
97
+ | Devise | >= 4.7.2 |
18
98
 
19
- ## Installation
99
+ ## Quick start
100
+
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
59
132
  end
60
133
  ```
61
134
 
62
- Your user model is now ready to use `devise-api` gem. It will draw routes for token authenticatable and token refreshable.
135
+ That's it your existing `devise_for :users` in `config/routes.rb` now draws the token endpoints automatically.
63
136
 
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 |
137
+ **4. Try it**
71
138
 
72
- ### You can look up the [example requests](#example-api-requests).
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
221
+ end
222
+ ```
223
+
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`.
239
+
240
+ **Errors** are consistent JSON with a symbolic type and human-readable descriptions (translated via i18n):
241
+
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
96
- api.sign_up.extra_fields = []
273
+ api.sign_up.extra_fields = [] # e.g. %i[first_name last_name] — writable at sign-up AND echoed in responses
97
274
 
98
- # Authorization
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
278
+
279
+ # Token extraction
99
280
  api.authorization.key = 'Authorization'
100
281
  api.authorization.scheme = 'Bearer'
101
- api.authorization.location = :both # :header or :params or :both
282
+ api.authorization.location = :both # :header, :params, or :both (params win)
102
283
  api.authorization.params_key = 'access_token'
103
284
 
104
-
105
- # Base classes
285
+ # Base classes (string names, constantized lazily — point at your own subclasses)
106
286
  api.base_token_model = 'Devise::Api::Token'
107
287
  api.base_controller = '::DeviseController'
108
288
 
109
-
110
- # After successful callbacks
111
- api.after_successful_sign_in = ->(_resource_owner, _token, _request) { }
112
- api.after_successful_sign_up = ->(_resource_owner, _token, _request) { }
113
- api.after_successful_refresh = ->(_resource_owner, _token, _request) { }
114
- api.after_successful_revoke = ->(_resource_owner, _token, _request) { }
115
-
116
-
117
- # Before callbacks
118
- api.before_sign_in = ->(_params, _request, _resource_class) { }
119
- api.before_sign_up = ->(_params, _request, _resource_class) { }
120
- api.before_refresh = ->(_token_model, _request) { }
121
- api.before_revoke = ->(_token_model, _request) { }
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) {}
122
298
  end
123
299
  end
124
300
  ```
125
301
 
126
- ## 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).
127
303
 
128
- You can configure the tokens routes with the orginally `devise_for` method. For example:
129
- ```ruby
130
- # config/routes.rb
131
- Rails.application.routes.draw do
132
- devise_for :customers,
133
- controllers: { tokens: 'customers/api/tokens' }
134
- end
135
- ```
304
+ ## Security checklist
136
305
 
137
- ## Usage
138
- `devise-api` module works with `:lockable` and `:confirmable` modules. It also works with `:trackable` module.
306
+ Recommended production settings and guardrails:
139
307
 
140
- `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:
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.
141
314
 
142
- - [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.
315
+ The full threat-model review is in [docs/analysis/security-review.md](docs/analysis/security-review.md).
143
316
 
144
- - [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.
317
+ ## Devise module compatibility
145
318
 
146
- - [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.
319
+ `devise-api` feature-detects the other modules on your model and adapts:
147
320
 
148
- - [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.
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 |
326
+
327
+ ## Customization
328
+
329
+ ### Override the responses
330
+
331
+ Prepend a decorator module to `TokenResponse` or `ErrorResponse`:
149
332
 
150
- ## Overriding Responses
151
- You can prepend your decorators to the response classes to override the default responses. For example:
152
333
  ```ruby
153
334
  # app/lib/devise/api/responses/token_response_decorator.rb
154
335
  module Devise::Api::Responses::TokenResponseDecorator
155
336
  def body
156
- return default_body.merge({ roles: resource_owner.roles })
337
+ default_body.merge({ roles: resource_owner.roles })
157
338
  end
158
339
  end
159
340
  ```
160
341
 
161
- Then you need to load and prepend your decorator to the response class. For example:
162
-
163
342
  ```ruby
164
343
  # config/initializers/devise.rb
165
- require 'devise/api/responses/token_response_decorator' # Either do this or autoload the lib directory
166
-
167
- Devise.setup do |config|
168
- end
344
+ require 'devise/api/responses/token_response_decorator'
169
345
 
170
346
  Devise::Api::Responses::TokenResponse.prepend Devise::Api::Responses::TokenResponseDecorator
171
347
  ```
172
348
 
173
- ## Using helpers
174
- `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
175
350
 
176
- Example:
177
- ```ruby
178
- # app/controllers/api/v1/orders_controller.rb
179
- class Api::V1::OrdersController < YourBaseController
180
- skip_before_action :verify_authenticity_token, raise: false
181
- 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:
182
352
 
183
- def index
184
- render json: current_devise_api_user.orders, status: :ok
185
- end
353
+ ```ruby
354
+ # app/models/api_token.rb
355
+ class ApiToken < Devise::Api::Token
356
+ belongs_to :organization, optional: true
357
+ end
358
+ ```
186
359
 
187
- def show
188
- devise_api_token = current_devise_api_token
189
- 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'
190
366
  end
191
367
  end
192
368
  ```
193
369
 
194
- ## Using devise base services
195
- `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
196
371
 
197
- - [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.
198
- - [dry-monads](https://dry-rb.org/gems/dry-monads)
199
- - [dry-types](https://dry-rb.org/gems/dry-types)
200
- - [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:
201
385
 
202
- You can create a service by inheriting the `Devise::Api::BaseService` class. For example:
203
386
  ```ruby
204
387
  # app/services/devise/api/tokens_service/v2/create.rb
205
388
  module Devise::Api::TokensService::V2
@@ -208,85 +391,60 @@ module Devise::Api::TokensService::V2
208
391
  option :resource_class, type: Types::Class, reader: true
209
392
 
210
393
  def call
211
- ...
212
-
394
+ # ...
213
395
  Success(resource)
214
396
  end
215
397
  end
216
398
  end
217
399
  ```
218
400
 
219
- Then you can call the service in your controller. For example:
220
401
  ```ruby
221
402
  # app/controllers/api/v1/tokens_controller.rb
222
- class Api::V1::TokensController < YourBaseController
223
- 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
224
405
 
225
- def create
226
- service = Devise::Api::TokensService::V2::Create.call(params: params, resource_class: Customer || resource_class)
227
- if service.success?
228
- render json: service.success, status: :created
229
- else
230
- render json: service.failure, status: :unprocessable_entity
231
- end
406
+ if result.success?
407
+ render json: result.success, status: :created
408
+ else
409
+ render json: result.failure, status: :unprocessable_entity
232
410
  end
233
411
  end
234
412
  ```
235
413
 
236
- ## Example API requests
237
-
238
- ### Sign in
239
- ```curl
240
- curl --location --request POST 'http://127.0.0.1:3000/users/tokens/sign_in' \
241
- --header 'Content-Type: application/json' \
242
- --data-raw '{
243
- "email": "test@development.com",
244
- "password": "123456"
245
- }'
246
- ```
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).
247
415
 
248
- ### Sign up
249
- ```curl
250
- curl --location --request POST 'http://127.0.0.1:3000/users/tokens/sign_up' \
251
- --header 'Content-Type: application/json' \
252
- --data-raw '{
253
- "email": "test@development.com",
254
- "password": "123456"
255
- }'
256
- ```
257
-
258
- ### Refresh token
259
- ```curl
260
- curl --location --request POST 'http://127.0.0.1:3000/users/tokens/refresh' \
261
- --header 'Authorization: Bearer <refresh_token>'
262
- ```
416
+ ## Documentation
263
417
 
264
- ### Revoke
265
- ```curl
266
- curl --location --request POST 'http://127.0.0.1:3000/users/tokens/revoke' \
267
- --header 'Authorization: Bearer <access_token>'
268
- ```
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:
269
419
 
270
- ### Info
271
- ```curl
272
- curl --location --request GET 'http://127.0.0.1:3000/users/tokens/info' \
273
- --header 'Authorization: Bearer <access_token>'
274
- ```
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 |
275
430
 
276
431
  ## Development
277
432
 
278
- 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
+ ```
279
439
 
280
- 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`.
281
441
 
282
442
  ## Contributing
283
443
 
284
- 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).
285
447
 
286
448
  ## License
287
449
 
288
450
  The gem is available as open source under the terms of the [MIT License](LICENSE).
289
-
290
- ## Code of Conduct
291
-
292
- 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