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.
- package/.claude/agents/wordpress-admin.md +271 -0
- package/.env.example +0 -1
- package/COOLIFY_DEPLOYMENT.md +1 -1
- package/DEV_WORKFLOW_GUIDE.md +1 -1
- package/PACKAGE_FIXES_SUMMARY.md +319 -0
- package/PAYLOADCMS_AUTH_UPDATE.md +248 -0
- package/PUBLISHING_GUIDE.md +1 -1
- package/README.md +7 -7
- package/USER_GUIDE.md +261 -1
- package/WORDPRESS_ADMIN_SCRIPTS.md +1 -1
- package/bin/cli.js +36 -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 +26 -6
- 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/payloadcms-publish.md +34 -14
- 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
- package/src/templates/docs/wordpress-troubleshoot.js +2 -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,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
|
+
};
|