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.
Files changed (158) hide show
  1. package/.claude/agents/wordpress-admin.md +271 -0
  2. package/.env.example +0 -1
  3. package/PACKAGE_FIXES_SUMMARY.md +319 -0
  4. package/PAYLOADCMS_AUTH_UPDATE.md +248 -0
  5. package/USER_GUIDE.md +260 -0
  6. package/bin/cli.js +70 -0
  7. package/dist/server/.tsbuildinfo +1 -0
  8. package/dist/server/auth/controllers/AuthController.d.ts +34 -0
  9. package/dist/server/auth/controllers/AuthController.d.ts.map +1 -0
  10. package/dist/server/auth/controllers/AuthController.js +43 -0
  11. package/dist/server/auth/controllers/AuthController.js.map +1 -0
  12. package/dist/server/auth/example-usage.d.ts +53 -0
  13. package/dist/server/auth/example-usage.d.ts.map +1 -0
  14. package/dist/server/auth/example-usage.js +129 -0
  15. package/dist/server/auth/example-usage.js.map +1 -0
  16. package/dist/server/auth/index.d.ts +11 -0
  17. package/dist/server/auth/index.d.ts.map +1 -0
  18. package/dist/server/auth/index.js +15 -0
  19. package/dist/server/auth/index.js.map +1 -0
  20. package/dist/server/auth/layers.d.ts +19 -0
  21. package/dist/server/auth/layers.d.ts.map +1 -0
  22. package/dist/server/auth/layers.js +33 -0
  23. package/dist/server/auth/layers.js.map +1 -0
  24. package/dist/server/auth/middleware/authMiddleware.d.ts +24 -0
  25. package/dist/server/auth/middleware/authMiddleware.d.ts.map +1 -0
  26. package/dist/server/auth/middleware/authMiddleware.js +65 -0
  27. package/dist/server/auth/middleware/authMiddleware.js.map +1 -0
  28. package/dist/server/auth/routes/authRoutes.d.ts +11 -0
  29. package/dist/server/auth/routes/authRoutes.d.ts.map +1 -0
  30. package/dist/server/auth/routes/authRoutes.js +213 -0
  31. package/dist/server/auth/routes/authRoutes.js.map +1 -0
  32. package/dist/server/auth/services/AuditLogService.d.ts +21 -0
  33. package/dist/server/auth/services/AuditLogService.d.ts.map +1 -0
  34. package/dist/server/auth/services/AuditLogService.js +28 -0
  35. package/dist/server/auth/services/AuditLogService.js.map +1 -0
  36. package/dist/server/auth/services/AuthService.d.ts +27 -0
  37. package/dist/server/auth/services/AuthService.d.ts.map +1 -0
  38. package/dist/server/auth/services/AuthService.js +246 -0
  39. package/dist/server/auth/services/AuthService.js.map +1 -0
  40. package/dist/server/auth/services/PasswordService.d.ts +12 -0
  41. package/dist/server/auth/services/PasswordService.d.ts.map +1 -0
  42. package/dist/server/auth/services/PasswordService.js +31 -0
  43. package/dist/server/auth/services/PasswordService.js.map +1 -0
  44. package/dist/server/auth/services/SessionRepository.d.ts +24 -0
  45. package/dist/server/auth/services/SessionRepository.d.ts.map +1 -0
  46. package/dist/server/auth/services/SessionRepository.js +101 -0
  47. package/dist/server/auth/services/SessionRepository.js.map +1 -0
  48. package/dist/server/auth/services/TokenService.d.ts +12 -0
  49. package/dist/server/auth/services/TokenService.d.ts.map +1 -0
  50. package/dist/server/auth/services/TokenService.js +86 -0
  51. package/dist/server/auth/services/TokenService.js.map +1 -0
  52. package/dist/server/auth/services/UserRepository.d.ts +23 -0
  53. package/dist/server/auth/services/UserRepository.d.ts.map +1 -0
  54. package/dist/server/auth/services/UserRepository.js +168 -0
  55. package/dist/server/auth/services/UserRepository.js.map +1 -0
  56. package/dist/server/auth/services/example.d.ts +26 -0
  57. package/dist/server/auth/services/example.d.ts.map +1 -0
  58. package/dist/server/auth/services/example.js +221 -0
  59. package/dist/server/auth/services/example.js.map +1 -0
  60. package/dist/server/auth/services/index.d.ts +6 -0
  61. package/dist/server/auth/services/index.d.ts.map +1 -0
  62. package/dist/server/auth/services/index.js +7 -0
  63. package/dist/server/auth/services/index.js.map +1 -0
  64. package/dist/server/database/db.d.ts +28 -0
  65. package/dist/server/database/db.d.ts.map +1 -0
  66. package/dist/server/database/db.js +91 -0
  67. package/dist/server/database/db.js.map +1 -0
  68. package/dist/server/database/schema.sql +95 -0
  69. package/dist/server/hono/app.d.ts +10 -0
  70. package/dist/server/hono/app.d.ts.map +1 -0
  71. package/dist/server/hono/app.js +26 -0
  72. package/dist/server/hono/app.js.map +1 -0
  73. package/dist/server/hono/routes.d.ts +12 -0
  74. package/dist/server/hono/routes.d.ts.map +1 -0
  75. package/dist/server/hono/routes.js +40 -0
  76. package/dist/server/hono/routes.js.map +1 -0
  77. package/dist/server/main.d.ts +2 -0
  78. package/dist/server/main.d.ts.map +1 -0
  79. package/dist/server/main.js +94 -0
  80. package/dist/server/main.js.map +1 -0
  81. package/dist/server/user-management/DirectoryService.d.ts +62 -0
  82. package/dist/server/user-management/DirectoryService.d.ts.map +1 -0
  83. package/dist/server/user-management/DirectoryService.js +201 -0
  84. package/dist/server/user-management/DirectoryService.js.map +1 -0
  85. package/dist/server/user-management/LinuxUserService.d.ts +71 -0
  86. package/dist/server/user-management/LinuxUserService.d.ts.map +1 -0
  87. package/dist/server/user-management/LinuxUserService.js +192 -0
  88. package/dist/server/user-management/LinuxUserService.js.map +1 -0
  89. package/dist/server/user-management/QuotaService.d.ts +59 -0
  90. package/dist/server/user-management/QuotaService.d.ts.map +1 -0
  91. package/dist/server/user-management/QuotaService.js +148 -0
  92. package/dist/server/user-management/QuotaService.js.map +1 -0
  93. package/dist/server/user-management/UserManagementService.d.ts +74 -0
  94. package/dist/server/user-management/UserManagementService.d.ts.map +1 -0
  95. package/dist/server/user-management/UserManagementService.js +122 -0
  96. package/dist/server/user-management/UserManagementService.js.map +1 -0
  97. package/dist/server/user-management/index.d.ts +26 -0
  98. package/dist/server/user-management/index.d.ts.map +1 -0
  99. package/dist/server/user-management/index.js +26 -0
  100. package/dist/server/user-management/index.js.map +1 -0
  101. package/dist/server/user-management/layers.d.ts +27 -0
  102. package/dist/server/user-management/layers.d.ts.map +1 -0
  103. package/dist/server/user-management/layers.js +37 -0
  104. package/dist/server/user-management/layers.js.map +1 -0
  105. package/dist/shared/types.d.ts +94 -0
  106. package/dist/shared/types.d.ts.map +1 -0
  107. package/dist/shared/types.js +32 -0
  108. package/dist/shared/types.js.map +1 -0
  109. package/package.json +25 -5
  110. package/src/lib/payloadcms-utils.js +5 -12
  111. package/src/server/auth/ARCHITECTURE.md +575 -0
  112. package/src/server/auth/IMPLEMENTATION_SUMMARY.md +287 -0
  113. package/src/server/auth/QUICK_START.md +283 -0
  114. package/src/server/auth/README.md +290 -0
  115. package/src/server/auth/controllers/AuthController.ts +129 -0
  116. package/src/server/auth/example-usage.ts +159 -0
  117. package/src/server/auth/index.ts +19 -0
  118. package/src/server/auth/layers.ts +57 -0
  119. package/src/server/auth/middleware/authMiddleware.ts +118 -0
  120. package/src/server/auth/routes/authRoutes.ts +319 -0
  121. package/src/server/auth/services/AuditLogService.ts +81 -0
  122. package/src/server/auth/services/AuthService.ts +408 -0
  123. package/src/server/auth/services/IMPLEMENTATION_SUMMARY.md +404 -0
  124. package/src/server/auth/services/PasswordService.ts +85 -0
  125. package/src/server/auth/services/README.md +361 -0
  126. package/src/server/auth/services/SessionRepository.ts +227 -0
  127. package/src/server/auth/services/TokenService.ts +174 -0
  128. package/src/server/auth/services/UserRepository.ts +318 -0
  129. package/src/server/auth/services/example.ts +346 -0
  130. package/src/server/auth/services/index.ts +6 -0
  131. package/src/server/database/db.ts +161 -0
  132. package/src/server/database/schema.sql +95 -0
  133. package/src/server/hono/app.ts +41 -0
  134. package/src/server/main.ts +115 -0
  135. package/src/server/user-management/DirectoryService.ts +348 -0
  136. package/src/server/user-management/LinuxUserService.ts +338 -0
  137. package/src/server/user-management/QuotaService.ts +256 -0
  138. package/src/server/user-management/README.md +333 -0
  139. package/src/server/user-management/UserManagementService.ts +335 -0
  140. package/src/server/user-management/index.ts +26 -0
  141. package/src/server/user-management/layers.ts +51 -0
  142. package/src/shared/types.ts +111 -0
  143. package/src/templates/claude/agents/coolify-deploy.md +50 -50
  144. package/src/templates/claude/agents/payloadcms-publish.md +46 -18
  145. package/src/templates/codex/commands/myai-astro-publish.md +8 -2
  146. package/src/templates/codex/commands/myai-content-writer.md +8 -2
  147. package/src/templates/codex/commands/myai-coolify-deploy.md +8 -2
  148. package/src/templates/codex/commands/myai-dev-architect.md +8 -2
  149. package/src/templates/codex/commands/myai-dev-code.md +8 -2
  150. package/src/templates/codex/commands/myai-dev-docs.md +8 -2
  151. package/src/templates/codex/commands/myai-dev-review.md +8 -2
  152. package/src/templates/codex/commands/myai-dev-test.md +8 -2
  153. package/src/templates/codex/commands/myai-docusaurus-publish.md +8 -2
  154. package/src/templates/codex/commands/myai-mintlify-publish.md +8 -2
  155. package/src/templates/codex/commands/myai-payloadcms-publish.md +17 -3
  156. package/src/templates/codex/commands/myai-sparc-workflow.md +8 -2
  157. package/src/templates/codex/commands/myai-wordpress-admin.md +8 -2
  158. package/src/templates/codex/commands/myai-wordpress-publish.md +8 -2
