myaidev-method 0.2.7 → 0.2.9

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 (163) hide show
  1. package/.claude/agents/wordpress-admin.md +271 -0
  2. package/.env.example +0 -1
  3. package/COOLIFY_DEPLOYMENT.md +1 -1
  4. package/DEV_WORKFLOW_GUIDE.md +1 -1
  5. package/PACKAGE_FIXES_SUMMARY.md +319 -0
  6. package/PAYLOADCMS_AUTH_UPDATE.md +248 -0
  7. package/PUBLISHING_GUIDE.md +1 -1
  8. package/README.md +7 -7
  9. package/USER_GUIDE.md +261 -1
  10. package/WORDPRESS_ADMIN_SCRIPTS.md +1 -1
  11. package/bin/cli.js +36 -0
  12. package/dist/server/.tsbuildinfo +1 -0
  13. package/dist/server/auth/controllers/AuthController.d.ts +34 -0
  14. package/dist/server/auth/controllers/AuthController.d.ts.map +1 -0
  15. package/dist/server/auth/controllers/AuthController.js +43 -0
  16. package/dist/server/auth/controllers/AuthController.js.map +1 -0
  17. package/dist/server/auth/example-usage.d.ts +53 -0
  18. package/dist/server/auth/example-usage.d.ts.map +1 -0
  19. package/dist/server/auth/example-usage.js +129 -0
  20. package/dist/server/auth/example-usage.js.map +1 -0
  21. package/dist/server/auth/index.d.ts +11 -0
  22. package/dist/server/auth/index.d.ts.map +1 -0
  23. package/dist/server/auth/index.js +15 -0
  24. package/dist/server/auth/index.js.map +1 -0
  25. package/dist/server/auth/layers.d.ts +19 -0
  26. package/dist/server/auth/layers.d.ts.map +1 -0
  27. package/dist/server/auth/layers.js +33 -0
  28. package/dist/server/auth/layers.js.map +1 -0
  29. package/dist/server/auth/middleware/authMiddleware.d.ts +24 -0
  30. package/dist/server/auth/middleware/authMiddleware.d.ts.map +1 -0
  31. package/dist/server/auth/middleware/authMiddleware.js +65 -0
  32. package/dist/server/auth/middleware/authMiddleware.js.map +1 -0
  33. package/dist/server/auth/routes/authRoutes.d.ts +11 -0
  34. package/dist/server/auth/routes/authRoutes.d.ts.map +1 -0
  35. package/dist/server/auth/routes/authRoutes.js +213 -0
  36. package/dist/server/auth/routes/authRoutes.js.map +1 -0
  37. package/dist/server/auth/services/AuditLogService.d.ts +21 -0
  38. package/dist/server/auth/services/AuditLogService.d.ts.map +1 -0
  39. package/dist/server/auth/services/AuditLogService.js +28 -0
  40. package/dist/server/auth/services/AuditLogService.js.map +1 -0
  41. package/dist/server/auth/services/AuthService.d.ts +27 -0
  42. package/dist/server/auth/services/AuthService.d.ts.map +1 -0
  43. package/dist/server/auth/services/AuthService.js +246 -0
  44. package/dist/server/auth/services/AuthService.js.map +1 -0
  45. package/dist/server/auth/services/PasswordService.d.ts +12 -0
  46. package/dist/server/auth/services/PasswordService.d.ts.map +1 -0
  47. package/dist/server/auth/services/PasswordService.js +31 -0
  48. package/dist/server/auth/services/PasswordService.js.map +1 -0
  49. package/dist/server/auth/services/SessionRepository.d.ts +24 -0
  50. package/dist/server/auth/services/SessionRepository.d.ts.map +1 -0
  51. package/dist/server/auth/services/SessionRepository.js +101 -0
  52. package/dist/server/auth/services/SessionRepository.js.map +1 -0
  53. package/dist/server/auth/services/TokenService.d.ts +12 -0
  54. package/dist/server/auth/services/TokenService.d.ts.map +1 -0
  55. package/dist/server/auth/services/TokenService.js +86 -0
  56. package/dist/server/auth/services/TokenService.js.map +1 -0
  57. package/dist/server/auth/services/UserRepository.d.ts +23 -0
  58. package/dist/server/auth/services/UserRepository.d.ts.map +1 -0
  59. package/dist/server/auth/services/UserRepository.js +168 -0
  60. package/dist/server/auth/services/UserRepository.js.map +1 -0
  61. package/dist/server/auth/services/example.d.ts +26 -0
  62. package/dist/server/auth/services/example.d.ts.map +1 -0
  63. package/dist/server/auth/services/example.js +221 -0
  64. package/dist/server/auth/services/example.js.map +1 -0
  65. package/dist/server/auth/services/index.d.ts +6 -0
  66. package/dist/server/auth/services/index.d.ts.map +1 -0
  67. package/dist/server/auth/services/index.js +7 -0
  68. package/dist/server/auth/services/index.js.map +1 -0
  69. package/dist/server/database/db.d.ts +28 -0
  70. package/dist/server/database/db.d.ts.map +1 -0
  71. package/dist/server/database/db.js +91 -0
  72. package/dist/server/database/db.js.map +1 -0
  73. package/dist/server/database/schema.sql +95 -0
  74. package/dist/server/hono/app.d.ts +10 -0
  75. package/dist/server/hono/app.d.ts.map +1 -0
  76. package/dist/server/hono/app.js +26 -0
  77. package/dist/server/hono/app.js.map +1 -0
  78. package/dist/server/hono/routes.d.ts +12 -0
  79. package/dist/server/hono/routes.d.ts.map +1 -0
  80. package/dist/server/hono/routes.js +40 -0
  81. package/dist/server/hono/routes.js.map +1 -0
  82. package/dist/server/main.d.ts +2 -0
  83. package/dist/server/main.d.ts.map +1 -0
  84. package/dist/server/main.js +94 -0
  85. package/dist/server/main.js.map +1 -0
  86. package/dist/server/user-management/DirectoryService.d.ts +62 -0
  87. package/dist/server/user-management/DirectoryService.d.ts.map +1 -0
  88. package/dist/server/user-management/DirectoryService.js +201 -0
  89. package/dist/server/user-management/DirectoryService.js.map +1 -0
  90. package/dist/server/user-management/LinuxUserService.d.ts +71 -0
  91. package/dist/server/user-management/LinuxUserService.d.ts.map +1 -0
  92. package/dist/server/user-management/LinuxUserService.js +192 -0
  93. package/dist/server/user-management/LinuxUserService.js.map +1 -0
  94. package/dist/server/user-management/QuotaService.d.ts +59 -0
  95. package/dist/server/user-management/QuotaService.d.ts.map +1 -0
  96. package/dist/server/user-management/QuotaService.js +148 -0
  97. package/dist/server/user-management/QuotaService.js.map +1 -0
  98. package/dist/server/user-management/UserManagementService.d.ts +74 -0
  99. package/dist/server/user-management/UserManagementService.d.ts.map +1 -0
  100. package/dist/server/user-management/UserManagementService.js +122 -0
  101. package/dist/server/user-management/UserManagementService.js.map +1 -0
  102. package/dist/server/user-management/index.d.ts +26 -0
  103. package/dist/server/user-management/index.d.ts.map +1 -0
  104. package/dist/server/user-management/index.js +26 -0
  105. package/dist/server/user-management/index.js.map +1 -0
  106. package/dist/server/user-management/layers.d.ts +27 -0
  107. package/dist/server/user-management/layers.d.ts.map +1 -0
  108. package/dist/server/user-management/layers.js +37 -0
  109. package/dist/server/user-management/layers.js.map +1 -0
  110. package/dist/shared/types.d.ts +94 -0
  111. package/dist/shared/types.d.ts.map +1 -0
  112. package/dist/shared/types.js +32 -0
  113. package/dist/shared/types.js.map +1 -0
  114. package/package.json +26 -6
  115. package/src/lib/payloadcms-utils.js +5 -12
  116. package/src/server/auth/ARCHITECTURE.md +575 -0
  117. package/src/server/auth/IMPLEMENTATION_SUMMARY.md +287 -0
  118. package/src/server/auth/QUICK_START.md +283 -0
  119. package/src/server/auth/README.md +290 -0
  120. package/src/server/auth/controllers/AuthController.ts +129 -0
  121. package/src/server/auth/example-usage.ts +159 -0
  122. package/src/server/auth/index.ts +19 -0
  123. package/src/server/auth/layers.ts +57 -0
  124. package/src/server/auth/middleware/authMiddleware.ts +118 -0
  125. package/src/server/auth/routes/authRoutes.ts +319 -0
  126. package/src/server/auth/services/AuditLogService.ts +81 -0
  127. package/src/server/auth/services/AuthService.ts +408 -0
  128. package/src/server/auth/services/IMPLEMENTATION_SUMMARY.md +404 -0
  129. package/src/server/auth/services/PasswordService.ts +85 -0
  130. package/src/server/auth/services/README.md +361 -0
  131. package/src/server/auth/services/SessionRepository.ts +227 -0
  132. package/src/server/auth/services/TokenService.ts +174 -0
  133. package/src/server/auth/services/UserRepository.ts +318 -0
  134. package/src/server/auth/services/example.ts +346 -0
  135. package/src/server/auth/services/index.ts +6 -0
  136. package/src/server/database/db.ts +161 -0
  137. package/src/server/database/schema.sql +95 -0
  138. package/src/server/hono/app.ts +41 -0
  139. package/src/server/main.ts +115 -0
  140. package/src/server/user-management/DirectoryService.ts +348 -0
  141. package/src/server/user-management/LinuxUserService.ts +338 -0
  142. package/src/server/user-management/QuotaService.ts +256 -0
  143. package/src/server/user-management/README.md +333 -0
  144. package/src/server/user-management/UserManagementService.ts +335 -0
  145. package/src/server/user-management/index.ts +26 -0
  146. package/src/server/user-management/layers.ts +51 -0
  147. package/src/shared/types.ts +111 -0
  148. package/src/templates/claude/agents/payloadcms-publish.md +34 -14
  149. package/src/templates/codex/commands/myai-astro-publish.md +8 -2
  150. package/src/templates/codex/commands/myai-content-writer.md +8 -2
  151. package/src/templates/codex/commands/myai-coolify-deploy.md +8 -2
  152. package/src/templates/codex/commands/myai-dev-architect.md +8 -2
  153. package/src/templates/codex/commands/myai-dev-code.md +8 -2
  154. package/src/templates/codex/commands/myai-dev-docs.md +8 -2
  155. package/src/templates/codex/commands/myai-dev-review.md +8 -2
  156. package/src/templates/codex/commands/myai-dev-test.md +8 -2
  157. package/src/templates/codex/commands/myai-docusaurus-publish.md +8 -2
  158. package/src/templates/codex/commands/myai-mintlify-publish.md +8 -2
  159. package/src/templates/codex/commands/myai-payloadcms-publish.md +17 -3
  160. package/src/templates/codex/commands/myai-sparc-workflow.md +8 -2
  161. package/src/templates/codex/commands/myai-wordpress-admin.md +8 -2
  162. package/src/templates/codex/commands/myai-wordpress-publish.md +8 -2
  163. package/src/templates/docs/wordpress-troubleshoot.js +2 -2
