red64-cli 0.1.0 → 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.
Files changed (103) hide show
  1. package/dist/cli/parseArgs.d.ts.map +1 -1
  2. package/dist/cli/parseArgs.js +5 -0
  3. package/dist/cli/parseArgs.js.map +1 -1
  4. package/dist/components/init/CompleteStep.d.ts.map +1 -1
  5. package/dist/components/init/CompleteStep.js +2 -2
  6. package/dist/components/init/CompleteStep.js.map +1 -1
  7. package/dist/components/init/TestCheckStep.d.ts +16 -0
  8. package/dist/components/init/TestCheckStep.d.ts.map +1 -0
  9. package/dist/components/init/TestCheckStep.js +120 -0
  10. package/dist/components/init/TestCheckStep.js.map +1 -0
  11. package/dist/components/init/index.d.ts +1 -0
  12. package/dist/components/init/index.d.ts.map +1 -1
  13. package/dist/components/init/index.js +1 -0
  14. package/dist/components/init/index.js.map +1 -1
  15. package/dist/components/init/types.d.ts +9 -0
  16. package/dist/components/init/types.d.ts.map +1 -1
  17. package/dist/components/screens/InitScreen.d.ts.map +1 -1
  18. package/dist/components/screens/InitScreen.js +69 -6
  19. package/dist/components/screens/InitScreen.js.map +1 -1
  20. package/dist/components/screens/StartScreen.d.ts.map +1 -1
  21. package/dist/components/screens/StartScreen.js +89 -3
  22. package/dist/components/screens/StartScreen.js.map +1 -1
  23. package/dist/services/ConfigService.d.ts +1 -0
  24. package/dist/services/ConfigService.d.ts.map +1 -1
  25. package/dist/services/ConfigService.js.map +1 -1
  26. package/dist/services/ProjectDetector.d.ts +28 -0
  27. package/dist/services/ProjectDetector.d.ts.map +1 -0
  28. package/dist/services/ProjectDetector.js +236 -0
  29. package/dist/services/ProjectDetector.js.map +1 -0
  30. package/dist/services/TestRunner.d.ts +46 -0
  31. package/dist/services/TestRunner.d.ts.map +1 -0
  32. package/dist/services/TestRunner.js +85 -0
  33. package/dist/services/TestRunner.js.map +1 -0
  34. package/dist/services/index.d.ts +2 -0
  35. package/dist/services/index.d.ts.map +1 -1
  36. package/dist/services/index.js +2 -0
  37. package/dist/services/index.js.map +1 -1
  38. package/dist/types/index.d.ts +1 -0
  39. package/dist/types/index.d.ts.map +1 -1
  40. package/dist/types/index.js.map +1 -1
  41. package/framework/agents/claude/.claude/agents/red64/spec-impl.md +131 -2
  42. package/framework/agents/claude/.claude/commands/red64/spec-impl.md +24 -0
  43. package/framework/agents/codex/.codex/agents/red64/spec-impl.md +131 -2
  44. package/framework/agents/codex/.codex/commands/red64/spec-impl.md +24 -0
  45. package/framework/stacks/generic/feedback.md +80 -0
  46. package/framework/stacks/nextjs/accessibility.md +437 -0
  47. package/framework/stacks/nextjs/api.md +431 -0
  48. package/framework/stacks/nextjs/coding-style.md +282 -0
  49. package/framework/stacks/nextjs/commenting.md +226 -0
  50. package/framework/stacks/nextjs/components.md +411 -0
  51. package/framework/stacks/nextjs/conventions.md +333 -0
  52. package/framework/stacks/nextjs/css.md +310 -0
  53. package/framework/stacks/nextjs/error-handling.md +442 -0
  54. package/framework/stacks/nextjs/feedback.md +124 -0
  55. package/framework/stacks/nextjs/migrations.md +332 -0
  56. package/framework/stacks/nextjs/models.md +362 -0
  57. package/framework/stacks/nextjs/queries.md +410 -0
  58. package/framework/stacks/nextjs/responsive.md +338 -0
  59. package/framework/stacks/nextjs/tech-stack.md +177 -0
  60. package/framework/stacks/nextjs/test-writing.md +475 -0
  61. package/framework/stacks/nextjs/validation.md +467 -0
  62. package/framework/stacks/python/api.md +468 -0
  63. package/framework/stacks/python/authentication.md +342 -0
  64. package/framework/stacks/python/code-quality.md +283 -0
  65. package/framework/stacks/python/code-refactoring.md +315 -0
  66. package/framework/stacks/python/coding-style.md +462 -0
  67. package/framework/stacks/python/conventions.md +399 -0
  68. package/framework/stacks/python/error-handling.md +512 -0
  69. package/framework/stacks/python/feedback.md +92 -0
  70. package/framework/stacks/python/implement-ai-llm.md +468 -0
  71. package/framework/stacks/python/migrations.md +388 -0
  72. package/framework/stacks/python/models.md +399 -0
  73. package/framework/stacks/python/python.md +232 -0
  74. package/framework/stacks/python/queries.md +451 -0
  75. package/framework/stacks/python/structure.md +245 -58
  76. package/framework/stacks/python/tech.md +92 -35
  77. package/framework/stacks/python/testing.md +380 -0
  78. package/framework/stacks/python/validation.md +471 -0
  79. package/framework/stacks/rails/authentication.md +176 -0
  80. package/framework/stacks/rails/code-quality.md +287 -0
  81. package/framework/stacks/rails/code-refactoring.md +299 -0
  82. package/framework/stacks/rails/feedback.md +130 -0
  83. package/framework/stacks/rails/implement-ai-llm-with-rubyllm.md +342 -0
  84. package/framework/stacks/rails/rails.md +301 -0
  85. package/framework/stacks/rails/rails8-best-practices.md +498 -0
  86. package/framework/stacks/rails/rails8-css.md +573 -0
  87. package/framework/stacks/rails/structure.md +140 -0
  88. package/framework/stacks/rails/tech.md +108 -0
  89. package/framework/stacks/react/code-quality.md +521 -0
  90. package/framework/stacks/react/components.md +625 -0
  91. package/framework/stacks/react/data-fetching.md +586 -0
  92. package/framework/stacks/react/feedback.md +110 -0
  93. package/framework/stacks/react/forms.md +694 -0
  94. package/framework/stacks/react/performance.md +640 -0
  95. package/framework/stacks/react/product.md +22 -9
  96. package/framework/stacks/react/state-management.md +472 -0
  97. package/framework/stacks/react/structure.md +351 -44
  98. package/framework/stacks/react/tech.md +219 -30
  99. package/framework/stacks/react/testing.md +690 -0
  100. package/package.json +1 -1
  101. package/framework/stacks/node/product.md +0 -27
  102. package/framework/stacks/node/structure.md +0 -82
  103. package/framework/stacks/node/tech.md +0 -63