@@ -0,0 +1,346 @@
1
+ /**
2
+ * Example usage of authentication services
3
+ *
4
+ * This file demonstrates how to use all authentication services together
5
+ * following Effect-TS patterns from the ccviewer codebase.
6
+ */
7
+
8
+ import { Effect, Layer } from "effect";
9
+ import { DatabaseService } from "../../database/db.js";
10
+ import {
11
+ PasswordService,
12
+ TokenService,
13
+ UserRepository,
14
+ SessionRepository,
15
+ AuditLogService,
16
+ type CreateUserData,
17
+ } from "./index.js";
18
+
19
+ // Example: Complete user registration flow
20
+ export const registerUser = (
21
+ username: string,
22
+ email: string,
23
+ password: string,
24
+ linuxUsername: string,
25
+ ipAddress: string | null,
26
+ userAgent: string | null
27
+ ) =>
28
+ Effect.gen(function* (_) {
29
+ // Get services
30
+ const passwordService = yield* _(PasswordService);
31
+ const userRepo = yield* _(UserRepository);
32
+ const auditLog = yield* _(AuditLogService);
33
+
34
+ // Validate password strength
35
+ yield* _(passwordService.validatePasswordStrength(password));
36
+
37
+ // Hash the password
38
+ const passwordHash = yield* _(passwordService.hash(password));
39
+
40
+ // Create user data
41
+ const userData: CreateUserData = {
42
+ username,
43
+ email,
44
+ passwordHash,
45
+ linuxUsername,
46
+ };
47
+
48
+ // Create user in database
49
+ const user = yield* _(userRepo.create(userData));
50
+
51
+ // Log the registration
52
+ yield* _(
53
+ auditLog.log({
54
+ userId: user.id,
55
+ action: "USER_REGISTERED",
56
+ ipAddress,
57
+ userAgent,
58
+ details: JSON.stringify({ username, email }),
59
+ })
60
+ );
61
+
62
+ return user;
63
+ });
64
+
65
+ // Example: Complete login flow
66
+ export const loginUser = (
67
+ email: string,
68
+ password: string,
69
+ ipAddress: string | null,
70
+ userAgent: string | null
71
+ ) =>
72
+ Effect.gen(function* (_) {
73
+ // Get services
74
+ const passwordService = yield* _(PasswordService);
75
+ const tokenService = yield* _(TokenService);
76
+ const userRepo = yield* _(UserRepository);
77
+ const sessionRepo = yield* _(SessionRepository);
78
+ const auditLog = yield* _(AuditLogService);
79
+
80
+ // Find user by email
81
+ const user = yield* _(userRepo.findByEmail(email));
82
+
83
+ if (!user) {
84
+ // Log failed login attempt
85
+ yield* _(
86
+ auditLog.log({
87
+ userId: null,
88
+ action: "LOGIN_FAILED",
89
+ ipAddress,
90
+ userAgent,
91
+ details: JSON.stringify({ email, reason: "User not found" }),
92
+ })
93
+ );
94
+ return yield* _(
95
+ Effect.fail(new Error("Invalid email or password"))
96
+ );
97
+ }
98
+
99
+ // Check if user is active
100
+ if (!user.isActive) {
101
+ yield* _(
102
+ auditLog.log({
103
+ userId: user.id,
104
+ action: "LOGIN_FAILED",
105
+ ipAddress,
106
+ userAgent,
107
+ details: JSON.stringify({ reason: "Account inactive" }),
108
+ })
109
+ );
110
+ return yield* _(Effect.fail(new Error("Account is inactive")));
111
+ }
112
+
113
+ // Check if password hash exists
114
+ if (!user.passwordHash) {
115
+ yield* _(
116
+ auditLog.log({
117
+ userId: user.id,
118
+ action: "LOGIN_FAILED",
119
+ ipAddress,
120
+ userAgent,
121
+ details: JSON.stringify({ reason: "No password set" }),
122
+ })
123
+ );
124
+ return yield* _(Effect.fail(new Error("Invalid email or password")));
125
+ }
126
+
127
+ // Verify password
128
+ const isValid = yield* _(
129
+ passwordService.verify(password, user.passwordHash)
130
+ );
131
+
132
+ if (!isValid) {
133
+ // Increment failed login attempts
134
+ yield* _(userRepo.incrementFailedLogins(user.id));
135
+
136
+ yield* _(
137
+ auditLog.log({
138
+ userId: user.id,
139
+ action: "LOGIN_FAILED",
140
+ ipAddress,
141
+ userAgent,
142
+ details: JSON.stringify({ reason: "Invalid password" }),
143
+ })
144
+ );
145
+ return yield* _(Effect.fail(new Error("Invalid email or password")));
146
+ }
147
+
148
+ // Reset failed login attempts on successful login
149
+ yield* _(userRepo.resetFailedLogins(user.id));
150
+
151
+ // Generate JWT token
152
+ const tokenPayload = {
153
+ sub: user.id,
154
+ username: user.username,
155
+ email: user.email,
156
+ jti: "", // Will be set to session ID
157
+ };
158
+
159
+ const token = yield* _(tokenService.generateToken(tokenPayload));
160
+
161
+ // Hash the token for storage
162
+ const tokenHash = yield* _(tokenService.hashToken(token));
163
+
164
+ // Verify token to get the actual payload with jti
165
+ const verifiedPayload = yield* _(tokenService.verifyToken(token));
166
+
167
+ // Create session
168
+ const session = yield* _(
169
+ sessionRepo.create({
170
+ userId: user.id,
171
+ tokenHash,
172
+ ipAddress,
173
+ userAgent,
174
+ expiresAt: verifiedPayload.exp * 1000, // Convert to milliseconds
175
+ })
176
+ );
177
+
178
+ // Update last login timestamp
179
+ yield* _(
180
+ userRepo.update(user.id, {
181
+ lastLoginAt: Date.now(),
182
+ })
183
+ );
184
+
185
+ // Log successful login
186
+ yield* _(
187
+ auditLog.log({
188
+ userId: user.id,
189
+ action: "USER_LOGIN",
190
+ resourceType: "session",
191
+ resourceId: session.id,
192
+ ipAddress,
193
+ userAgent,
194
+ })
195
+ );
196
+
197
+ return { user, token, session };
198
+ });
199
+
200
+ // Example: Verify and validate a token
201
+ export const verifyToken = (token: string) =>
202
+ Effect.gen(function* (_) {
203
+ const tokenService = yield* _(TokenService);
204
+ const sessionRepo = yield* _(SessionRepository);
205
+ const userRepo = yield* _(UserRepository);
206
+
207
+ // Verify token signature and expiration
208
+ const payload = yield* _(tokenService.verifyToken(token));
209
+
210
+ // Hash the token to look up session
211
+ const tokenHash = yield* _(tokenService.hashToken(token));
212
+
213
+ // Find session by token hash
214
+ const session = yield* _(sessionRepo.findByTokenHash(tokenHash));
215
+
216
+ if (!session) {
217
+ return yield* _(Effect.fail(new Error("Session not found")));
218
+ }
219
+
220
+ // Check if session is revoked
221
+ if (session.isRevoked) {
222
+ return yield* _(Effect.fail(new Error("Session has been revoked")));
223
+ }
224
+
225
+ // Check if session is expired
226
+ if (session.expiresAt < Date.now()) {
227
+ return yield* _(Effect.fail(new Error("Session has expired")));
228
+ }
229
+
230
+ // Get user
231
+ const user = yield* _(userRepo.findById(payload.sub));
232
+
233
+ if (!user) {
234
+ return yield* _(Effect.fail(new Error("User not found")));
235
+ }
236
+
237
+ if (!user.isActive) {
238
+ return yield* _(Effect.fail(new Error("User account is inactive")));
239
+ }
240
+
241
+ return { user, session, payload };
242
+ });
243
+
244
+ // Example: Logout user
245
+ export const logoutUser = (token: string) =>
246
+ Effect.gen(function* (_) {
247
+ const tokenService = yield* _(TokenService);
248
+ const sessionRepo = yield* _(SessionRepository);
249
+ const auditLog = yield* _(AuditLogService);
250
+
251
+ // Verify token
252
+ const payload = yield* _(tokenService.verifyToken(token));
253
+
254
+ // Hash token to find session
255
+ const tokenHash = yield* _(tokenService.hashToken(token));
256
+
257
+ // Find session
258
+ const session = yield* _(sessionRepo.findByTokenHash(tokenHash));
259
+
260
+ if (!session) {
261
+ return yield* _(Effect.fail(new Error("Session not found")));
262
+ }
263
+
264
+ // Revoke session
265
+ yield* _(sessionRepo.revoke(session.id));
266
+
267
+ // Log logout
268
+ yield* _(
269
+ auditLog.log({
270
+ userId: payload.sub,
271
+ action: "USER_LOGOUT",
272
+ resourceType: "session",
273
+ resourceId: session.id,
274
+ })
275
+ );
276
+
277
+ return { success: true };
278
+ });
279
+
280
+ // Example: Create the complete authentication layer
281
+ export const AuthenticationLayer = Layer.mergeAll(
282
+ PasswordService.Live,
283
+ TokenService.Live,
284
+ UserRepository.Live,
285
+ SessionRepository.Live,
286
+ AuditLogService.Live
287
+ );
288
+
289
+ // Example: Run a complete authentication flow with all dependencies
290
+ export const runAuthExample = () => {
291
+ const program = Effect.gen(function* (_) {
292
+ // Register a new user
293
+ const user = yield* _(
294
+ registerUser(
295
+ "johndoe",
296
+ "john@example.com",
297
+ "SecurePass123",
298
+ "john-doe",
299
+ "127.0.0.1",
300
+ "Mozilla/5.0"
301
+ )
302
+ );
303
+
304
+ console.log("User registered:", user);
305
+
306
+ // Login the user
307
+ const loginResult = yield* _(
308
+ loginUser(
309
+ "john@example.com",
310
+ "SecurePass123",
311
+ "127.0.0.1",
312
+ "Mozilla/5.0"
313
+ )
314
+ );
315
+
316
+ console.log("Login successful:", loginResult);
317
+
318
+ // Verify the token
319
+ const verifyResult = yield* _(verifyToken(loginResult.token));
320
+
321
+ console.log("Token verified:", verifyResult);
322
+
323
+ // Logout the user
324
+ const logoutResult = yield* _(logoutUser(loginResult.token));
325
+
326
+ console.log("Logout successful:", logoutResult);
327
+ });
328
+
329
+ // Create database configuration
330
+ const dbConfig = {
331
+ path: "./auth-example.db",
332
+ };
333
+
334
+ // Combine all layers - properly merging DatabaseService with AuthenticationLayer
335
+ const AppLayer = DatabaseService.Live(dbConfig).pipe(
336
+ Layer.provideMerge(AuthenticationLayer)
337
+ );
338
+
339
+ // Run the program with all dependencies provided
340
+ // Effect-TS Layer type inference limitation
341
+ // @ts-ignore
342
+ return Effect.runPromise(Effect.provide(program, AppLayer));
343
+ };
344
+
345
+ // Uncomment to run the example:
346
+ // runAuthExample().catch(console.error);
@@ -0,0 +1,6 @@
1
+ // Export all authentication services
2
+ export * from "./PasswordService.js";
3
+ export * from "./TokenService.js";
4
+ export * from "./UserRepository.js";
5
+ export * from "./SessionRepository.js";
6
+ export * from "./AuditLogService.js";
@@ -0,0 +1,161 @@
1
+ import Database from "better-sqlite3";
2
+ import { Context, Effect, Layer } from "effect";
3
+ import { readFileSync } from "node:fs";
4
+ import { fileURLToPath } from "node:url";
5
+ import { dirname, join } from "node:path";
6
+
7
+ const __filename = fileURLToPath(import.meta.url);
8
+ const __dirname = dirname(__filename);
9
+
10
+ export class DatabaseError {
11
+ readonly _tag = "DatabaseError";
12
+ constructor(readonly message: string, readonly cause?: unknown) {}
13
+ }
14
+
15
+ export interface DatabaseConfig {
16
+ readonly path: string;
17
+ readonly readOnly?: boolean;
18
+ readonly fileMustExist?: boolean;
19
+ readonly timeout?: number;
20
+ readonly verbose?: boolean;
21
+ }
22
+
23
+ export class DatabaseService extends Context.Tag("DatabaseService")<
24
+ DatabaseService,
25
+ {
26
+ readonly db: Database.Database;
27
+ readonly run: <T = Database.RunResult>(
28
+ sql: string,
29
+ params?: unknown[]
30
+ ) => Effect.Effect<T, DatabaseError>;
31
+ readonly get: <T = unknown>(
32
+ sql: string,
33
+ params?: unknown[]
34
+ ) => Effect.Effect<T | undefined, DatabaseError>;
35
+ readonly all: <T = unknown>(
36
+ sql: string,
37
+ params?: unknown[]
38
+ ) => Effect.Effect<T[], DatabaseError>;
39
+ readonly exec: (sql: string) => Effect.Effect<void, DatabaseError>;
40
+ readonly close: () => Effect.Effect<void, DatabaseError>;
41
+ }
42
+ >() {
43
+ static Live = (config: DatabaseConfig) =>
44
+ Layer.effect(
45
+ this,
46
+ Effect.gen(function* (_) {
47
+ const db = yield* _(
48
+ Effect.try({
49
+ try: () => {
50
+ const options: {
51
+ readonly?: boolean;
52
+ fileMustExist?: boolean;
53
+ timeout?: number;
54
+ verbose?: (message?: unknown) => void;
55
+ } = {};
56
+
57
+ if (config.readOnly !== undefined) {
58
+ options.readonly = config.readOnly;
59
+ }
60
+ if (config.fileMustExist !== undefined) {
61
+ options.fileMustExist = config.fileMustExist;
62
+ }
63
+ if (config.timeout !== undefined) {
64
+ options.timeout = config.timeout;
65
+ }
66
+ if (config.verbose) {
67
+ options.verbose = (message?: unknown) => console.log(message);
68
+ }
69
+
70
+ return new Database(config.path, options);
71
+ },
72
+ catch: (error) =>
73
+ new DatabaseError("Failed to open database", error),
74
+ })
75
+ );
76
+
77
+ yield* _(
78
+ Effect.try({
79
+ try: () => {
80
+ db.pragma("journal_mode = WAL");
81
+ db.pragma("foreign_keys = ON");
82
+ db.pragma("synchronous = NORMAL");
83
+ },
84
+ catch: (error) =>
85
+ new DatabaseError("Failed to set pragmas", error),
86
+ })
87
+ );
88
+
89
+ const schemaPath = join(__dirname, "schema.sql");
90
+ const schema = yield* _(
91
+ Effect.try({
92
+ try: () => readFileSync(schemaPath, "utf-8"),
93
+ catch: (error) =>
94
+ new DatabaseError("Failed to read schema file", error),
95
+ })
96
+ );
97
+
98
+ yield* _(
99
+ Effect.try({
100
+ try: () => db.exec(schema),
101
+ catch: (error) =>
102
+ new DatabaseError("Failed to initialize schema", error),
103
+ })
104
+ );
105
+
106
+ const run = <T = Database.RunResult>(
107
+ sql: string,
108
+ params?: unknown[]
109
+ ): Effect.Effect<T, DatabaseError> =>
110
+ Effect.try({
111
+ try: () => {
112
+ const stmt = db.prepare(sql);
113
+ return stmt.run(...(params ?? [])) as T;
114
+ },
115
+ catch: (error) => new DatabaseError("Query execution failed", error),
116
+ });
117
+
118
+ const get = <T = unknown>(
119
+ sql: string,
120
+ params?: unknown[]
121
+ ): Effect.Effect<T | undefined, DatabaseError> =>
122
+ Effect.try({
123
+ try: () => {
124
+ const stmt = db.prepare(sql);
125
+ return stmt.get(...(params ?? [])) as T | undefined;
126
+ },
127
+ catch: (error) => new DatabaseError("Query execution failed", error),
128
+ });
129
+
130
+ const all = <T = unknown>(
131
+ sql: string,
132
+ params?: unknown[]
133
+ ): Effect.Effect<T[], DatabaseError> =>
134
+ Effect.try({
135
+ try: () => {
136
+ const stmt = db.prepare(sql);
137
+ return stmt.all(...(params ?? [])) as T[];
138
+ },
139
+ catch: (error) => new DatabaseError("Query execution failed", error),
140
+ });
141
+
142
+ const exec = (sql: string): Effect.Effect<void, DatabaseError> =>
143
+ Effect.try({
144
+ try: () => {
145
+ db.exec(sql);
146
+ },
147
+ catch: (error) => new DatabaseError("Exec failed", error),
148
+ });
149
+
150
+ const close = (): Effect.Effect<void, DatabaseError> =>
151
+ Effect.try({
152
+ try: () => {
153
+ db.close();
154
+ },
155
+ catch: (error) => new DatabaseError("Close failed", error),
156
+ });
157
+
158
+ return { db, run, get, all, exec, close };
159
+ })
160
+ );
161
+ }
@@ -0,0 +1,95 @@
1
+ -- Users table
2
+ CREATE TABLE IF NOT EXISTS users (
3
+ id TEXT PRIMARY KEY,
4
+ username TEXT UNIQUE NOT NULL,
5
+ email TEXT UNIQUE NOT NULL,
6
+ password_hash TEXT,
7
+ linux_username TEXT UNIQUE NOT NULL,
8
+ created_at INTEGER DEFAULT (strftime('%s','now')),
9
+ updated_at INTEGER DEFAULT (strftime('%s','now')),
10
+ is_active INTEGER DEFAULT 1,
11
+ email_verified INTEGER DEFAULT 0,
12
+ failed_login_attempts INTEGER DEFAULT 0,
13
+ last_login_at INTEGER,
14
+ CONSTRAINT username_format CHECK (length(username) >= 3 AND length(username) <= 32),
15
+ CONSTRAINT email_format CHECK (email LIKE '%_@__%.__%')
16
+ );
17
+
18
+ -- Index for faster user lookups
19
+ CREATE INDEX IF NOT EXISTS idx_users_email ON users(email);
20
+ CREATE INDEX IF NOT EXISTS idx_users_username ON users(username);
21
+ CREATE INDEX IF NOT EXISTS idx_users_linux_username ON users(linux_username);
22
+
23
+ -- OAuth2 providers table
24
+ CREATE TABLE IF NOT EXISTS oauth2_providers (
25
+ id TEXT PRIMARY KEY,
26
+ user_id TEXT NOT NULL,
27
+ provider TEXT NOT NULL,
28
+ provider_user_id TEXT NOT NULL,
29
+ provider_email TEXT,
30
+ access_token TEXT,
31
+ refresh_token TEXT,
32
+ token_expires_at INTEGER,
33
+ created_at INTEGER DEFAULT (strftime('%s','now')),
34
+ updated_at INTEGER DEFAULT (strftime('%s','now')),
35
+ UNIQUE(provider, provider_user_id),
36
+ FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
37
+ );
38
+
39
+ -- Index for OAuth lookups
40
+ CREATE INDEX IF NOT EXISTS idx_oauth2_user_id ON oauth2_providers(user_id);
41
+ CREATE INDEX IF NOT EXISTS idx_oauth2_provider_lookup ON oauth2_providers(provider, provider_user_id);
42
+
43
+ -- Sessions table
44
+ CREATE TABLE IF NOT EXISTS sessions (
45
+ id TEXT PRIMARY KEY,
46
+ user_id TEXT NOT NULL,
47
+ token_hash TEXT NOT NULL UNIQUE,
48
+ ip_address TEXT,
49
+ user_agent TEXT,
50
+ created_at INTEGER DEFAULT (strftime('%s','now')),
51
+ expires_at INTEGER NOT NULL,
52
+ is_revoked INTEGER DEFAULT 0,
53
+ revoked_at INTEGER,
54
+ FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
55
+ );
56
+
57
+ -- Index for session lookups
58
+ CREATE INDEX IF NOT EXISTS idx_sessions_token_hash ON sessions(token_hash);
59
+ CREATE INDEX IF NOT EXISTS idx_sessions_user_id ON sessions(user_id);
60
+ CREATE INDEX IF NOT EXISTS idx_sessions_expires_at ON sessions(expires_at);
61
+
62
+ -- Audit log table
63
+ CREATE TABLE IF NOT EXISTS audit_logs (
64
+ id TEXT PRIMARY KEY,
65
+ user_id TEXT,
66
+ action TEXT NOT NULL,
67
+ resource_type TEXT,
68
+ resource_id TEXT,
69
+ ip_address TEXT,
70
+ user_agent TEXT,
71
+ details TEXT,
72
+ created_at INTEGER DEFAULT (strftime('%s','now')),
73
+ FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE SET NULL
74
+ );
75
+
76
+ -- Index for audit log queries
77
+ CREATE INDEX IF NOT EXISTS idx_audit_logs_user_id ON audit_logs(user_id);
78
+ CREATE INDEX IF NOT EXISTS idx_audit_logs_action ON audit_logs(action);
79
+ CREATE INDEX IF NOT EXISTS idx_audit_logs_created_at ON audit_logs(created_at);
80
+
81
+ -- Trigger to update updated_at timestamp for users
82
+ CREATE TRIGGER IF NOT EXISTS update_users_timestamp
83
+ AFTER UPDATE ON users
84
+ FOR EACH ROW
85
+ BEGIN
86
+ UPDATE users SET updated_at = strftime('%s','now') WHERE id = NEW.id;
87
+ END;
88
+
89
+ -- Trigger to update updated_at timestamp for oauth2_providers
90
+ CREATE TRIGGER IF NOT EXISTS update_oauth2_providers_timestamp
91
+ AFTER UPDATE ON oauth2_providers
92
+ FOR EACH ROW
93
+ BEGIN
94
+ UPDATE oauth2_providers SET updated_at = strftime('%s','now') WHERE id = NEW.id;
95
+ END;
@@ -0,0 +1,41 @@
1
+ import { Hono } from "hono";
2
+ import { cors } from "hono/cors";
3
+ import { logger } from "hono/logger";
4
+ import type { User, Session } from "@shared/types.js";
5
+
6
+ export type HonoContext = {
7
+ Variables: {
8
+ user?: User;
9
+ session?: Session;
10
+ };
11
+ };
12
+
13
+ export const createApp = () => {
14
+ const app = new Hono<HonoContext>();
15
+
16
+ app.use("*", logger());
17
+
18
+ app.use(
19
+ "*",
20
+ cors({
21
+ origin: (origin) => {
22
+ if (process.env["NODE_ENV"] === "development") {
23
+ return origin;
24
+ }
25
+ const allowedOrigins = process.env["ALLOWED_ORIGINS"]?.split(",") ?? [];
26
+ return allowedOrigins.includes(origin) ? origin : allowedOrigins[0];
27
+ },
28
+ credentials: true,
29
+ })
30
+ );
31
+
32
+ app.get("/health", (c) => {
33
+ return c.json({
34
+ status: "healthy",
35
+ timestamp: new Date().toISOString(),
36
+ uptime: process.uptime(),
37
+ });
38
+ });
39
+
40
+ return app;
41
+ };