myaidev-method 0.2.8 → 0.2.10
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.
- package/.claude/agents/wordpress-admin.md +271 -0
- package/.env.example +0 -1
- package/PACKAGE_FIXES_SUMMARY.md +319 -0
- package/PAYLOADCMS_AUTH_UPDATE.md +248 -0
- package/USER_GUIDE.md +260 -0
- package/bin/cli.js +70 -0
- package/dist/server/.tsbuildinfo +1 -0
- package/dist/server/auth/controllers/AuthController.d.ts +34 -0
- package/dist/server/auth/controllers/AuthController.d.ts.map +1 -0
- package/dist/server/auth/controllers/AuthController.js +43 -0
- package/dist/server/auth/controllers/AuthController.js.map +1 -0
- package/dist/server/auth/example-usage.d.ts +53 -0
- package/dist/server/auth/example-usage.d.ts.map +1 -0
- package/dist/server/auth/example-usage.js +129 -0
- package/dist/server/auth/example-usage.js.map +1 -0
- package/dist/server/auth/index.d.ts +11 -0
- package/dist/server/auth/index.d.ts.map +1 -0
- package/dist/server/auth/index.js +15 -0
- package/dist/server/auth/index.js.map +1 -0
- package/dist/server/auth/layers.d.ts +19 -0
- package/dist/server/auth/layers.d.ts.map +1 -0
- package/dist/server/auth/layers.js +33 -0
- package/dist/server/auth/layers.js.map +1 -0
- package/dist/server/auth/middleware/authMiddleware.d.ts +24 -0
- package/dist/server/auth/middleware/authMiddleware.d.ts.map +1 -0
- package/dist/server/auth/middleware/authMiddleware.js +65 -0
- package/dist/server/auth/middleware/authMiddleware.js.map +1 -0
- package/dist/server/auth/routes/authRoutes.d.ts +11 -0
- package/dist/server/auth/routes/authRoutes.d.ts.map +1 -0
- package/dist/server/auth/routes/authRoutes.js +213 -0
- package/dist/server/auth/routes/authRoutes.js.map +1 -0
- package/dist/server/auth/services/AuditLogService.d.ts +21 -0
- package/dist/server/auth/services/AuditLogService.d.ts.map +1 -0
- package/dist/server/auth/services/AuditLogService.js +28 -0
- package/dist/server/auth/services/AuditLogService.js.map +1 -0
- package/dist/server/auth/services/AuthService.d.ts +27 -0
- package/dist/server/auth/services/AuthService.d.ts.map +1 -0
- package/dist/server/auth/services/AuthService.js +246 -0
- package/dist/server/auth/services/AuthService.js.map +1 -0
- package/dist/server/auth/services/PasswordService.d.ts +12 -0
- package/dist/server/auth/services/PasswordService.d.ts.map +1 -0
- package/dist/server/auth/services/PasswordService.js +31 -0
- package/dist/server/auth/services/PasswordService.js.map +1 -0
- package/dist/server/auth/services/SessionRepository.d.ts +24 -0
- package/dist/server/auth/services/SessionRepository.d.ts.map +1 -0
- package/dist/server/auth/services/SessionRepository.js +101 -0
- package/dist/server/auth/services/SessionRepository.js.map +1 -0
- package/dist/server/auth/services/TokenService.d.ts +12 -0
- package/dist/server/auth/services/TokenService.d.ts.map +1 -0
- package/dist/server/auth/services/TokenService.js +86 -0
- package/dist/server/auth/services/TokenService.js.map +1 -0
- package/dist/server/auth/services/UserRepository.d.ts +23 -0
- package/dist/server/auth/services/UserRepository.d.ts.map +1 -0
- package/dist/server/auth/services/UserRepository.js +168 -0
- package/dist/server/auth/services/UserRepository.js.map +1 -0
- package/dist/server/auth/services/example.d.ts +26 -0
- package/dist/server/auth/services/example.d.ts.map +1 -0
- package/dist/server/auth/services/example.js +221 -0
- package/dist/server/auth/services/example.js.map +1 -0
- package/dist/server/auth/services/index.d.ts +6 -0
- package/dist/server/auth/services/index.d.ts.map +1 -0
- package/dist/server/auth/services/index.js +7 -0
- package/dist/server/auth/services/index.js.map +1 -0
- package/dist/server/database/db.d.ts +28 -0
- package/dist/server/database/db.d.ts.map +1 -0
- package/dist/server/database/db.js +91 -0
- package/dist/server/database/db.js.map +1 -0
- package/dist/server/database/schema.sql +95 -0
- package/dist/server/hono/app.d.ts +10 -0
- package/dist/server/hono/app.d.ts.map +1 -0
- package/dist/server/hono/app.js +26 -0
- package/dist/server/hono/app.js.map +1 -0
- package/dist/server/hono/routes.d.ts +12 -0
- package/dist/server/hono/routes.d.ts.map +1 -0
- package/dist/server/hono/routes.js +40 -0
- package/dist/server/hono/routes.js.map +1 -0
- package/dist/server/main.d.ts +2 -0
- package/dist/server/main.d.ts.map +1 -0
- package/dist/server/main.js +94 -0
- package/dist/server/main.js.map +1 -0
- package/dist/server/user-management/DirectoryService.d.ts +62 -0
- package/dist/server/user-management/DirectoryService.d.ts.map +1 -0
- package/dist/server/user-management/DirectoryService.js +201 -0
- package/dist/server/user-management/DirectoryService.js.map +1 -0
- package/dist/server/user-management/LinuxUserService.d.ts +71 -0
- package/dist/server/user-management/LinuxUserService.d.ts.map +1 -0
- package/dist/server/user-management/LinuxUserService.js +192 -0
- package/dist/server/user-management/LinuxUserService.js.map +1 -0
- package/dist/server/user-management/QuotaService.d.ts +59 -0
- package/dist/server/user-management/QuotaService.d.ts.map +1 -0
- package/dist/server/user-management/QuotaService.js +148 -0
- package/dist/server/user-management/QuotaService.js.map +1 -0
- package/dist/server/user-management/UserManagementService.d.ts +74 -0
- package/dist/server/user-management/UserManagementService.d.ts.map +1 -0
- package/dist/server/user-management/UserManagementService.js +122 -0
- package/dist/server/user-management/UserManagementService.js.map +1 -0
- package/dist/server/user-management/index.d.ts +26 -0
- package/dist/server/user-management/index.d.ts.map +1 -0
- package/dist/server/user-management/index.js +26 -0
- package/dist/server/user-management/index.js.map +1 -0
- package/dist/server/user-management/layers.d.ts +27 -0
- package/dist/server/user-management/layers.d.ts.map +1 -0
- package/dist/server/user-management/layers.js +37 -0
- package/dist/server/user-management/layers.js.map +1 -0
- package/dist/shared/types.d.ts +94 -0
- package/dist/shared/types.d.ts.map +1 -0
- package/dist/shared/types.js +32 -0
- package/dist/shared/types.js.map +1 -0
- package/package.json +25 -5
- package/src/lib/payloadcms-utils.js +5 -12
- package/src/server/auth/ARCHITECTURE.md +575 -0
- package/src/server/auth/IMPLEMENTATION_SUMMARY.md +287 -0
- package/src/server/auth/QUICK_START.md +283 -0
- package/src/server/auth/README.md +290 -0
- package/src/server/auth/controllers/AuthController.ts +129 -0
- package/src/server/auth/example-usage.ts +159 -0
- package/src/server/auth/index.ts +19 -0
- package/src/server/auth/layers.ts +57 -0
- package/src/server/auth/middleware/authMiddleware.ts +118 -0
- package/src/server/auth/routes/authRoutes.ts +319 -0
- package/src/server/auth/services/AuditLogService.ts +81 -0
- package/src/server/auth/services/AuthService.ts +408 -0
- package/src/server/auth/services/IMPLEMENTATION_SUMMARY.md +404 -0
- package/src/server/auth/services/PasswordService.ts +85 -0
- package/src/server/auth/services/README.md +361 -0
- package/src/server/auth/services/SessionRepository.ts +227 -0
- package/src/server/auth/services/TokenService.ts +174 -0
- package/src/server/auth/services/UserRepository.ts +318 -0
- package/src/server/auth/services/example.ts +346 -0
- package/src/server/auth/services/index.ts +6 -0
- package/src/server/database/db.ts +161 -0
- package/src/server/database/schema.sql +95 -0
- package/src/server/hono/app.ts +41 -0
- package/src/server/main.ts +115 -0
- package/src/server/user-management/DirectoryService.ts +348 -0
- package/src/server/user-management/LinuxUserService.ts +338 -0
- package/src/server/user-management/QuotaService.ts +256 -0
- package/src/server/user-management/README.md +333 -0
- package/src/server/user-management/UserManagementService.ts +335 -0
- package/src/server/user-management/index.ts +26 -0
- package/src/server/user-management/layers.ts +51 -0
- package/src/shared/types.ts +111 -0
- package/src/templates/claude/agents/coolify-deploy.md +50 -50
- package/src/templates/claude/agents/payloadcms-publish.md +46 -18
- package/src/templates/codex/commands/myai-astro-publish.md +8 -2
- package/src/templates/codex/commands/myai-content-writer.md +8 -2
- package/src/templates/codex/commands/myai-coolify-deploy.md +8 -2
- package/src/templates/codex/commands/myai-dev-architect.md +8 -2
- package/src/templates/codex/commands/myai-dev-code.md +8 -2
- package/src/templates/codex/commands/myai-dev-docs.md +8 -2
- package/src/templates/codex/commands/myai-dev-review.md +8 -2
- package/src/templates/codex/commands/myai-dev-test.md +8 -2
- package/src/templates/codex/commands/myai-docusaurus-publish.md +8 -2
- package/src/templates/codex/commands/myai-mintlify-publish.md +8 -2
- package/src/templates/codex/commands/myai-payloadcms-publish.md +17 -3
- package/src/templates/codex/commands/myai-sparc-workflow.md +8 -2
- package/src/templates/codex/commands/myai-wordpress-admin.md +8 -2
- package/src/templates/codex/commands/myai-wordpress-publish.md +8 -2
|
@@ -0,0 +1,361 @@
|
|
|
1
|
+
# Authentication Services
|
|
2
|
+
|
|
3
|
+
This directory contains Effect-TS based authentication services following the ccviewer codebase patterns.
|
|
4
|
+
|
|
5
|
+
## Architecture
|
|
6
|
+
|
|
7
|
+
All services follow these principles:
|
|
8
|
+
|
|
9
|
+
- **Effect-TS Only**: No async/await, no classes except Context.Tag
|
|
10
|
+
- **No Type Casting**: No `as` type casting allowed
|
|
11
|
+
- **Functional Composition**: Use Effect.gen with yield* for composition
|
|
12
|
+
- **Proper Error Handling**: Typed errors (AuthError, ValidationError, DatabaseError)
|
|
13
|
+
- **Layer-based DI**: Context.Tag with Layer.effect or Layer.succeed
|
|
14
|
+
|
|
15
|
+
## Services
|
|
16
|
+
|
|
17
|
+
### 1. PasswordService
|
|
18
|
+
|
|
19
|
+
Handles password hashing and validation.
|
|
20
|
+
|
|
21
|
+
```typescript
|
|
22
|
+
import { PasswordService } from "./services/PasswordService.js";
|
|
23
|
+
|
|
24
|
+
const program = Effect.gen(function* (_) {
|
|
25
|
+
const passwordService = yield* _(PasswordService);
|
|
26
|
+
|
|
27
|
+
// Validate password strength
|
|
28
|
+
yield* _(passwordService.validatePasswordStrength("SecurePass123"));
|
|
29
|
+
|
|
30
|
+
// Hash password
|
|
31
|
+
const hash = yield* _(passwordService.hash("SecurePass123"));
|
|
32
|
+
|
|
33
|
+
// Verify password
|
|
34
|
+
const isValid = yield* _(passwordService.verify("SecurePass123", hash));
|
|
35
|
+
});
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
**Methods:**
|
|
39
|
+
- `hash(password: string)` - Hash password with bcrypt (cost 12)
|
|
40
|
+
- `verify(password: string, hash: string)` - Verify password against hash
|
|
41
|
+
- `validatePasswordStrength(password: string)` - Validate password requirements
|
|
42
|
+
|
|
43
|
+
**Password Requirements:**
|
|
44
|
+
- Minimum 8 characters
|
|
45
|
+
- At least 1 uppercase letter
|
|
46
|
+
- At least 1 lowercase letter
|
|
47
|
+
- At least 1 number
|
|
48
|
+
|
|
49
|
+
### 2. TokenService
|
|
50
|
+
|
|
51
|
+
JWT token generation and verification using RS256 algorithm.
|
|
52
|
+
|
|
53
|
+
```typescript
|
|
54
|
+
import { TokenService } from "./services/TokenService.js";
|
|
55
|
+
|
|
56
|
+
const program = Effect.gen(function* (_) {
|
|
57
|
+
const tokenService = yield* _(TokenService);
|
|
58
|
+
|
|
59
|
+
// Generate JWT token
|
|
60
|
+
const token = yield* _(tokenService.generateToken({
|
|
61
|
+
sub: "user-id",
|
|
62
|
+
username: "johndoe",
|
|
63
|
+
email: "john@example.com",
|
|
64
|
+
jti: "session-id"
|
|
65
|
+
}));
|
|
66
|
+
|
|
67
|
+
// Verify token
|
|
68
|
+
const payload = yield* _(tokenService.verifyToken(token));
|
|
69
|
+
|
|
70
|
+
// Hash token for database storage
|
|
71
|
+
const tokenHash = yield* _(tokenService.hashToken(token));
|
|
72
|
+
});
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
**Methods:**
|
|
76
|
+
- `generateToken(payload)` - Generate JWT with RS256 (7 day expiry)
|
|
77
|
+
- `verifyToken(token)` - Verify token signature and expiration
|
|
78
|
+
- `hashToken(token)` - SHA-256 hash for database storage
|
|
79
|
+
|
|
80
|
+
**Key Generation:**
|
|
81
|
+
- RS256 key pair generated at service initialization
|
|
82
|
+
- Keys stored in memory (can be modified to use environment variables)
|
|
83
|
+
|
|
84
|
+
### 3. UserRepository
|
|
85
|
+
|
|
86
|
+
User CRUD operations with database integration.
|
|
87
|
+
|
|
88
|
+
```typescript
|
|
89
|
+
import { UserRepository } from "./services/UserRepository.js";
|
|
90
|
+
|
|
91
|
+
const program = Effect.gen(function* (_) {
|
|
92
|
+
const userRepo = yield* _(UserRepository);
|
|
93
|
+
|
|
94
|
+
// Create user
|
|
95
|
+
const user = yield* _(userRepo.create({
|
|
96
|
+
username: "johndoe",
|
|
97
|
+
email: "john@example.com",
|
|
98
|
+
passwordHash: "hashed-password",
|
|
99
|
+
linuxUsername: "john-doe"
|
|
100
|
+
}));
|
|
101
|
+
|
|
102
|
+
// Find by email
|
|
103
|
+
const found = yield* _(userRepo.findByEmail("john@example.com"));
|
|
104
|
+
|
|
105
|
+
// Update user
|
|
106
|
+
const updated = yield* _(userRepo.update(user.id, {
|
|
107
|
+
emailVerified: true
|
|
108
|
+
}));
|
|
109
|
+
|
|
110
|
+
// Manage failed logins
|
|
111
|
+
yield* _(userRepo.incrementFailedLogins(user.id));
|
|
112
|
+
yield* _(userRepo.resetFailedLogins(user.id));
|
|
113
|
+
});
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
**Methods:**
|
|
117
|
+
- `create(data)` - Create new user with generated UUID
|
|
118
|
+
- `findById(id)` - Find user by ID
|
|
119
|
+
- `findByEmail(email)` - Find user by email
|
|
120
|
+
- `findByUsername(username)` - Find user by username
|
|
121
|
+
- `update(id, data)` - Update user fields
|
|
122
|
+
- `incrementFailedLogins(id)` - Increment failed login counter
|
|
123
|
+
- `resetFailedLogins(id)` - Reset failed login counter
|
|
124
|
+
|
|
125
|
+
### 4. SessionRepository
|
|
126
|
+
|
|
127
|
+
Session management with token hash storage.
|
|
128
|
+
|
|
129
|
+
```typescript
|
|
130
|
+
import { SessionRepository } from "./services/SessionRepository.js";
|
|
131
|
+
|
|
132
|
+
const program = Effect.gen(function* (_) {
|
|
133
|
+
const sessionRepo = yield* _(SessionRepository);
|
|
134
|
+
|
|
135
|
+
// Create session
|
|
136
|
+
const session = yield* _(sessionRepo.create({
|
|
137
|
+
userId: "user-id",
|
|
138
|
+
tokenHash: "sha256-hash",
|
|
139
|
+
ipAddress: "127.0.0.1",
|
|
140
|
+
userAgent: "Mozilla/5.0",
|
|
141
|
+
expiresAt: Date.now() + 7 * 24 * 60 * 60 * 1000
|
|
142
|
+
}));
|
|
143
|
+
|
|
144
|
+
// Find by token hash
|
|
145
|
+
const found = yield* _(sessionRepo.findByTokenHash("sha256-hash"));
|
|
146
|
+
|
|
147
|
+
// Revoke session
|
|
148
|
+
yield* _(sessionRepo.revoke(session.id));
|
|
149
|
+
|
|
150
|
+
// Revoke all user sessions
|
|
151
|
+
yield* _(sessionRepo.revokeAllForUser("user-id"));
|
|
152
|
+
|
|
153
|
+
// Clean up expired sessions
|
|
154
|
+
const deletedCount = yield* _(sessionRepo.deleteExpired());
|
|
155
|
+
});
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
**Methods:**
|
|
159
|
+
- `create(data)` - Create new session with generated UUID
|
|
160
|
+
- `findById(id)` - Find session by ID
|
|
161
|
+
- `findByTokenHash(tokenHash)` - Find session by token hash
|
|
162
|
+
- `revoke(id)` - Revoke single session
|
|
163
|
+
- `revokeAllForUser(userId)` - Revoke all user sessions
|
|
164
|
+
- `deleteExpired()` - Delete expired sessions (returns count)
|
|
165
|
+
|
|
166
|
+
### 5. AuditLogService
|
|
167
|
+
|
|
168
|
+
Audit logging for security and compliance.
|
|
169
|
+
|
|
170
|
+
```typescript
|
|
171
|
+
import { AuditLogService } from "./services/AuditLogService.js";
|
|
172
|
+
|
|
173
|
+
const program = Effect.gen(function* (_) {
|
|
174
|
+
const auditLog = yield* _(AuditLogService);
|
|
175
|
+
|
|
176
|
+
// Log user action
|
|
177
|
+
yield* _(auditLog.log({
|
|
178
|
+
userId: "user-id",
|
|
179
|
+
action: "USER_LOGIN",
|
|
180
|
+
resourceType: "session",
|
|
181
|
+
resourceId: "session-id",
|
|
182
|
+
ipAddress: "127.0.0.1",
|
|
183
|
+
userAgent: "Mozilla/5.0",
|
|
184
|
+
details: JSON.stringify({ method: "password" })
|
|
185
|
+
}));
|
|
186
|
+
});
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
**Methods:**
|
|
190
|
+
- `log(data)` - Create audit log entry
|
|
191
|
+
|
|
192
|
+
**Predefined Actions:**
|
|
193
|
+
- `USER_REGISTERED` - New user registration
|
|
194
|
+
- `USER_LOGIN` - Successful login
|
|
195
|
+
- `USER_LOGOUT` - User logout
|
|
196
|
+
- `LOGIN_FAILED` - Failed login attempt
|
|
197
|
+
- `PASSWORD_CHANGED` - Password update
|
|
198
|
+
- `PASSWORD_RESET_REQUESTED` - Password reset initiated
|
|
199
|
+
- `PASSWORD_RESET_COMPLETED` - Password reset completed
|
|
200
|
+
- `EMAIL_VERIFIED` - Email verification
|
|
201
|
+
- `EMAIL_CHANGED` - Email address update
|
|
202
|
+
- `PROFILE_UPDATED` - Profile information update
|
|
203
|
+
- `ACCOUNT_LOCKED` - Account locked
|
|
204
|
+
- `ACCOUNT_UNLOCKED` - Account unlocked
|
|
205
|
+
- `SESSION_CREATED` - New session created
|
|
206
|
+
- `SESSION_REVOKED` - Session revoked
|
|
207
|
+
- `TOKEN_REFRESHED` - Token refreshed
|
|
208
|
+
- `OAUTH_LINKED` - OAuth provider linked
|
|
209
|
+
- `OAUTH_UNLINKED` - OAuth provider unlinked
|
|
210
|
+
- `TWO_FACTOR_ENABLED` - 2FA enabled
|
|
211
|
+
- `TWO_FACTOR_DISABLED` - 2FA disabled
|
|
212
|
+
|
|
213
|
+
## Layer Composition
|
|
214
|
+
|
|
215
|
+
Create a complete authentication layer:
|
|
216
|
+
|
|
217
|
+
```typescript
|
|
218
|
+
import { Layer } from "effect";
|
|
219
|
+
import { DatabaseService } from "../../database/db.js";
|
|
220
|
+
import {
|
|
221
|
+
PasswordService,
|
|
222
|
+
TokenService,
|
|
223
|
+
UserRepository,
|
|
224
|
+
SessionRepository,
|
|
225
|
+
AuditLogService
|
|
226
|
+
} from "./services/index.js";
|
|
227
|
+
|
|
228
|
+
// Combine authentication services
|
|
229
|
+
const AuthenticationLayer = Layer.mergeAll(
|
|
230
|
+
PasswordService.Live,
|
|
231
|
+
TokenService.Live,
|
|
232
|
+
UserRepository.Live,
|
|
233
|
+
SessionRepository.Live,
|
|
234
|
+
AuditLogService.Live
|
|
235
|
+
);
|
|
236
|
+
|
|
237
|
+
// Complete application layer with database
|
|
238
|
+
const AppLayer = Layer.mergeAll(
|
|
239
|
+
DatabaseService.Live({ path: "./app.db" }),
|
|
240
|
+
AuthenticationLayer
|
|
241
|
+
);
|
|
242
|
+
|
|
243
|
+
// Use in your program
|
|
244
|
+
const program = Effect.gen(function* (_) {
|
|
245
|
+
// Your authentication logic here
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
Effect.runPromise(program.pipe(Effect.provide(AppLayer)));
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
## Complete Examples
|
|
252
|
+
|
|
253
|
+
See `example.ts` for complete authentication flows:
|
|
254
|
+
|
|
255
|
+
1. **User Registration Flow**
|
|
256
|
+
- Password validation
|
|
257
|
+
- Password hashing
|
|
258
|
+
- User creation
|
|
259
|
+
- Audit logging
|
|
260
|
+
|
|
261
|
+
2. **Login Flow**
|
|
262
|
+
- User lookup
|
|
263
|
+
- Password verification
|
|
264
|
+
- Token generation
|
|
265
|
+
- Session creation
|
|
266
|
+
- Failed login tracking
|
|
267
|
+
- Audit logging
|
|
268
|
+
|
|
269
|
+
3. **Token Verification Flow**
|
|
270
|
+
- JWT verification
|
|
271
|
+
- Session validation
|
|
272
|
+
- User status check
|
|
273
|
+
|
|
274
|
+
4. **Logout Flow**
|
|
275
|
+
- Session revocation
|
|
276
|
+
- Audit logging
|
|
277
|
+
|
|
278
|
+
## Database Schema
|
|
279
|
+
|
|
280
|
+
All services require these database tables (already defined in `src/server/database/schema.sql`):
|
|
281
|
+
|
|
282
|
+
- `users` - User accounts
|
|
283
|
+
- `sessions` - Active sessions with token hashes
|
|
284
|
+
- `audit_logs` - Security audit trail
|
|
285
|
+
|
|
286
|
+
## Error Handling
|
|
287
|
+
|
|
288
|
+
All services use typed errors from `src/shared/types.ts`:
|
|
289
|
+
|
|
290
|
+
- **AuthError** - Authentication/authorization failures
|
|
291
|
+
- **ValidationError** - Input validation failures
|
|
292
|
+
- **DatabaseError** - Database operation failures
|
|
293
|
+
|
|
294
|
+
```typescript
|
|
295
|
+
import { AuthError, ValidationError, DatabaseError } from "../../../shared/types.js";
|
|
296
|
+
|
|
297
|
+
const program = Effect.gen(function* (_) {
|
|
298
|
+
// Handle specific error types
|
|
299
|
+
}).pipe(
|
|
300
|
+
Effect.catchTag("AuthError", (error) =>
|
|
301
|
+
Effect.succeed({ error: error.message })
|
|
302
|
+
),
|
|
303
|
+
Effect.catchTag("ValidationError", (error) =>
|
|
304
|
+
Effect.succeed({ field: error.field, error: error.message })
|
|
305
|
+
)
|
|
306
|
+
);
|
|
307
|
+
```
|
|
308
|
+
|
|
309
|
+
## Testing
|
|
310
|
+
|
|
311
|
+
All services can be tested using Effect test layers:
|
|
312
|
+
|
|
313
|
+
```typescript
|
|
314
|
+
import { expect, test } from "vitest";
|
|
315
|
+
import { Effect, Layer } from "effect";
|
|
316
|
+
import { DatabaseService } from "../../database/db.js";
|
|
317
|
+
import { PasswordService } from "./PasswordService.js";
|
|
318
|
+
|
|
319
|
+
test("password hashing and verification", async () => {
|
|
320
|
+
const program = Effect.gen(function* (_) {
|
|
321
|
+
const passwordService = yield* _(PasswordService);
|
|
322
|
+
const hash = yield* _(passwordService.hash("SecurePass123"));
|
|
323
|
+
const isValid = yield* _(passwordService.verify("SecurePass123", hash));
|
|
324
|
+
return isValid;
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
const testLayer = Layer.mergeAll(
|
|
328
|
+
DatabaseService.Live({ path: ":memory:" }),
|
|
329
|
+
PasswordService.Live
|
|
330
|
+
);
|
|
331
|
+
|
|
332
|
+
const result = await Effect.runPromise(
|
|
333
|
+
program.pipe(Effect.provide(testLayer))
|
|
334
|
+
);
|
|
335
|
+
|
|
336
|
+
expect(result).toBe(true);
|
|
337
|
+
});
|
|
338
|
+
```
|
|
339
|
+
|
|
340
|
+
## Security Considerations
|
|
341
|
+
|
|
342
|
+
1. **Password Storage**: Bcrypt with cost factor 12
|
|
343
|
+
2. **Token Storage**: Never store raw JWT tokens, use SHA-256 hash
|
|
344
|
+
3. **Session Management**: Track and revoke sessions
|
|
345
|
+
4. **Audit Trail**: Log all security-relevant actions
|
|
346
|
+
5. **Failed Login Tracking**: Monitor and limit failed attempts
|
|
347
|
+
6. **Token Expiry**: 7-day expiration (configurable)
|
|
348
|
+
|
|
349
|
+
## Dependencies
|
|
350
|
+
|
|
351
|
+
- `effect` - Effect-TS runtime
|
|
352
|
+
- `bcrypt` - Password hashing
|
|
353
|
+
- `jose` - JWT operations
|
|
354
|
+
- `better-sqlite3` - Database (via DatabaseService)
|
|
355
|
+
- `node:crypto` - UUID generation and SHA-256 hashing
|
|
356
|
+
|
|
357
|
+
## References
|
|
358
|
+
|
|
359
|
+
- Effect-TS documentation: https://effect.website/
|
|
360
|
+
- ccviewer patterns: `/home/ubuntu/projects/myaidev-method/ccviewer/`
|
|
361
|
+
- Database schema: `/home/ubuntu/projects/myaidev-method/src/server/database/schema.sql`
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
import { Context, Effect, Layer } from "effect";
|
|
2
|
+
import { randomUUID } from "node:crypto";
|
|
3
|
+
import { DatabaseService } from "../../database/db.js";
|
|
4
|
+
import { DatabaseError, Session } from "../../../shared/types.js";
|
|
5
|
+
|
|
6
|
+
export interface CreateSessionData {
|
|
7
|
+
userId: string;
|
|
8
|
+
tokenHash: string;
|
|
9
|
+
ipAddress: string | null;
|
|
10
|
+
userAgent: string | null;
|
|
11
|
+
expiresAt: number;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export class SessionRepository extends Context.Tag("SessionRepository")<
|
|
15
|
+
SessionRepository,
|
|
16
|
+
{
|
|
17
|
+
readonly create: (
|
|
18
|
+
data: CreateSessionData
|
|
19
|
+
) => Effect.Effect<Session, DatabaseError>;
|
|
20
|
+
readonly findById: (
|
|
21
|
+
id: string
|
|
22
|
+
) => Effect.Effect<Session | undefined, DatabaseError>;
|
|
23
|
+
readonly findByTokenHash: (
|
|
24
|
+
tokenHash: string
|
|
25
|
+
) => Effect.Effect<Session | undefined, DatabaseError>;
|
|
26
|
+
readonly updateTokenHash: (
|
|
27
|
+
id: string,
|
|
28
|
+
tokenHash: string
|
|
29
|
+
) => Effect.Effect<Session, DatabaseError>;
|
|
30
|
+
readonly revoke: (id: string) => Effect.Effect<void, DatabaseError>;
|
|
31
|
+
readonly revokeAllForUser: (
|
|
32
|
+
userId: string
|
|
33
|
+
) => Effect.Effect<void, DatabaseError>;
|
|
34
|
+
readonly deleteExpired: () => Effect.Effect<number, DatabaseError>;
|
|
35
|
+
}
|
|
36
|
+
>() {
|
|
37
|
+
static Live = Layer.effect(
|
|
38
|
+
this,
|
|
39
|
+
Effect.gen(function* (_) {
|
|
40
|
+
const db = yield* _(DatabaseService);
|
|
41
|
+
|
|
42
|
+
const create = (
|
|
43
|
+
data: CreateSessionData
|
|
44
|
+
): Effect.Effect<Session, DatabaseError> =>
|
|
45
|
+
Effect.gen(function* (_) {
|
|
46
|
+
const id = randomUUID();
|
|
47
|
+
const now = Date.now();
|
|
48
|
+
|
|
49
|
+
yield* _(
|
|
50
|
+
db.run(
|
|
51
|
+
`INSERT INTO sessions (
|
|
52
|
+
id, user_id, token_hash, ip_address, user_agent,
|
|
53
|
+
created_at, expires_at, is_revoked, revoked_at
|
|
54
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
55
|
+
[
|
|
56
|
+
id,
|
|
57
|
+
data.userId,
|
|
58
|
+
data.tokenHash,
|
|
59
|
+
data.ipAddress,
|
|
60
|
+
data.userAgent,
|
|
61
|
+
now,
|
|
62
|
+
data.expiresAt,
|
|
63
|
+
0, // is_revoked = false
|
|
64
|
+
null, // revoked_at = null
|
|
65
|
+
]
|
|
66
|
+
)
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
const session = yield* _(findById(id));
|
|
70
|
+
if (!session) {
|
|
71
|
+
return yield* _(
|
|
72
|
+
Effect.fail(
|
|
73
|
+
new DatabaseError("Failed to retrieve created session")
|
|
74
|
+
)
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return session;
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
const findById = (
|
|
82
|
+
id: string
|
|
83
|
+
): Effect.Effect<Session | undefined, DatabaseError> =>
|
|
84
|
+
Effect.gen(function* (_) {
|
|
85
|
+
const row = yield* _(
|
|
86
|
+
db.get<{
|
|
87
|
+
id: string;
|
|
88
|
+
user_id: string;
|
|
89
|
+
token_hash: string;
|
|
90
|
+
ip_address: string | null;
|
|
91
|
+
user_agent: string | null;
|
|
92
|
+
created_at: number;
|
|
93
|
+
expires_at: number;
|
|
94
|
+
is_revoked: number;
|
|
95
|
+
revoked_at: number | null;
|
|
96
|
+
}>("SELECT * FROM sessions WHERE id = ?", [id])
|
|
97
|
+
);
|
|
98
|
+
|
|
99
|
+
if (!row) {
|
|
100
|
+
return undefined;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return {
|
|
104
|
+
id: row.id,
|
|
105
|
+
userId: row.user_id,
|
|
106
|
+
tokenHash: row.token_hash,
|
|
107
|
+
ipAddress: row.ip_address,
|
|
108
|
+
userAgent: row.user_agent,
|
|
109
|
+
createdAt: row.created_at,
|
|
110
|
+
expiresAt: row.expires_at,
|
|
111
|
+
isRevoked: row.is_revoked === 1,
|
|
112
|
+
revokedAt: row.revoked_at,
|
|
113
|
+
};
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
const findByTokenHash = (
|
|
117
|
+
tokenHash: string
|
|
118
|
+
): Effect.Effect<Session | undefined, DatabaseError> =>
|
|
119
|
+
Effect.gen(function* (_) {
|
|
120
|
+
const row = yield* _(
|
|
121
|
+
db.get<{
|
|
122
|
+
id: string;
|
|
123
|
+
user_id: string;
|
|
124
|
+
token_hash: string;
|
|
125
|
+
ip_address: string | null;
|
|
126
|
+
user_agent: string | null;
|
|
127
|
+
created_at: number;
|
|
128
|
+
expires_at: number;
|
|
129
|
+
is_revoked: number;
|
|
130
|
+
revoked_at: number | null;
|
|
131
|
+
}>("SELECT * FROM sessions WHERE token_hash = ?", [tokenHash])
|
|
132
|
+
);
|
|
133
|
+
|
|
134
|
+
if (!row) {
|
|
135
|
+
return undefined;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return {
|
|
139
|
+
id: row.id,
|
|
140
|
+
userId: row.user_id,
|
|
141
|
+
tokenHash: row.token_hash,
|
|
142
|
+
ipAddress: row.ip_address,
|
|
143
|
+
userAgent: row.user_agent,
|
|
144
|
+
createdAt: row.created_at,
|
|
145
|
+
expiresAt: row.expires_at,
|
|
146
|
+
isRevoked: row.is_revoked === 1,
|
|
147
|
+
revokedAt: row.revoked_at,
|
|
148
|
+
};
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
const updateTokenHash = (
|
|
152
|
+
id: string,
|
|
153
|
+
tokenHash: string
|
|
154
|
+
): Effect.Effect<Session, DatabaseError> =>
|
|
155
|
+
Effect.gen(function* (_) {
|
|
156
|
+
yield* _(
|
|
157
|
+
db.run(
|
|
158
|
+
`UPDATE sessions
|
|
159
|
+
SET token_hash = ?
|
|
160
|
+
WHERE id = ?`,
|
|
161
|
+
[tokenHash, id]
|
|
162
|
+
)
|
|
163
|
+
);
|
|
164
|
+
|
|
165
|
+
const session = yield* _(findById(id));
|
|
166
|
+
if (!session) {
|
|
167
|
+
return yield* _(
|
|
168
|
+
Effect.fail(
|
|
169
|
+
new DatabaseError("Failed to retrieve updated session")
|
|
170
|
+
)
|
|
171
|
+
);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
return session;
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
const revoke = (id: string): Effect.Effect<void, DatabaseError> =>
|
|
178
|
+
Effect.gen(function* (_) {
|
|
179
|
+
yield* _(
|
|
180
|
+
db.run(
|
|
181
|
+
`UPDATE sessions
|
|
182
|
+
SET is_revoked = 1, revoked_at = ?
|
|
183
|
+
WHERE id = ?`,
|
|
184
|
+
[Date.now(), id]
|
|
185
|
+
)
|
|
186
|
+
);
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
const revokeAllForUser = (
|
|
190
|
+
userId: string
|
|
191
|
+
): Effect.Effect<void, DatabaseError> =>
|
|
192
|
+
Effect.gen(function* (_) {
|
|
193
|
+
yield* _(
|
|
194
|
+
db.run(
|
|
195
|
+
`UPDATE sessions
|
|
196
|
+
SET is_revoked = 1, revoked_at = ?
|
|
197
|
+
WHERE user_id = ? AND is_revoked = 0`,
|
|
198
|
+
[Date.now(), userId]
|
|
199
|
+
)
|
|
200
|
+
);
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
const deleteExpired = (): Effect.Effect<number, DatabaseError> =>
|
|
204
|
+
Effect.gen(function* (_) {
|
|
205
|
+
const now = Date.now();
|
|
206
|
+
const result = yield* _(
|
|
207
|
+
db.run<{ changes: number }>(
|
|
208
|
+
`DELETE FROM sessions WHERE expires_at < ?`,
|
|
209
|
+
[now]
|
|
210
|
+
)
|
|
211
|
+
);
|
|
212
|
+
|
|
213
|
+
return result.changes ?? 0;
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
return {
|
|
217
|
+
create,
|
|
218
|
+
findById,
|
|
219
|
+
findByTokenHash,
|
|
220
|
+
updateTokenHash,
|
|
221
|
+
revoke,
|
|
222
|
+
revokeAllForUser,
|
|
223
|
+
deleteExpired,
|
|
224
|
+
};
|
|
225
|
+
})
|
|
226
|
+
);
|
|
227
|
+
}
|