@@ -0,0 +1,471 @@
1
+ # Validation Patterns
2
+
3
+ Input validation with Pydantic v2 and FastAPI for type-safe, self-documenting APIs.
4
+
5
+ ---
6
+
7
+ ## Philosophy
8
+
9
+ - **Server-side is the source of truth**: Never trust client-side validation alone
10
+ - **Fail early**: Reject invalid data before it reaches business logic
11
+ - **Descriptive errors**: Field-specific messages that help users fix input
12
+ - **Schema as documentation**: Pydantic models generate OpenAPI specs automatically
13
+
14
+ ---
15
+
16
+ ## Pydantic Model Basics
17
+
18
+ ### Request Schemas
19
+
20
+ ```python
21
+ # app/schemas/user.py
22
+ from pydantic import BaseModel, EmailStr, Field
23
+
24
+
25
+ class CreateUserRequest(BaseModel):
26
+ email: EmailStr
27
+ name: str = Field(min_length=1, max_length=255)
28
+ password: str = Field(min_length=8, max_length=128)
29
+ bio: str | None = Field(default=None, max_length=2000)
30
+
31
+
32
+ class UpdateUserRequest(BaseModel):
33
+ name: str | None = Field(default=None, min_length=1, max_length=255)
34
+ bio: str | None = Field(default=None, max_length=2000)
35
+ ```
36
+
37
+ ### Response Schemas
38
+
39
+ ```python
40
+ from datetime import datetime
41
+ from pydantic import BaseModel, ConfigDict
42
+
43
+
44
+ class UserResponse(BaseModel):
45
+ model_config = ConfigDict(from_attributes=True)
46
+
47
+ id: int
48
+ email: str
49
+ name: str
50
+ bio: str | None
51
+ is_active: bool
52
+ created_at: datetime
53
+ ```
54
+
55
+ **Rule**: Always use separate request and response schemas. Never expose internal fields (password hashes, soft-delete timestamps) in responses.
56
+
57
+ ---
58
+
59
+ ## Field Validators
60
+
61
+ ### field_validator (Single Field)
62
+
63
+ ```python
64
+ from pydantic import field_validator
65
+ import re
66
+
67
+
68
+ class CreateUserRequest(BaseModel):
69
+ email: EmailStr
70
+ name: str = Field(min_length=1, max_length=255)
71
+ password: str = Field(min_length=8, max_length=128)
72
+ username: str = Field(min_length=3, max_length=30)
73
+
74
+ @field_validator("password")
75
+ @classmethod
76
+ def password_complexity(cls, v: str) -> str:
77
+ if not re.search(r"[A-Z]", v):
78
+ raise ValueError("Must contain at least one uppercase letter")
79
+ if not re.search(r"[0-9]", v):
80
+ raise ValueError("Must contain at least one digit")
81
+ return v
82
+
83
+ @field_validator("username")
84
+ @classmethod
85
+ def username_format(cls, v: str) -> str:
86
+ if not re.match(r"^[a-z0-9_-]+$", v):
87
+ raise ValueError("Only lowercase letters, numbers, hyphens, and underscores")
88
+ return v
89
+ ```
90
+
91
+ ### model_validator (Cross-Field)
92
+
93
+ ```python
94
+ from pydantic import model_validator
95
+
96
+
97
+ class DateRangeRequest(BaseModel):
98
+ start_date: date
99
+ end_date: date
100
+
101
+ @model_validator(mode="after")
102
+ def validate_date_range(self) -> "DateRangeRequest":
103
+ if self.end_date <= self.start_date:
104
+ raise ValueError("end_date must be after start_date")
105
+ if (self.end_date - self.start_date).days > 365:
106
+ raise ValueError("Date range cannot exceed one year")
107
+ return self
108
+
109
+
110
+ class ChangePasswordRequest(BaseModel):
111
+ current_password: str
112
+ new_password: str = Field(min_length=8)
113
+ confirm_password: str
114
+
115
+ @model_validator(mode="after")
116
+ def passwords_match(self) -> "ChangePasswordRequest":
117
+ if self.new_password != self.confirm_password:
118
+ raise ValueError("new_password and confirm_password must match")
119
+ if self.new_password == self.current_password:
120
+ raise ValueError("New password must differ from current password")
121
+ return self
122
+ ```
123
+
124
+ ---
125
+
126
+ ## FastAPI Request Validation
127
+
128
+ ### Automatic Validation
129
+
130
+ FastAPI validates request bodies, query params, and path params automatically:
131
+
132
+ ```python
133
+ from fastapi import APIRouter, Query, Path
134
+
135
+ router = APIRouter()
136
+
137
+
138
+ @router.post("/users", status_code=201)
139
+ async def create_user(data: CreateUserRequest) -> UserResponse:
140
+ # data is already validated by Pydantic
141
+ ...
142
+
143
+
144
+ @router.get("/users")
145
+ async def list_users(
146
+ page: int = Query(default=1, ge=1, description="Page number"),
147
+ per_page: int = Query(default=20, ge=1, le=100, description="Items per page"),
148
+ search: str | None = Query(default=None, min_length=1, max_length=100),
149
+ status: str | None = Query(default=None, pattern="^(active|inactive|all)$"),
150
+ ) -> PaginatedResponse[UserResponse]:
151
+ ...
152
+
153
+
154
+ @router.get("/users/{user_id}")
155
+ async def get_user(
156
+ user_id: int = Path(ge=1, description="User ID"),
157
+ ) -> UserResponse:
158
+ ...
159
+ ```
160
+
161
+ ### Custom Error Messages for FastAPI
162
+
163
+ Override the default Pydantic validation error format:
164
+
165
+ ```python
166
+ from fastapi import FastAPI, Request
167
+ from fastapi.exceptions import RequestValidationError
168
+ from fastapi.responses import JSONResponse
169
+
170
+
171
+ @app.exception_handler(RequestValidationError)
172
+ async def validation_exception_handler(
173
+ request: Request, exc: RequestValidationError,
174
+ ) -> JSONResponse:
175
+ errors = {}
176
+ for error in exc.errors():
177
+ field = ".".join(str(loc) for loc in error["loc"] if loc != "body")
178
+ errors[field] = error["msg"]
179
+
180
+ return JSONResponse(
181
+ status_code=422,
182
+ content={
183
+ "error": {
184
+ "code": "VALIDATION_ERROR",
185
+ "message": "Request validation failed",
186
+ "details": {"field_errors": errors},
187
+ },
188
+ },
189
+ )
190
+ ```
191
+
192
+ ---
193
+
194
+ ## Custom Types
195
+
196
+ ### Reusable Annotated Types
197
+
198
+ ```python
199
+ from typing import Annotated
200
+ from pydantic import Field, AfterValidator
201
+
202
+
203
+ def validate_slug(v: str) -> str:
204
+ import re
205
+ if not re.match(r"^[a-z0-9]+(?:-[a-z0-9]+)*$", v):
206
+ raise ValueError("Invalid slug format (lowercase, hyphens only)")
207
+ return v
208
+
209
+
210
+ # Reusable types
211
+ Slug = Annotated[str, Field(min_length=1, max_length=200), AfterValidator(validate_slug)]
212
+ NonEmptyStr = Annotated[str, Field(min_length=1, strip_whitespace=True)]
213
+ PositiveInt = Annotated[int, Field(gt=0)]
214
+ PageSize = Annotated[int, Field(ge=1, le=100)]
215
+
216
+
217
+ # Usage
218
+ class CreatePostRequest(BaseModel):
219
+ title: NonEmptyStr = Field(max_length=500)
220
+ slug: Slug
221
+ body: str = Field(min_length=1)
222
+ category_id: PositiveInt
223
+ ```
224
+
225
+ ---
226
+
227
+ ## Coercion vs Strict Mode
228
+
229
+ ### Default (Coercing)
230
+
231
+ Pydantic coerces compatible types by default:
232
+
233
+ ```python
234
+ class Item(BaseModel):
235
+ count: int
236
+ price: float
237
+
238
+ # These all work:
239
+ Item(count="5", price="9.99") # Strings coerced to numbers
240
+ Item(count=5.0, price=9) # Float to int, int to float
241
+ ```
242
+
243
+ ### Strict Mode
244
+
245
+ ```python
246
+ from pydantic import BaseModel, ConfigDict
247
+
248
+
249
+ class StrictItem(BaseModel):
250
+ model_config = ConfigDict(strict=True)
251
+
252
+ count: int
253
+ price: float
254
+
255
+ # StrictItem(count="5", price="9.99") # ValidationError: int expected
256
+ StrictItem(count=5, price=9.99) # OK
257
+ ```
258
+
259
+ ### Per-Field Strict
260
+
261
+ ```python
262
+ from pydantic import Field
263
+
264
+
265
+ class MixedItem(BaseModel):
266
+ id: int = Field(strict=True) # Must be int
267
+ quantity: int # Coercion allowed
268
+ label: str # Coercion allowed
269
+ ```
270
+
271
+ **Guidance**: Use strict mode for IDs and critical fields. Allow coercion for user-facing inputs where `"5"` meaning `5` is reasonable.
272
+
273
+ ---
274
+
275
+ ## Nested Model Validation
276
+
277
+ ### Composable Schemas
278
+
279
+ ```python
280
+ class Address(BaseModel):
281
+ street: str = Field(min_length=1, max_length=500)
282
+ city: str = Field(min_length=1, max_length=100)
283
+ state: str = Field(min_length=2, max_length=2)
284
+ zip_code: str = Field(pattern=r"^\d{5}(-\d{4})?$")
285
+
286
+
287
+ class CreateOrderRequest(BaseModel):
288
+ items: list[OrderItemRequest] = Field(min_length=1, max_length=50)
289
+ shipping_address: Address
290
+ billing_address: Address | None = None
291
+ notes: str | None = Field(default=None, max_length=1000)
292
+
293
+ @model_validator(mode="after")
294
+ def set_billing_default(self) -> "CreateOrderRequest":
295
+ if self.billing_address is None:
296
+ self.billing_address = self.shipping_address
297
+ return self
298
+
299
+
300
+ class OrderItemRequest(BaseModel):
301
+ product_id: int = Field(gt=0)
302
+ quantity: int = Field(ge=1, le=999)
303
+ ```
304
+
305
+ ---
306
+
307
+ ## Discriminated Unions
308
+
309
+ ### Tagged Unions for Polymorphic Data
310
+
311
+ ```python
312
+ from typing import Literal, Annotated
313
+ from pydantic import BaseModel, Field
314
+
315
+
316
+ class EmailNotification(BaseModel):
317
+ type: Literal["email"]
318
+ to: EmailStr
319
+ subject: str = Field(max_length=200)
320
+ body: str
321
+
322
+
323
+ class SmsNotification(BaseModel):
324
+ type: Literal["sms"]
325
+ phone: str = Field(pattern=r"^\+[1-9]\d{1,14}$")
326
+ message: str = Field(max_length=160)
327
+
328
+
329
+ class WebhookNotification(BaseModel):
330
+ type: Literal["webhook"]
331
+ url: str
332
+ payload: dict
333
+
334
+
335
+ Notification = Annotated[
336
+ EmailNotification | SmsNotification | WebhookNotification,
337
+ Field(discriminator="type"),
338
+ ]
339
+
340
+
341
+ class SendNotificationRequest(BaseModel):
342
+ notifications: list[Notification] = Field(min_length=1, max_length=10)
343
+ ```
344
+
345
+ Pydantic uses the `type` field to determine which model to validate against, giving precise error messages.
346
+
347
+ ---
348
+
349
+ ## Serialization Aliases
350
+
351
+ ### Different Names for API vs Internal
352
+
353
+ ```python
354
+ from pydantic import BaseModel, ConfigDict, Field
355
+
356
+
357
+ class UserResponse(BaseModel):
358
+ model_config = ConfigDict(from_attributes=True, populate_by_name=True)
359
+
360
+ id: int
361
+ email_address: str = Field(alias="email", serialization_alias="email_address")
362
+ full_name: str = Field(alias="name")
363
+ is_active: bool
364
+
365
+
366
+ # From SQLAlchemy model (uses alias to read "email" attribute)
367
+ user_response = UserResponse.model_validate(user_model)
368
+
369
+ # Serialized output uses serialization_alias
370
+ # {"id": 1, "email_address": "a@b.com", "full_name": "Test", "is_active": true}
371
+ ```
372
+
373
+ ### Snake Case to Camel Case
374
+
375
+ ```python
376
+ from pydantic import BaseModel, ConfigDict
377
+ from pydantic.alias_generators import to_camel
378
+
379
+
380
+ class CamelCaseModel(BaseModel):
381
+ model_config = ConfigDict(
382
+ alias_generator=to_camel,
383
+ populate_by_name=True,
384
+ )
385
+
386
+
387
+ class UserResponse(CamelCaseModel):
388
+ id: int
389
+ email: str
390
+ full_name: str
391
+ is_active: bool
392
+
393
+ # Serializes to: {"id": 1, "email": "...", "fullName": "...", "isActive": true}
394
+ # Accepts both: {"full_name": "..."} and {"fullName": "..."}
395
+ ```
396
+
397
+ ---
398
+
399
+ ## OpenAPI Schema Generation
400
+
401
+ ### Automatic from Pydantic
402
+
403
+ FastAPI generates OpenAPI schemas from Pydantic models automatically:
404
+
405
+ ```python
406
+ class CreateUserRequest(BaseModel):
407
+ """Create a new user account."""
408
+
409
+ email: EmailStr = Field(description="User's email address", examples=["user@example.com"])
410
+ name: str = Field(
411
+ min_length=1,
412
+ max_length=255,
413
+ description="Display name",
414
+ examples=["Jane Doe"],
415
+ )
416
+ password: str = Field(
417
+ min_length=8,
418
+ max_length=128,
419
+ description="Account password",
420
+ )
421
+ ```
422
+
423
+ ### JSON Schema Examples
424
+
425
+ ```python
426
+ class CreateUserRequest(BaseModel):
427
+ model_config = ConfigDict(
428
+ json_schema_extra={
429
+ "examples": [
430
+ {
431
+ "email": "jane@example.com",
432
+ "name": "Jane Doe",
433
+ "password": "SecurePass1",
434
+ },
435
+ ],
436
+ },
437
+ )
438
+
439
+ email: EmailStr
440
+ name: str = Field(min_length=1, max_length=255)
441
+ password: str = Field(min_length=8)
442
+ ```
443
+
444
+ ---
445
+
446
+ ## Validation Layers
447
+
448
+ | Layer | Tool | Purpose |
449
+ |---|---|---|
450
+ | HTTP request | FastAPI + Pydantic | Type, format, range validation |
451
+ | Business rules | Service layer | Domain logic (duplicates, permissions) |
452
+ | Database | SQLAlchemy constraints | Data integrity (unique, foreign keys, checks) |
453
+
454
+ **Rule**: Each layer validates its own concerns. Do not rely on a single layer.
455
+
456
+ ---
457
+
458
+ ## Anti-Patterns
459
+
460
+ | Anti-Pattern | Problem | Correct Approach |
461
+ |---|---|---|
462
+ | Validating in route handlers | Scattered logic, hard to test | Use Pydantic schemas |
463
+ | Same schema for create/read | Exposes internal fields | Separate request/response schemas |
464
+ | No `max_length` on strings | Unbounded input, DoS risk | Always set `max_length` |
465
+ | Bare `dict` for request body | No validation, no docs | Use typed Pydantic model |
466
+ | Validating in the database only | Late failure, poor error messages | Validate at entry point too |
467
+ | Mutable default values | Shared state bugs | Use `Field(default_factory=list)` |
468
+
469
+ ---
470
+
471
+ _Validation is the first line of defense. If the data is wrong, reject it immediately with a clear explanation._
@@ -0,0 +1,176 @@
1
+ # Authentication Patterns
2
+
3
+ Rails 8 built-in authentication using session-based auth with bcrypt password hashing.
4
+
5
+ ---
6
+
7
+ ## Philosophy
8
+
9
+ - **Session-based authentication**: Database-backed sessions, not JWTs
10
+ - **Secure by default**: httpOnly cookies, signed session IDs, rate limiting
11
+ - **Device tracking**: Sessions store IP address and user agent for security auditing
12
+ - **Simple model**: User has many Sessions; each login creates a new Session record
13
+
14
+ ---
15
+
16
+ ## Authentication Flow
17
+
18
+ ```
19
+ 1) User submits email + password to SessionsController#create
20
+ 2) User.authenticate_by verifies credentials (bcrypt)
21
+ 3) Server creates Session record with device info
22
+ 4) Signed, permanent, httpOnly cookie stores session_id
23
+ 5) Subsequent requests resume session via cookie lookup
24
+ 6) Current.session provides request-scoped access to authenticated session/user
25
+ ```
26
+
27
+ ---
28
+
29
+ ## Core Components
30
+
31
+ ### Authentication Concern (`app/controllers/concerns/authentication.rb`)
32
+ - Included in `ApplicationController` - all controllers require auth by default
33
+ - `allow_unauthenticated_access` - opt-out for public actions
34
+ - `authenticated?` - helper method for views
35
+ - `Current.session` / `Current.user` - request-scoped user access
36
+
37
+ ### Key Methods
38
+
39
+ | Method | Purpose |
40
+ |--------|---------|
41
+ | `require_authentication` | Before action, redirects to login if no session |
42
+ | `start_new_session_for(user)` | Creates session, sets cookie, tracks device |
43
+ | `terminate_session` | Destroys session, clears cookie |
44
+ | `after_authentication_url` | Return-to URL after login |
45
+
46
+ ### Usage Pattern
47
+
48
+ ```ruby
49
+ # Controller requiring authentication (default)
50
+ class ProjectsController < ApplicationController
51
+ # All actions require auth automatically
52
+ end
53
+
54
+ # Controller with public actions
55
+ class PagesController < ApplicationController
56
+ allow_unauthenticated_access only: %i[home about]
57
+ end
58
+
59
+ # Accessing current user
60
+ def index
61
+ @projects = Current.user.projects
62
+ end
63
+
64
+ # Check in views
65
+ <% if authenticated? %>
66
+ Welcome, <%= Current.user.email_address %>
67
+ <% end %>
68
+ ```
69
+
70
+ ---
71
+
72
+ ## Session Management
73
+
74
+ ### Session Model
75
+ - `belongs_to :user`
76
+ - Tracks `ip_address` and `user_agent` for security
77
+ - Destroyed on logout or password reset
78
+
79
+ ### Session Cookie
80
+ - **Signed**: Tamper-proof via Rails signing
81
+ - **Permanent**: Long-lived (browser remembers)
82
+ - **httpOnly**: Not accessible to JavaScript
83
+ - **SameSite: Lax**: CSRF protection
84
+
85
+ ### Multi-Device Sessions
86
+ Users can have multiple active sessions. Password reset destroys all sessions:
87
+
88
+ ```ruby
89
+ @user.sessions.destroy_all # Force re-login on all devices
90
+ ```
91
+
92
+ ---
93
+
94
+ ## Password Management
95
+
96
+ ### Password Storage
97
+ - `has_secure_password` with bcrypt (`bcrypt` gem)
98
+ - `password_digest` column stores hash
99
+ - Never store or log plaintext passwords
100
+
101
+ ### Password Reset Flow
102
+ ```
103
+ 1) User requests reset via email
104
+ 2) PasswordsMailer sends time-limited token
105
+ 3) User clicks link with token
106
+ 4) Token verified via find_by_password_reset_token!
107
+ 5) Password updated, all sessions destroyed
108
+ ```
109
+
110
+ ### Rate Limiting
111
+ Login and password reset endpoints are rate-limited:
112
+ ```ruby
113
+ rate_limit to: 10, within: 3.minutes, only: :create
114
+ ```
115
+
116
+ ---
117
+
118
+ ## Extending Authentication
119
+
120
+ ### Adding OAuth (Future)
121
+ When adding OAuth providers:
122
+ - Create `Identity` model linking external providers to User
123
+ - Use OmniAuth gem for provider strategies
124
+ - Keep session creation logic in Authentication concern
125
+
126
+ ### Adding API Authentication (Future)
127
+ For API endpoints:
128
+ - Create `ApiToken` model with `authenticate_by_token` method
129
+ - Use separate controller concern for token auth
130
+ - Never mix cookie and token auth in same controller
131
+
132
+ ### Adding MFA (Future)
133
+ For multi-factor authentication:
134
+ - Add `otp_secret` to User model
135
+ - Step-up verification for sensitive actions
136
+ - Consider WebAuthn for hardware key support
137
+
138
+ ---
139
+
140
+ ## Authorization (Not Yet Implemented)
141
+
142
+ Authentication verifies identity; authorization controls access. When adding:
143
+
144
+ ### Recommended Pattern
145
+ ```ruby
146
+ # Policy-based (e.g., Pundit)
147
+ authorize @project
148
+
149
+ # Or ownership checks
150
+ redirect_to root_path unless @project.user == Current.user
151
+ ```
152
+
153
+ ### Scoping Resources
154
+ ```ruby
155
+ # Always scope to current user
156
+ def set_project
157
+ @project = Current.user.projects.find(params[:id])
158
+ end
159
+ ```
160
+
161
+ ---
162
+
163
+ ## Security Checklist
164
+
165
+ - [x] Passwords hashed with bcrypt
166
+ - [x] Session cookies httpOnly and signed
167
+ - [x] Rate limiting on login/reset endpoints
168
+ - [x] Device tracking (IP, user agent)
169
+ - [x] Session invalidation on password change
170
+ - [ ] Account lockout after failed attempts (implement as needed)
171
+ - [ ] Email verification (implement as needed)
172
+ - [ ] MFA support (implement as needed)
173
+
174
+ ---
175
+
176
+ _Document patterns and extension points, not implementation details._