propel_authentication 0.1.4 → 0.2.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +43 -2
- data/README.md +6 -6
- data/lib/generators/propel_authentication/install_generator.rb +135 -153
- data/lib/generators/propel_authentication/templates/application_mailer.rb +6 -0
- data/lib/generators/propel_authentication/templates/auth/passwords_controller.rb.tt +84 -78
- data/lib/generators/propel_authentication/templates/auth/signup_controller.rb.tt +242 -0
- data/lib/generators/propel_authentication/templates/{tokens_controller.rb.tt → auth/tokens_controller.rb.tt} +39 -22
- data/lib/generators/propel_authentication/templates/auth_mailer.rb +3 -1
- data/lib/generators/propel_authentication/templates/authenticatable.rb +8 -2
- data/lib/generators/propel_authentication/templates/concerns/confirmable.rb +1 -1
- data/lib/generators/propel_authentication/templates/concerns/lockable.rb +4 -2
- data/lib/generators/propel_authentication/templates/concerns/{propel_authentication.rb → propel_authentication_concern.rb} +33 -3
- data/lib/generators/propel_authentication/templates/concerns/recoverable.rb +16 -6
- data/lib/generators/propel_authentication/templates/core/configuration_methods.rb +104 -64
- data/lib/generators/propel_authentication/templates/db/seeds.rb +50 -4
- data/lib/generators/propel_authentication/templates/doc/signup_flow.md +315 -0
- data/lib/generators/propel_authentication/templates/models/agency.rb.tt +13 -0
- data/lib/generators/propel_authentication/templates/models/agent.rb.tt +13 -0
- data/lib/generators/propel_authentication/templates/{invitation.rb → models/invitation.rb.tt} +6 -0
- data/lib/generators/propel_authentication/templates/models/organization.rb.tt +12 -0
- data/lib/generators/propel_authentication/templates/{user.rb → models/user.rb.tt} +5 -0
- data/lib/generators/propel_authentication/templates/propel_authentication.rb.tt +94 -9
- data/lib/generators/propel_authentication/templates/routes/auth_routes.rb.tt +55 -0
- data/lib/generators/propel_authentication/templates/services/auth_notification_service.rb +3 -3
- data/lib/generators/propel_authentication/templates/test/concerns/confirmable_test.rb.tt +34 -10
- data/lib/generators/propel_authentication/templates/test/concerns/propel_authentication_test.rb.tt +1 -1
- data/lib/generators/propel_authentication/templates/test/concerns/recoverable_test.rb.tt +4 -4
- data/lib/generators/propel_authentication/templates/test/controllers/auth/lockable_integration_test.rb.tt +18 -15
- data/lib/generators/propel_authentication/templates/test/controllers/auth/password_reset_integration_test.rb.tt +38 -40
- data/lib/generators/propel_authentication/templates/test/controllers/auth/signup_controller_test.rb.tt +201 -0
- data/lib/generators/propel_authentication/templates/test/controllers/auth/tokens_controller_test.rb.tt +33 -25
- data/lib/generators/propel_authentication/templates/test/mailers/auth_mailer_test.rb.tt +51 -36
- data/lib/generators/propel_authentication/templates/views/auth_mailer/email_confirmation.html.erb +2 -2
- data/lib/generators/propel_authentication/templates/views/auth_mailer/email_confirmation.text.erb +1 -1
- data/lib/generators/propel_authentication/test/generators/authentication/install_generator_test.rb +4 -4
- data/lib/generators/propel_authentication/test/generators/authentication/uninstall_generator_test.rb +1 -1
- data/lib/generators/propel_authentication/test/integration/generator_integration_test.rb +1 -1
- data/lib/generators/propel_authentication/test/integration/multi_version_generator_test.rb +13 -12
- data/lib/generators/propel_authentication/unpack_generator.rb +19 -15
- data/lib/propel_authentication.rb +1 -1
- metadata +14 -11
- data/lib/generators/propel_authentication/templates/agency.rb +0 -7
- data/lib/generators/propel_authentication/templates/agent.rb +0 -7
- data/lib/generators/propel_authentication/templates/auth/base_passwords_controller.rb.tt +0 -99
- data/lib/generators/propel_authentication/templates/auth/base_tokens_controller.rb.tt +0 -90
- data/lib/generators/propel_authentication/templates/organization.rb +0 -7
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
# Progressive Signup Flow
|
|
2
|
+
|
|
3
|
+
This authentication system supports a **progressive multi-step signup flow** that allows users to create organizations and agencies based on their needs.
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
The signup flow creates:
|
|
8
|
+
1. **Organization** - The main tenant/company
|
|
9
|
+
2. **User** - The primary user account (owner role)
|
|
10
|
+
3. **Agency** - Optional organizational unit (if agency tenancy enabled + provided)
|
|
11
|
+
4. **Agent** - Automatic relationship between user and agency (if agency created)
|
|
12
|
+
|
|
13
|
+
## Configuration
|
|
14
|
+
|
|
15
|
+
Agency tenancy can be enabled/disabled in `config/initializers/propel_api.rb`:
|
|
16
|
+
|
|
17
|
+
```ruby
|
|
18
|
+
PropelApi.configure do |config|
|
|
19
|
+
config.agency_tenancy = true # Enable agency-level tenancy
|
|
20
|
+
# config.agency_tenancy = false # Disable for organization-only tenancy
|
|
21
|
+
end
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Signup Endpoints
|
|
25
|
+
|
|
26
|
+
### Simple Signup (Organization + User only)
|
|
27
|
+
```http
|
|
28
|
+
POST /signup
|
|
29
|
+
Content-Type: application/json
|
|
30
|
+
|
|
31
|
+
{
|
|
32
|
+
"user": {
|
|
33
|
+
"email_address": "john@newcompany.com",
|
|
34
|
+
"username": "john_doe",
|
|
35
|
+
"password": "securepassword123",
|
|
36
|
+
"password_confirmation": "securepassword123",
|
|
37
|
+
"first_name": "John",
|
|
38
|
+
"last_name": "Doe",
|
|
39
|
+
"phone_number": "+1-555-0123",
|
|
40
|
+
"time_zone": "America/New_York"
|
|
41
|
+
},
|
|
42
|
+
"organization": {
|
|
43
|
+
"name": "NewCo Inc",
|
|
44
|
+
"website": "https://newco.com",
|
|
45
|
+
"time_zone": "America/New_York",
|
|
46
|
+
"description": "Innovative solutions company"
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
#### Response (agency tenancy disabled)
|
|
52
|
+
```json
|
|
53
|
+
{
|
|
54
|
+
"token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9...",
|
|
55
|
+
"user": {
|
|
56
|
+
"id": 123,
|
|
57
|
+
"email_address": "john@newcompany.com",
|
|
58
|
+
"username": "john_doe",
|
|
59
|
+
"first_name": "John",
|
|
60
|
+
"last_name": "Doe",
|
|
61
|
+
"organization_id": 456,
|
|
62
|
+
"created_at": "2024-01-15T10:30:00Z"
|
|
63
|
+
},
|
|
64
|
+
"organization": {
|
|
65
|
+
"id": 456,
|
|
66
|
+
"name": "NewCo Inc",
|
|
67
|
+
"website": "https://newco.com",
|
|
68
|
+
"time_zone": "America/New_York"
|
|
69
|
+
},
|
|
70
|
+
"agency": null,
|
|
71
|
+
"agent": null,
|
|
72
|
+
"message": "Account created successfully! Ready to start working.",
|
|
73
|
+
"next_steps": [
|
|
74
|
+
{
|
|
75
|
+
"action": "invite_team_members",
|
|
76
|
+
"description": "Invite colleagues to join your organization",
|
|
77
|
+
"endpoint": "/invitations",
|
|
78
|
+
"method": "POST"
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
"action": "start_creating_resources",
|
|
82
|
+
"description": "Begin creating and managing your content",
|
|
83
|
+
"endpoint": "/"
|
|
84
|
+
}
|
|
85
|
+
]
|
|
86
|
+
}
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
### Complete Signup (Organization + User + Agency + Agent)
|
|
90
|
+
```http
|
|
91
|
+
POST /signup
|
|
92
|
+
Content-Type: application/json
|
|
93
|
+
|
|
94
|
+
{
|
|
95
|
+
"user": {
|
|
96
|
+
"email_address": "sarah@designstudio.com",
|
|
97
|
+
"username": "sarah_creative",
|
|
98
|
+
"password": "securepassword123",
|
|
99
|
+
"password_confirmation": "securepassword123",
|
|
100
|
+
"first_name": "Sarah",
|
|
101
|
+
"last_name": "Designer"
|
|
102
|
+
},
|
|
103
|
+
"organization": {
|
|
104
|
+
"name": "Creative Design Studio",
|
|
105
|
+
"website": "https://designstudio.com",
|
|
106
|
+
"time_zone": "UTC"
|
|
107
|
+
},
|
|
108
|
+
"agency": {
|
|
109
|
+
"name": "Creative Department",
|
|
110
|
+
"description": "Main creative and design operations",
|
|
111
|
+
"website": "https://creative.designstudio.com",
|
|
112
|
+
"phone_number": "+1-555-DESIGN"
|
|
113
|
+
},
|
|
114
|
+
"agent": {
|
|
115
|
+
"role": "owner"
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
#### Response (agency tenancy enabled)
|
|
121
|
+
```json
|
|
122
|
+
{
|
|
123
|
+
"token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9...",
|
|
124
|
+
"user": {
|
|
125
|
+
"id": 789,
|
|
126
|
+
"email_address": "sarah@designstudio.com",
|
|
127
|
+
"username": "sarah_creative",
|
|
128
|
+
"first_name": "Sarah",
|
|
129
|
+
"last_name": "Designer",
|
|
130
|
+
"organization_id": 101,
|
|
131
|
+
"created_at": "2024-01-15T14:20:00Z"
|
|
132
|
+
},
|
|
133
|
+
"organization": {
|
|
134
|
+
"id": 101,
|
|
135
|
+
"name": "Creative Design Studio",
|
|
136
|
+
"website": "https://designstudio.com",
|
|
137
|
+
"time_zone": "UTC"
|
|
138
|
+
},
|
|
139
|
+
"agency": {
|
|
140
|
+
"id": 202,
|
|
141
|
+
"name": "Creative Department",
|
|
142
|
+
"organization_id": 101,
|
|
143
|
+
"description": "Main creative and design operations",
|
|
144
|
+
"website": "https://creative.designstudio.com"
|
|
145
|
+
},
|
|
146
|
+
"agent": {
|
|
147
|
+
"id": 303,
|
|
148
|
+
"user_id": 789,
|
|
149
|
+
"agency_id": 202,
|
|
150
|
+
"role": "owner",
|
|
151
|
+
"created_at": "2024-01-15T14:20:00Z"
|
|
152
|
+
},
|
|
153
|
+
"message": "Account created successfully! Ready to start working.",
|
|
154
|
+
"next_steps": [
|
|
155
|
+
{
|
|
156
|
+
"action": "create_additional_agencies",
|
|
157
|
+
"description": "Add more agencies to organize your team",
|
|
158
|
+
"endpoint": "/agencies",
|
|
159
|
+
"method": "POST"
|
|
160
|
+
},
|
|
161
|
+
{
|
|
162
|
+
"action": "start_creating_resources",
|
|
163
|
+
"description": "Begin creating and managing your content (you can create resources immediately)",
|
|
164
|
+
"endpoint": "/",
|
|
165
|
+
"note": "Your agency is ready for resource creation"
|
|
166
|
+
},
|
|
167
|
+
{
|
|
168
|
+
"action": "invite_team_members",
|
|
169
|
+
"description": "Invite colleagues to join your organization",
|
|
170
|
+
"endpoint": "/invitations",
|
|
171
|
+
"method": "POST"
|
|
172
|
+
}
|
|
173
|
+
]
|
|
174
|
+
}
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
## Progressive Enhancement Flow
|
|
178
|
+
|
|
179
|
+
### Step 1: Core Signup
|
|
180
|
+
User creates account with minimal information (User + Organization).
|
|
181
|
+
|
|
182
|
+
### Step 2: Add Agencies (Optional)
|
|
183
|
+
```http
|
|
184
|
+
POST /api/v1/agencies
|
|
185
|
+
Authorization: Bearer {token_from_signup}
|
|
186
|
+
Content-Type: application/json
|
|
187
|
+
|
|
188
|
+
{
|
|
189
|
+
"data": {
|
|
190
|
+
"name": "Marketing Department",
|
|
191
|
+
"description": "Marketing and growth operations"
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
### Step 3: Invite Team Members
|
|
197
|
+
```http
|
|
198
|
+
POST /api/v1/invitations
|
|
199
|
+
Authorization: Bearer {token_from_signup}
|
|
200
|
+
Content-Type: application/json
|
|
201
|
+
|
|
202
|
+
{
|
|
203
|
+
"data": {
|
|
204
|
+
"email_address": "teammate@designstudio.com",
|
|
205
|
+
"agency_id": 202,
|
|
206
|
+
"role": "manager"
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
### Step 4: Start Creating Resources
|
|
212
|
+
With the JWT token, users can immediately create resources:
|
|
213
|
+
|
|
214
|
+
```http
|
|
215
|
+
POST /api/v1/projects
|
|
216
|
+
Authorization: Bearer {token_from_signup}
|
|
217
|
+
Content-Type: application/json
|
|
218
|
+
|
|
219
|
+
{
|
|
220
|
+
"data": {
|
|
221
|
+
"name": "New Website Design",
|
|
222
|
+
"agency_id": 202
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
## Error Handling
|
|
228
|
+
|
|
229
|
+
### Missing Required Agency Data (when agency tenancy enabled)
|
|
230
|
+
```json
|
|
231
|
+
{
|
|
232
|
+
"error": "Agency information required",
|
|
233
|
+
"message": "Agency details must be provided when agency tenancy is enabled",
|
|
234
|
+
"code": "MISSING_AGENCY_DATA",
|
|
235
|
+
"hint": "Include an 'agency' object with name and other details in your request"
|
|
236
|
+
}
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
### Validation Errors
|
|
240
|
+
```json
|
|
241
|
+
{
|
|
242
|
+
"error": "Validation failed",
|
|
243
|
+
"details": {
|
|
244
|
+
"email_address": ["Email address has already been taken"],
|
|
245
|
+
"password": ["Password confirmation doesn't match Password"]
|
|
246
|
+
},
|
|
247
|
+
"message": "Please correct the errors and try again"
|
|
248
|
+
}
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
## JWT Token Structure
|
|
252
|
+
|
|
253
|
+
The returned JWT token contains:
|
|
254
|
+
```json
|
|
255
|
+
{
|
|
256
|
+
"user_id": 789,
|
|
257
|
+
"email_address": "sarah@designstudio.com",
|
|
258
|
+
"organization_id": 101,
|
|
259
|
+
"agency_ids": [202],
|
|
260
|
+
"iat": 1705329600,
|
|
261
|
+
"exp": 1705416000
|
|
262
|
+
}
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
This enables immediate API access with proper organizational and agency scoping.
|
|
266
|
+
|
|
267
|
+
## User-Provided vs. System-Automatic
|
|
268
|
+
|
|
269
|
+
### 🔴 REQUIRED User Input
|
|
270
|
+
- **email_address** - Validated for format & uniqueness
|
|
271
|
+
- **username** - Validated for uniqueness
|
|
272
|
+
- **password** - Minimum 8 characters, secure validation
|
|
273
|
+
- **password_confirmation** - Must match password
|
|
274
|
+
- **organization.name** - Organization name (required)
|
|
275
|
+
|
|
276
|
+
### 🟡 OPTIONAL User Input
|
|
277
|
+
- **first_name, last_name** - For personalization (optional)
|
|
278
|
+
- **phone_number** - Contact information (optional)
|
|
279
|
+
- **time_zone** - User/organization timezone (optional)
|
|
280
|
+
- **organization.website** - Company website (optional)
|
|
281
|
+
- **organization.description** - Company description (optional)
|
|
282
|
+
- **agency.{name, description, etc}** - When agency tenancy enabled (optional)
|
|
283
|
+
|
|
284
|
+
### 🤖 SYSTEM-AUTOMATIC
|
|
285
|
+
- **user.id** - Auto-generated primary key
|
|
286
|
+
- **password_digest** - Auto-hashed with bcrypt
|
|
287
|
+
- **organization_id** - Auto-assigned from created organization
|
|
288
|
+
- **role: 'owner'** - Auto-assigned for signup user
|
|
289
|
+
- **JWT token** - Auto-generated for immediate API access
|
|
290
|
+
- **Agent record** - Auto-created if agency provided
|
|
291
|
+
- **created_at/updated_at** - Auto-set timestamps
|
|
292
|
+
- **Organization/agency scoping** - Auto-applied to all resources
|
|
293
|
+
|
|
294
|
+
## Architecture Benefits
|
|
295
|
+
|
|
296
|
+
1. **Immediate Access**: Users can start working immediately after signup
|
|
297
|
+
2. **Progressive Enhancement**: Start simple, add complexity as needed
|
|
298
|
+
3. **Proper Scoping**: All resources automatically scoped to organization/agency
|
|
299
|
+
4. **Security**: JWT tokens contain proper context for authorization
|
|
300
|
+
5. **Flexibility**: Works with or without agency tenancy
|
|
301
|
+
6. **Smart Defaults**: Minimal required input, system handles the rest
|
|
302
|
+
|
|
303
|
+
## Configuration Options
|
|
304
|
+
|
|
305
|
+
Users can choose their tenancy model:
|
|
306
|
+
|
|
307
|
+
- **Organization-only tenancy**: `agency_tenancy = false`
|
|
308
|
+
- Simpler data model
|
|
309
|
+
- Resources scoped to organization only
|
|
310
|
+
- Good for single-agency organizations
|
|
311
|
+
|
|
312
|
+
- **Multi-agency tenancy**: `agency_tenancy = true` (default)
|
|
313
|
+
- More complex but flexible
|
|
314
|
+
- Resources scoped to organization + agency
|
|
315
|
+
- Good for multi-department organizations
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
class Agency < ApplicationRecord
|
|
2
|
+
# Multi-tenant associations
|
|
3
|
+
belongs_to :organization
|
|
4
|
+
has_many :agents, dependent: :destroy
|
|
5
|
+
|
|
6
|
+
validates :name, presence: true
|
|
7
|
+
<% if @rendering_engine == 'json_facet' -%>
|
|
8
|
+
|
|
9
|
+
# Facets
|
|
10
|
+
json_facet :short, fields: [:id, :name]
|
|
11
|
+
json_facet :details, fields: [:id, :name, :organization_id, :created_at, :updated_at]
|
|
12
|
+
<% end -%>
|
|
13
|
+
end
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
class Agent < ApplicationRecord
|
|
2
|
+
# Multi-tenant associations
|
|
3
|
+
belongs_to :agency
|
|
4
|
+
belongs_to :user
|
|
5
|
+
|
|
6
|
+
validates :user_id, uniqueness: { scope: :agency_id }
|
|
7
|
+
<% if @rendering_engine == 'json_facet' -%>
|
|
8
|
+
|
|
9
|
+
# Facets
|
|
10
|
+
json_facet :short, fields: [:id, :title]
|
|
11
|
+
json_facet :details, fields: [:id, :title, :role, :organization_id, :created_at, :updated_at]
|
|
12
|
+
<% end -%>
|
|
13
|
+
end
|
data/lib/generators/propel_authentication/templates/{invitation.rb → models/invitation.rb.tt}
RENAMED
|
@@ -15,6 +15,12 @@ class Invitation < ApplicationRecord
|
|
|
15
15
|
scope :valid, -> { where(status: :pending).where('created_at > ?', 7.days.ago) }
|
|
16
16
|
scope :recent, -> { where('created_at > ?', 30.days.ago) }
|
|
17
17
|
|
|
18
|
+
<% if @rendering_engine == 'json_facet' -%>
|
|
19
|
+
# Facets
|
|
20
|
+
json_facet :short, fields: [:id, :email_address, :organization_id]
|
|
21
|
+
json_facet :details, fields: [:id, :name, :status, :organization_id, :created_at, :updated_at]
|
|
22
|
+
<% end -%>
|
|
23
|
+
|
|
18
24
|
# Check if invitation is still valid (not expired)
|
|
19
25
|
def invitation_valid?
|
|
20
26
|
pending? && created_at > 7.days.ago
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
class Organization < ApplicationRecord
|
|
2
|
+
# Multi-tenant associations
|
|
3
|
+
has_many :users, dependent: :destroy
|
|
4
|
+
has_many :agencies, dependent: :destroy
|
|
5
|
+
|
|
6
|
+
validates :name, presence: true
|
|
7
|
+
<% if @rendering_engine == 'json_facet' -%>
|
|
8
|
+
# Facets
|
|
9
|
+
json_facet :short, fields: [:id, :name, :website, :time_zone]
|
|
10
|
+
json_facet :details, fields: [:id, :name, :website, :time_zone, :meta, :settings, :created_at, :updated_at]
|
|
11
|
+
<% end -%>
|
|
12
|
+
end
|
|
@@ -3,6 +3,7 @@ class User < ApplicationRecord
|
|
|
3
3
|
belongs_to :organization
|
|
4
4
|
has_many :invitations, foreign_key: :inviter_id, dependent: :destroy
|
|
5
5
|
has_many :agents, dependent: :destroy
|
|
6
|
+
has_many :agencies, through: :agents
|
|
6
7
|
|
|
7
8
|
# Validations
|
|
8
9
|
validates :email_address, presence: true, uniqueness: true
|
|
@@ -18,4 +19,8 @@ class User < ApplicationRecord
|
|
|
18
19
|
# Future enhancements (Epic 2)
|
|
19
20
|
# include Invitable
|
|
20
21
|
# include Statusable
|
|
22
|
+
|
|
23
|
+
# Facets
|
|
24
|
+
json_facet :short, fields: [:id, :email_address, :username, :phone_number, :first_name, :middle_name, :last_name, :time_zone, :status]
|
|
25
|
+
json_facet :details, fields: [:id, :email_address, :username, :phone_number, :first_name, :middle_name, :last_name, :time_zone, :confirmation_sent_at, :unconfirmed_email_address, :confirmed_at, :status, :last_login_at, :failed_login_attempts, :locked_at, :organization_id, :meta, :settings, :created_at, :updated_at], include: [:organization]
|
|
21
26
|
end
|
|
@@ -44,8 +44,12 @@ module PropelAuthentication
|
|
|
44
44
|
:support_email,
|
|
45
45
|
:enable_email_notifications,
|
|
46
46
|
:enable_sms_notifications,
|
|
47
|
+
:agency_tenancy,
|
|
48
|
+
:require_organization_id,
|
|
49
|
+
:require_user_id,
|
|
47
50
|
:namespace,
|
|
48
|
-
:version
|
|
51
|
+
:version,
|
|
52
|
+
:auth_scope
|
|
49
53
|
|
|
50
54
|
def initialize
|
|
51
55
|
# JWT Configuration defaults
|
|
@@ -73,19 +77,48 @@ module PropelAuthentication
|
|
|
73
77
|
@password_reset_rate_limit = 1.minute
|
|
74
78
|
|
|
75
79
|
# Frontend URL for email links (configure for your frontend application)
|
|
76
|
-
|
|
80
|
+
# Priority: Rails credentials -> ENV variables -> environment-specific fallbacks
|
|
81
|
+
@frontend_url = Rails.application.credentials.frontend_url ||
|
|
82
|
+
ENV['FRONTEND_URL'] ||
|
|
83
|
+
case Rails.env
|
|
84
|
+
when 'development', 'test'
|
|
85
|
+
'http://localhost:3000'
|
|
86
|
+
when 'staging'
|
|
87
|
+
'https://staging.yourapp.com' # Replace with your staging URL
|
|
88
|
+
when 'production'
|
|
89
|
+
'https://yourapp.com' # Replace with your production URL
|
|
90
|
+
else
|
|
91
|
+
'http://localhost:3000' # Safe fallback for any other environment
|
|
92
|
+
end
|
|
77
93
|
|
|
78
94
|
# Email configuration
|
|
79
|
-
|
|
80
|
-
@
|
|
95
|
+
# Priority: Rails credentials -> ENV variables -> sensible defaults
|
|
96
|
+
@email_from_address = Rails.application.credentials.email_from_address ||
|
|
97
|
+
ENV['EMAIL_FROM_ADDRESS'] ||
|
|
98
|
+
"noreply@#{Rails.application.class.module_parent.name.downcase}.com"
|
|
99
|
+
|
|
100
|
+
@support_email = Rails.application.credentials.support_email ||
|
|
101
|
+
ENV['SUPPORT_EMAIL'] ||
|
|
102
|
+
"support@#{Rails.application.class.module_parent.name.downcase}.com"
|
|
81
103
|
|
|
82
104
|
# Notification preferences
|
|
83
105
|
@enable_email_notifications = true
|
|
84
106
|
@enable_sms_notifications = false # Requires SMS provider configuration
|
|
85
107
|
|
|
86
|
-
#
|
|
87
|
-
|
|
88
|
-
@
|
|
108
|
+
# Tenancy configuration
|
|
109
|
+
# Controls whether multi-agency tenancy is enabled within organizations
|
|
110
|
+
@agency_tenancy = true # Enable agency-level tenancy by default
|
|
111
|
+
|
|
112
|
+
# API tenancy requirements (for PropelApi integration)
|
|
113
|
+
# Controls whether APIs require explicit tenancy context in requests
|
|
114
|
+
@require_organization_id = false # Auto-assign organization when missing (developer-friendly default)
|
|
115
|
+
@require_user_id = false # Auto-assign current user when missing (common workflow default)
|
|
116
|
+
|
|
117
|
+
# API namespace and versioning configuration
|
|
118
|
+
# Default to no namespace/version/auth_scope for clean URLs like /login, /me, /signup
|
|
119
|
+
@namespace = nil
|
|
120
|
+
@version = nil
|
|
121
|
+
@auth_scope = nil
|
|
89
122
|
end
|
|
90
123
|
end
|
|
91
124
|
end
|
|
@@ -93,7 +126,13 @@ end
|
|
|
93
126
|
# Configure PropelAuthentication with secure defaults
|
|
94
127
|
PropelAuthentication.configure do |config|
|
|
95
128
|
# JWT Configuration for API authentication
|
|
96
|
-
|
|
129
|
+
# Priority: Rails credentials -> ENV variables -> fallback (development/test only)
|
|
130
|
+
config.jwt_secret = Rails.application.credentials.jwt_secret ||
|
|
131
|
+
ENV['JWT_SECRET'] ||
|
|
132
|
+
Rails.application.credentials.secret_key_base ||
|
|
133
|
+
ENV['SECRET_KEY_BASE'] ||
|
|
134
|
+
(Rails.env.development? || Rails.env.test? ? 'development-secret-key' :
|
|
135
|
+
raise('JWT_SECRET must be set in production'))
|
|
97
136
|
config.jwt_expiration = 24.hours
|
|
98
137
|
|
|
99
138
|
# Password requirements
|
|
@@ -102,6 +141,25 @@ PropelAuthentication.configure do |config|
|
|
|
102
141
|
# User registration settings
|
|
103
142
|
config.allow_registration = true
|
|
104
143
|
|
|
144
|
+
# Multi-tenancy settings
|
|
145
|
+
# Controls whether agency-level tenancy is enabled within organizations
|
|
146
|
+
# When true: Organization -> Agency -> Resources structure
|
|
147
|
+
# When false: Organization -> Resources structure (simpler)
|
|
148
|
+
config.agency_tenancy = true
|
|
149
|
+
|
|
150
|
+
# API tenancy requirements (for PropelApi integration)
|
|
151
|
+
# Controls whether clients must provide explicit tenancy context in API requests
|
|
152
|
+
|
|
153
|
+
# Require explicit organization_id in API requests?
|
|
154
|
+
# false (recommended): Auto-assigns current user's organization_id when missing
|
|
155
|
+
# true (strict): Returns 422 error if organization_id not provided by client
|
|
156
|
+
config.require_organization_id = false
|
|
157
|
+
|
|
158
|
+
# Require explicit user_id in API requests?
|
|
159
|
+
# false (recommended): Auto-assigns current user as creator when missing
|
|
160
|
+
# true (admin mode): Returns 422 error if user_id not provided (for delegation scenarios)
|
|
161
|
+
config.require_user_id = false
|
|
162
|
+
|
|
105
163
|
# Email confirmation (for future use)
|
|
106
164
|
config.require_email_confirmation = false
|
|
107
165
|
|
|
@@ -122,9 +180,36 @@ PropelAuthentication.configure do |config|
|
|
|
122
180
|
config.jwt_expiration = 2.hours
|
|
123
181
|
end
|
|
124
182
|
|
|
125
|
-
# URL structure configuration (set by generator)
|
|
183
|
+
# URL structure configuration (set by generator based on your choices)
|
|
184
|
+
# - Default: Clean URLs like /login, /me, /signup (no additional namespacing)
|
|
185
|
+
# - With PropelApi: Automatically adopts API namespace for consistency
|
|
186
|
+
# - Override: Set explicit values to customize URL structure
|
|
187
|
+
#
|
|
188
|
+
# Current configuration:
|
|
126
189
|
config.namespace = <%= @auth_namespace ? "'#{@auth_namespace}'" : 'nil' %>
|
|
127
190
|
config.version = <%= @auth_version ? "'#{@auth_version}'" : 'nil' %>
|
|
191
|
+
config.auth_scope = <%= @auth_scope ? "'#{@auth_scope}'" : 'nil' %>
|
|
192
|
+
|
|
193
|
+
# Examples for customization:
|
|
194
|
+
# For clean URLs (default):
|
|
195
|
+
# config.namespace = nil
|
|
196
|
+
# config.version = nil
|
|
197
|
+
# config.auth_scope = nil # → /login, /me → TokensController
|
|
198
|
+
#
|
|
199
|
+
# For organized URLs with auth scope:
|
|
200
|
+
# config.namespace = nil
|
|
201
|
+
# config.version = nil
|
|
202
|
+
# config.auth_scope = 'auth' # → /auth/login, /auth/me → Auth::TokensController
|
|
203
|
+
#
|
|
204
|
+
# For API-style URLs:
|
|
205
|
+
# config.namespace = 'api'
|
|
206
|
+
# config.version = 'v1'
|
|
207
|
+
# config.auth_scope = nil # → /api/v1/login, /api/v1/me → Api::V1::TokensController
|
|
208
|
+
#
|
|
209
|
+
# For API-style URLs with auth scope:
|
|
210
|
+
# config.namespace = 'api'
|
|
211
|
+
# config.version = 'v1'
|
|
212
|
+
# config.auth_scope = 'auth' # → /api/v1/auth/login → Api::V1::Auth::TokensController
|
|
128
213
|
end
|
|
129
214
|
|
|
130
215
|
# PropelAuthentication is now fully extracted to your application!
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# JWT Authentication routes for <%= auth_route_prefix %>
|
|
2
|
+
<%- if @auth_namespace.present? || @auth_version.present? || @auth_scope.present? -%>
|
|
3
|
+
<%- namespace_parts = [] -%>
|
|
4
|
+
<%- namespace_parts << @auth_namespace if @auth_namespace.present? -%>
|
|
5
|
+
<%- namespace_parts << @auth_version if @auth_version.present? -%>
|
|
6
|
+
<%- namespace_parts.each_with_index do |part, index| -%>
|
|
7
|
+
<%= " " * index %>namespace :<%= part %> do
|
|
8
|
+
<%- end -%>
|
|
9
|
+
<%- if @auth_scope.present? -%>
|
|
10
|
+
<%= " " * namespace_parts.length %>namespace :<%= @auth_scope %> do
|
|
11
|
+
<%= " " * (namespace_parts.length + 1) %>post 'signup', to: 'signup#create', as: :signup
|
|
12
|
+
<%= " " * (namespace_parts.length + 1) %>post 'login', to: 'tokens#create', as: :login
|
|
13
|
+
<%= " " * (namespace_parts.length + 1) %>get 'me', to: 'tokens#show', as: :me
|
|
14
|
+
<%= " " * (namespace_parts.length + 1) %>delete 'logout', to: 'tokens#destroy', as: :logout
|
|
15
|
+
<%= " " * (namespace_parts.length + 1) %>post 'unlock', to: 'tokens#unlock', as: :unlock
|
|
16
|
+
<%= " " * (namespace_parts.length + 1) %>post 'reset', to: 'passwords#create', as: :reset
|
|
17
|
+
<%= " " * (namespace_parts.length + 1) %>get 'reset', to: 'passwords#show', as: :verify_reset
|
|
18
|
+
<%= " " * (namespace_parts.length + 1) %>patch 'reset', to: 'passwords#update', as: :update_reset
|
|
19
|
+
<%= " " * namespace_parts.length %>end
|
|
20
|
+
<%- else -%>
|
|
21
|
+
<%= " " * namespace_parts.length %>post 'signup', to: 'signup#create', as: :signup
|
|
22
|
+
<%= " " * namespace_parts.length %>post 'login', to: 'tokens#create', as: :login
|
|
23
|
+
<%= " " * namespace_parts.length %>get 'me', to: 'tokens#show', as: :me
|
|
24
|
+
<%= " " * namespace_parts.length %>delete 'logout', to: 'tokens#destroy', as: :logout
|
|
25
|
+
<%= " " * namespace_parts.length %>post 'unlock', to: 'tokens#unlock', as: :unlock
|
|
26
|
+
<%= " " * namespace_parts.length %>post 'reset', to: 'passwords#create', as: :reset
|
|
27
|
+
<%= " " * namespace_parts.length %>get 'reset', to: 'passwords#show', as: :verify_reset
|
|
28
|
+
<%= " " * namespace_parts.length %>patch 'reset', to: 'passwords#update', as: :update_reset
|
|
29
|
+
<%- end -%>
|
|
30
|
+
<%- namespace_parts.length.times do |index| -%>
|
|
31
|
+
<%= " " * (namespace_parts.length - 1 - index) %>end
|
|
32
|
+
<%- end -%>
|
|
33
|
+
<%- else -%>
|
|
34
|
+
<%- if @auth_scope.present? -%>
|
|
35
|
+
namespace :<%= @auth_scope %> do
|
|
36
|
+
post 'signup', to: 'signup#create', as: :signup
|
|
37
|
+
post 'login', to: 'tokens#create', as: :login
|
|
38
|
+
get 'me', to: 'tokens#show', as: :me
|
|
39
|
+
delete 'logout', to: 'tokens#destroy', as: :logout
|
|
40
|
+
post 'unlock', to: 'tokens#unlock', as: :unlock
|
|
41
|
+
post 'reset', to: 'passwords#create', as: :reset
|
|
42
|
+
get 'reset', to: 'passwords#show', as: :verify_reset
|
|
43
|
+
patch 'reset', to: 'passwords#update', as: :update_reset
|
|
44
|
+
end
|
|
45
|
+
<%- else -%>
|
|
46
|
+
post 'signup', to: 'signup#create', as: :signup
|
|
47
|
+
post 'login', to: 'tokens#create', as: :login
|
|
48
|
+
get 'me', to: 'tokens#show', as: :me
|
|
49
|
+
delete 'logout', to: 'tokens#destroy', as: :logout
|
|
50
|
+
post 'unlock', to: 'tokens#unlock', as: :unlock
|
|
51
|
+
post 'reset', to: 'passwords#create', as: :reset
|
|
52
|
+
get 'reset', to: 'passwords#show', as: :verify_reset
|
|
53
|
+
patch 'reset', to: 'passwords#update', as: :update_reset
|
|
54
|
+
<%- end -%>
|
|
55
|
+
<%- end -%>
|
|
@@ -64,17 +64,17 @@ class AuthNotificationService
|
|
|
64
64
|
private
|
|
65
65
|
|
|
66
66
|
def build_password_reset_url(token)
|
|
67
|
-
base_url =
|
|
67
|
+
base_url = PropelAuthentication.configuration.frontend_url || default_frontend_url
|
|
68
68
|
"#{base_url}/reset-password?token=#{token}"
|
|
69
69
|
end
|
|
70
70
|
|
|
71
71
|
def build_account_unlock_url(token)
|
|
72
|
-
base_url =
|
|
72
|
+
base_url = PropelAuthentication.configuration.frontend_url || default_frontend_url
|
|
73
73
|
"#{base_url}/unlock-account?token=#{token}"
|
|
74
74
|
end
|
|
75
75
|
|
|
76
76
|
def build_invitation_url(token)
|
|
77
|
-
base_url =
|
|
77
|
+
base_url = PropelAuthentication.configuration.frontend_url || default_frontend_url
|
|
78
78
|
"#{base_url}/accept-invitation?token=#{token}"
|
|
79
79
|
end
|
|
80
80
|
|