@@ -0,0 +1,174 @@
1
+ import { Context, Effect, Layer } from "effect";
2
+ import * as jose from "jose";
3
+ import { createHash } from "node:crypto";
4
+ import { AuthError, JWTPayload } from "../../../shared/types.js";
5
+
6
+ const TOKEN_EXPIRY_DAYS = 7;
7
+ const ALGORITHM = "RS256";
8
+
9
+ let keyPair: { publicKey: jose.KeyLike; privateKey: jose.KeyLike } | null = null;
10
+
11
+ export class TokenService extends Context.Tag("TokenService")<
12
+ TokenService,
13
+ {
14
+ readonly generateToken: (
15
+ payload: Omit<JWTPayload, "iat" | "exp">
16
+ ) => Effect.Effect<string, AuthError>;
17
+ readonly verifyToken: (
18
+ token: string
19
+ ) => Effect.Effect<JWTPayload, AuthError>;
20
+ readonly hashToken: (token: string) => Effect.Effect<string, never>;
21
+ }
22
+ >() {
23
+ static Live = Layer.effect(
24
+ this,
25
+ Effect.gen(function* (_) {
26
+ // Generate key pair if not already generated
27
+ if (!keyPair) {
28
+ const { publicKey, privateKey } = yield* _(
29
+ Effect.tryPromise({
30
+ try: () => jose.generateKeyPair(ALGORITHM),
31
+ catch: (error) =>
32
+ new AuthError(
33
+ "KEY_GENERATION_FAILED",
34
+ "Failed to generate RSA key pair",
35
+ error
36
+ ),
37
+ })
38
+ );
39
+ keyPair = { publicKey, privateKey };
40
+ }
41
+
42
+ const generateToken = (
43
+ payload: Omit<JWTPayload, "iat" | "exp">
44
+ ): Effect.Effect<string, AuthError> =>
45
+ Effect.gen(function* (_) {
46
+ const currentKeyPair = keyPair;
47
+ if (!currentKeyPair) {
48
+ return yield* _(
49
+ Effect.fail(
50
+ new AuthError(
51
+ "KEY_NOT_INITIALIZED",
52
+ "Key pair not initialized"
53
+ )
54
+ )
55
+ );
56
+ }
57
+
58
+ const now = Math.floor(Date.now() / 1000);
59
+
60
+ // Type assertion needed for index signature compatibility
61
+ const sub = payload["sub"];
62
+ const username = payload["username"];
63
+ const email = payload["email"];
64
+ const jti = payload["jti"];
65
+
66
+ if (
67
+ typeof sub !== "string" ||
68
+ typeof username !== "string" ||
69
+ typeof email !== "string" ||
70
+ typeof jti !== "string"
71
+ ) {
72
+ return yield* _(
73
+ Effect.fail(
74
+ new AuthError(
75
+ "INVALID_PAYLOAD",
76
+ "Payload contains invalid field types"
77
+ )
78
+ )
79
+ );
80
+ }
81
+
82
+ const fullPayload: JWTPayload = {
83
+ sub,
84
+ username,
85
+ email,
86
+ jti,
87
+ iat: now,
88
+ exp: now + TOKEN_EXPIRY_DAYS * 24 * 60 * 60,
89
+ };
90
+
91
+ return yield* _(
92
+ Effect.tryPromise({
93
+ try: () =>
94
+ new jose.SignJWT(fullPayload)
95
+ .setProtectedHeader({ alg: ALGORITHM })
96
+ .setIssuedAt(fullPayload.iat)
97
+ .setExpirationTime(fullPayload.exp)
98
+ .sign(currentKeyPair.privateKey),
99
+ catch: (error) =>
100
+ new AuthError(
101
+ "TOKEN_GENERATION_FAILED",
102
+ "Failed to generate JWT token",
103
+ error
104
+ ),
105
+ })
106
+ );
107
+ });
108
+
109
+ const verifyToken = (token: string): Effect.Effect<JWTPayload, AuthError> =>
110
+ Effect.gen(function* (_) {
111
+ const currentKeyPair = keyPair;
112
+ if (!currentKeyPair) {
113
+ return yield* _(
114
+ Effect.fail(
115
+ new AuthError(
116
+ "KEY_NOT_INITIALIZED",
117
+ "Key pair not initialized"
118
+ )
119
+ )
120
+ );
121
+ }
122
+
123
+ const result = yield* _(
124
+ Effect.tryPromise({
125
+ try: () => jose.jwtVerify(token, currentKeyPair.publicKey),
126
+ catch: (error) =>
127
+ new AuthError(
128
+ "TOKEN_VERIFICATION_FAILED",
129
+ "Invalid or expired token",
130
+ error
131
+ ),
132
+ })
133
+ );
134
+
135
+ const payload = result.payload;
136
+
137
+ // Validate payload structure using bracket notation for index signature properties
138
+ if (
139
+ typeof payload.sub !== "string" ||
140
+ typeof payload["username"] !== "string" ||
141
+ typeof payload["email"] !== "string" ||
142
+ typeof payload.iat !== "number" ||
143
+ typeof payload.exp !== "number" ||
144
+ typeof payload.jti !== "string"
145
+ ) {
146
+ return yield* _(
147
+ Effect.fail(
148
+ new AuthError(
149
+ "INVALID_TOKEN_PAYLOAD",
150
+ "Token payload is missing required fields"
151
+ )
152
+ )
153
+ );
154
+ }
155
+
156
+ return {
157
+ sub: payload.sub,
158
+ username: payload["username"],
159
+ email: payload["email"],
160
+ iat: payload.iat,
161
+ exp: payload.exp,
162
+ jti: payload.jti,
163
+ };
164
+ });
165
+
166
+ const hashToken = (token: string): Effect.Effect<string, never> =>
167
+ Effect.sync(() => {
168
+ return createHash("sha256").update(token).digest("hex");
169
+ });
170
+
171
+ return { generateToken, verifyToken, hashToken };
172
+ })
173
+ );
174
+ }
@@ -0,0 +1,318 @@
1
+ import { Context, Effect, Layer } from "effect";
2
+ import { randomUUID } from "node:crypto";
3
+ import { DatabaseService } from "../../database/db.js";
4
+ import { DatabaseError, User } from "../../../shared/types.js";
5
+
6
+ export interface CreateUserData {
7
+ username: string;
8
+ email: string;
9
+ passwordHash: string | null;
10
+ linuxUsername: string;
11
+ }
12
+
13
+ export class UserRepository extends Context.Tag("UserRepository")<
14
+ UserRepository,
15
+ {
16
+ readonly create: (
17
+ data: CreateUserData
18
+ ) => Effect.Effect<User, DatabaseError>;
19
+ readonly findById: (
20
+ id: string
21
+ ) => Effect.Effect<User | undefined, DatabaseError>;
22
+ readonly findByEmail: (
23
+ email: string
24
+ ) => Effect.Effect<User | undefined, DatabaseError>;
25
+ readonly findByUsername: (
26
+ username: string
27
+ ) => Effect.Effect<User | undefined, DatabaseError>;
28
+ readonly update: (
29
+ id: string,
30
+ data: Partial<User>
31
+ ) => Effect.Effect<User, DatabaseError>;
32
+ readonly incrementFailedLogins: (
33
+ id: string
34
+ ) => Effect.Effect<void, DatabaseError>;
35
+ readonly resetFailedLogins: (
36
+ id: string
37
+ ) => Effect.Effect<void, DatabaseError>;
38
+ }
39
+ >() {
40
+ static Live = Layer.effect(
41
+ this,
42
+ Effect.gen(function* (_) {
43
+ const db = yield* _(DatabaseService);
44
+
45
+ const create = (data: CreateUserData): Effect.Effect<User, DatabaseError> =>
46
+ Effect.gen(function* (_) {
47
+ const id = randomUUID();
48
+ const now = Date.now();
49
+
50
+ yield* _(
51
+ db.run(
52
+ `INSERT INTO users (
53
+ id, username, email, password_hash, linux_username,
54
+ created_at, updated_at, is_active, email_verified,
55
+ failed_login_attempts, last_login_at
56
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
57
+ [
58
+ id,
59
+ data.username,
60
+ data.email,
61
+ data.passwordHash,
62
+ data.linuxUsername,
63
+ now,
64
+ now,
65
+ 1, // is_active = true
66
+ 0, // email_verified = false
67
+ 0, // failed_login_attempts = 0
68
+ null, // last_login_at = null
69
+ ]
70
+ )
71
+ );
72
+
73
+ const user = yield* _(findById(id));
74
+ if (!user) {
75
+ return yield* _(
76
+ Effect.fail(
77
+ new DatabaseError("Failed to retrieve created user")
78
+ )
79
+ );
80
+ }
81
+
82
+ return user;
83
+ });
84
+
85
+ const findById = (
86
+ id: string
87
+ ): Effect.Effect<User | undefined, DatabaseError> =>
88
+ Effect.gen(function* (_) {
89
+ const row = yield* _(
90
+ db.get<{
91
+ id: string;
92
+ username: string;
93
+ email: string;
94
+ password_hash: string | null;
95
+ linux_username: string;
96
+ created_at: number;
97
+ updated_at: number;
98
+ is_active: number;
99
+ email_verified: number;
100
+ failed_login_attempts: number;
101
+ last_login_at: number | null;
102
+ }>("SELECT * FROM users WHERE id = ?", [id])
103
+ );
104
+
105
+ if (!row) {
106
+ return undefined;
107
+ }
108
+
109
+ return {
110
+ id: row.id,
111
+ username: row.username,
112
+ email: row.email,
113
+ passwordHash: row.password_hash,
114
+ linuxUsername: row.linux_username,
115
+ createdAt: row.created_at,
116
+ updatedAt: row.updated_at,
117
+ isActive: row.is_active === 1,
118
+ emailVerified: row.email_verified === 1,
119
+ failedLoginAttempts: row.failed_login_attempts,
120
+ lastLoginAt: row.last_login_at,
121
+ };
122
+ });
123
+
124
+ const findByEmail = (
125
+ email: string
126
+ ): Effect.Effect<User | undefined, DatabaseError> =>
127
+ Effect.gen(function* (_) {
128
+ const row = yield* _(
129
+ db.get<{
130
+ id: string;
131
+ username: string;
132
+ email: string;
133
+ password_hash: string | null;
134
+ linux_username: string;
135
+ created_at: number;
136
+ updated_at: number;
137
+ is_active: number;
138
+ email_verified: number;
139
+ failed_login_attempts: number;
140
+ last_login_at: number | null;
141
+ }>("SELECT * FROM users WHERE email = ?", [email])
142
+ );
143
+
144
+ if (!row) {
145
+ return undefined;
146
+ }
147
+
148
+ return {
149
+ id: row.id,
150
+ username: row.username,
151
+ email: row.email,
152
+ passwordHash: row.password_hash,
153
+ linuxUsername: row.linux_username,
154
+ createdAt: row.created_at,
155
+ updatedAt: row.updated_at,
156
+ isActive: row.is_active === 1,
157
+ emailVerified: row.email_verified === 1,
158
+ failedLoginAttempts: row.failed_login_attempts,
159
+ lastLoginAt: row.last_login_at,
160
+ };
161
+ });
162
+
163
+ const findByUsername = (
164
+ username: string
165
+ ): Effect.Effect<User | undefined, DatabaseError> =>
166
+ Effect.gen(function* (_) {
167
+ const row = yield* _(
168
+ db.get<{
169
+ id: string;
170
+ username: string;
171
+ email: string;
172
+ password_hash: string | null;
173
+ linux_username: string;
174
+ created_at: number;
175
+ updated_at: number;
176
+ is_active: number;
177
+ email_verified: number;
178
+ failed_login_attempts: number;
179
+ last_login_at: number | null;
180
+ }>("SELECT * FROM users WHERE username = ?", [username])
181
+ );
182
+
183
+ if (!row) {
184
+ return undefined;
185
+ }
186
+
187
+ return {
188
+ id: row.id,
189
+ username: row.username,
190
+ email: row.email,
191
+ passwordHash: row.password_hash,
192
+ linuxUsername: row.linux_username,
193
+ createdAt: row.created_at,
194
+ updatedAt: row.updated_at,
195
+ isActive: row.is_active === 1,
196
+ emailVerified: row.email_verified === 1,
197
+ failedLoginAttempts: row.failed_login_attempts,
198
+ lastLoginAt: row.last_login_at,
199
+ };
200
+ });
201
+
202
+ const update = (
203
+ id: string,
204
+ data: Partial<User>
205
+ ): Effect.Effect<User, DatabaseError> =>
206
+ Effect.gen(function* (_) {
207
+ const updateFields: string[] = [];
208
+ const updateValues: unknown[] = [];
209
+
210
+ if (data.username !== undefined) {
211
+ updateFields.push("username = ?");
212
+ updateValues.push(data.username);
213
+ }
214
+ if (data.email !== undefined) {
215
+ updateFields.push("email = ?");
216
+ updateValues.push(data.email);
217
+ }
218
+ if (data.passwordHash !== undefined) {
219
+ updateFields.push("password_hash = ?");
220
+ updateValues.push(data.passwordHash);
221
+ }
222
+ if (data.linuxUsername !== undefined) {
223
+ updateFields.push("linux_username = ?");
224
+ updateValues.push(data.linuxUsername);
225
+ }
226
+ if (data.isActive !== undefined) {
227
+ updateFields.push("is_active = ?");
228
+ updateValues.push(data.isActive ? 1 : 0);
229
+ }
230
+ if (data.emailVerified !== undefined) {
231
+ updateFields.push("email_verified = ?");
232
+ updateValues.push(data.emailVerified ? 1 : 0);
233
+ }
234
+ if (data.failedLoginAttempts !== undefined) {
235
+ updateFields.push("failed_login_attempts = ?");
236
+ updateValues.push(data.failedLoginAttempts);
237
+ }
238
+ if (data.lastLoginAt !== undefined) {
239
+ updateFields.push("last_login_at = ?");
240
+ updateValues.push(data.lastLoginAt);
241
+ }
242
+
243
+ // Always update updated_at
244
+ updateFields.push("updated_at = ?");
245
+ updateValues.push(Date.now());
246
+
247
+ if (updateFields.length === 1) {
248
+ // Only updated_at, nothing to update
249
+ const user = yield* _(findById(id));
250
+ if (!user) {
251
+ return yield* _(
252
+ Effect.fail(new DatabaseError("User not found"))
253
+ );
254
+ }
255
+ return user;
256
+ }
257
+
258
+ updateValues.push(id);
259
+
260
+ yield* _(
261
+ db.run(
262
+ `UPDATE users SET ${updateFields.join(", ")} WHERE id = ?`,
263
+ updateValues
264
+ )
265
+ );
266
+
267
+ const user = yield* _(findById(id));
268
+ if (!user) {
269
+ return yield* _(
270
+ Effect.fail(new DatabaseError("User not found after update"))
271
+ );
272
+ }
273
+
274
+ return user;
275
+ });
276
+
277
+ const incrementFailedLogins = (
278
+ id: string
279
+ ): Effect.Effect<void, DatabaseError> =>
280
+ Effect.gen(function* (_) {
281
+ yield* _(
282
+ db.run(
283
+ `UPDATE users
284
+ SET failed_login_attempts = failed_login_attempts + 1,
285
+ updated_at = ?
286
+ WHERE id = ?`,
287
+ [Date.now(), id]
288
+ )
289
+ );
290
+ });
291
+
292
+ const resetFailedLogins = (
293
+ id: string
294
+ ): Effect.Effect<void, DatabaseError> =>
295
+ Effect.gen(function* (_) {
296
+ yield* _(
297
+ db.run(
298
+ `UPDATE users
299
+ SET failed_login_attempts = 0,
300
+ updated_at = ?
301
+ WHERE id = ?`,
302
+ [Date.now(), id]
303
+ )
304
+ );
305
+ });
306
+
307
+ return {
308
+ create,
309
+ findById,
310
+ findByEmail,
311
+ findByUsername,
312
+ update,
313
+ incrementFailedLogins,
314
+ resetFailedLogins,
315
+ };
316
+ })
317
+ );
318
+ }