myaidev-method 0.2.8 → 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/PACKAGE_FIXES_SUMMARY.md +319 -0
- package/PAYLOADCMS_AUTH_UPDATE.md +248 -0
- package/USER_GUIDE.md +260 -0
- 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 +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/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
|
@@ -0,0 +1,408 @@
|
|
|
1
|
+
import { Context, Effect, Layer } from "effect";
|
|
2
|
+
import {
|
|
3
|
+
AuthError,
|
|
4
|
+
DatabaseError,
|
|
5
|
+
Session,
|
|
6
|
+
User,
|
|
7
|
+
ValidationError,
|
|
8
|
+
} from "../../../shared/types.js";
|
|
9
|
+
import { PasswordService } from "./PasswordService.js";
|
|
10
|
+
import { TokenService } from "./TokenService.js";
|
|
11
|
+
import { UserRepository } from "./UserRepository.js";
|
|
12
|
+
import { SessionRepository } from "./SessionRepository.js";
|
|
13
|
+
import { AuditLogService } from "./AuditLogService.js";
|
|
14
|
+
import { UserManagementService } from "../../user-management/UserManagementService.js";
|
|
15
|
+
|
|
16
|
+
const MAX_FAILED_ATTEMPTS = 5;
|
|
17
|
+
const LOCKOUT_DURATION_MS = 15 * 60 * 1000; // 15 minutes
|
|
18
|
+
|
|
19
|
+
export interface AuthServiceDeps {
|
|
20
|
+
readonly register: (
|
|
21
|
+
username: string,
|
|
22
|
+
email: string,
|
|
23
|
+
password: string,
|
|
24
|
+
ipAddress?: string | null,
|
|
25
|
+
userAgent?: string | null
|
|
26
|
+
) => Effect.Effect<User, AuthError | ValidationError | DatabaseError>;
|
|
27
|
+
readonly login: (
|
|
28
|
+
email: string,
|
|
29
|
+
password: string,
|
|
30
|
+
ipAddress?: string | null,
|
|
31
|
+
userAgent?: string | null
|
|
32
|
+
) => Effect.Effect<
|
|
33
|
+
{ user: User; token: string; session: Session },
|
|
34
|
+
AuthError | DatabaseError
|
|
35
|
+
>;
|
|
36
|
+
readonly logout: (
|
|
37
|
+
sessionId: string,
|
|
38
|
+
userId: string
|
|
39
|
+
) => Effect.Effect<void, DatabaseError>;
|
|
40
|
+
readonly verifyToken: (
|
|
41
|
+
token: string
|
|
42
|
+
) => Effect.Effect<{ user: User; session: Session }, AuthError | DatabaseError>;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export class AuthService extends Context.Tag("AuthService")<
|
|
46
|
+
AuthService,
|
|
47
|
+
AuthServiceDeps
|
|
48
|
+
>() {
|
|
49
|
+
static Live = Layer.effect(
|
|
50
|
+
this,
|
|
51
|
+
Effect.gen(function* () {
|
|
52
|
+
const passwordService = yield* PasswordService;
|
|
53
|
+
const tokenService = yield* TokenService;
|
|
54
|
+
const userRepo = yield* UserRepository;
|
|
55
|
+
const sessionRepo = yield* SessionRepository;
|
|
56
|
+
const auditLog = yield* AuditLogService;
|
|
57
|
+
const userManagement = yield* UserManagementService;
|
|
58
|
+
|
|
59
|
+
const sanitizeLinuxUsername = (username: string): string => {
|
|
60
|
+
// Convert to lowercase, replace non-alphanumeric with underscore
|
|
61
|
+
let sanitized = username
|
|
62
|
+
.toLowerCase()
|
|
63
|
+
.replace(/[^a-z0-9_]/g, "_")
|
|
64
|
+
.replace(/^[0-9_]+/, "") // Cannot start with number or underscore
|
|
65
|
+
.slice(0, 32); // Max length for Linux usernames
|
|
66
|
+
|
|
67
|
+
// Ensure it starts with a letter
|
|
68
|
+
if (!/^[a-z]/.test(sanitized)) {
|
|
69
|
+
sanitized = "user_" + sanitized;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return sanitized;
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
const generateUniqueLinuxUsername = (
|
|
76
|
+
baseUsername: string
|
|
77
|
+
): Effect.Effect<string, DatabaseError> =>
|
|
78
|
+
Effect.gen(function* () {
|
|
79
|
+
let linuxUsername = sanitizeLinuxUsername(baseUsername);
|
|
80
|
+
let counter = 0;
|
|
81
|
+
|
|
82
|
+
// Keep trying until we find a unique username
|
|
83
|
+
while (true) {
|
|
84
|
+
const existingUser = yield* userRepo.findByUsername(linuxUsername);
|
|
85
|
+
if (!existingUser) {
|
|
86
|
+
return linuxUsername;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
counter++;
|
|
90
|
+
const suffix = `_${counter}`;
|
|
91
|
+
const maxBase = 32 - suffix.length;
|
|
92
|
+
linuxUsername =
|
|
93
|
+
sanitizeLinuxUsername(baseUsername).slice(0, maxBase) + suffix;
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
const validateEmail = (email: string): Effect.Effect<void, ValidationError> =>
|
|
98
|
+
Effect.gen(function* () {
|
|
99
|
+
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
100
|
+
if (!emailRegex.test(email)) {
|
|
101
|
+
return yield* Effect.fail(
|
|
102
|
+
new ValidationError("email", "Invalid email format")
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
const validateUsername = (
|
|
108
|
+
username: string
|
|
109
|
+
): Effect.Effect<void, ValidationError> =>
|
|
110
|
+
Effect.gen(function* () {
|
|
111
|
+
if (username.length < 3) {
|
|
112
|
+
return yield* Effect.fail(
|
|
113
|
+
new ValidationError(
|
|
114
|
+
"username",
|
|
115
|
+
"Username must be at least 3 characters long"
|
|
116
|
+
)
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (username.length > 32) {
|
|
121
|
+
return yield* Effect.fail(
|
|
122
|
+
new ValidationError(
|
|
123
|
+
"username",
|
|
124
|
+
"Username must be at most 32 characters long"
|
|
125
|
+
)
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (!/^[a-zA-Z0-9_]+$/.test(username)) {
|
|
130
|
+
return yield* Effect.fail(
|
|
131
|
+
new ValidationError(
|
|
132
|
+
"username",
|
|
133
|
+
"Username can only contain letters, numbers, and underscores"
|
|
134
|
+
)
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
const isAccountLocked = (user: User): boolean => {
|
|
140
|
+
if (user.failedLoginAttempts < MAX_FAILED_ATTEMPTS) {
|
|
141
|
+
return false;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Check if lockout period has expired
|
|
145
|
+
const now = Date.now();
|
|
146
|
+
const lockoutExpiry = user.updatedAt + LOCKOUT_DURATION_MS;
|
|
147
|
+
return now < lockoutExpiry;
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
const register: AuthServiceDeps["register"] = (
|
|
151
|
+
username,
|
|
152
|
+
email,
|
|
153
|
+
password,
|
|
154
|
+
ipAddress = null,
|
|
155
|
+
userAgent = null
|
|
156
|
+
) =>
|
|
157
|
+
Effect.gen(function* () {
|
|
158
|
+
// Validate inputs
|
|
159
|
+
yield* validateUsername(username);
|
|
160
|
+
yield* validateEmail(email);
|
|
161
|
+
yield* passwordService.validatePasswordStrength(password);
|
|
162
|
+
|
|
163
|
+
// Check if user already exists
|
|
164
|
+
const existingEmail = yield* userRepo.findByEmail(email);
|
|
165
|
+
if (existingEmail) {
|
|
166
|
+
return yield* Effect.fail(
|
|
167
|
+
new ValidationError("email", "Email already registered")
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const existingUsername = yield* userRepo.findByUsername(username);
|
|
172
|
+
if (existingUsername) {
|
|
173
|
+
return yield* Effect.fail(
|
|
174
|
+
new ValidationError("username", "Username already taken")
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Hash password
|
|
179
|
+
const passwordHash = yield* passwordService.hash(password);
|
|
180
|
+
|
|
181
|
+
// Generate unique Linux username
|
|
182
|
+
const linuxUsername = yield* generateUniqueLinuxUsername(username);
|
|
183
|
+
|
|
184
|
+
// Create user in database
|
|
185
|
+
const user = yield* userRepo.create({
|
|
186
|
+
username,
|
|
187
|
+
email,
|
|
188
|
+
passwordHash,
|
|
189
|
+
linuxUsername,
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
// Create Linux user with home directory and Claude config
|
|
193
|
+
// This runs in the background and doesn't block registration
|
|
194
|
+
// If it fails, user can still authenticate but won't have Linux access
|
|
195
|
+
yield* userManagement
|
|
196
|
+
.createUser({
|
|
197
|
+
username: linuxUsername,
|
|
198
|
+
email,
|
|
199
|
+
shell: "/bin/rbash", // Restricted bash for security
|
|
200
|
+
diskQuotaMB: 2048, // 2GB default quota
|
|
201
|
+
})
|
|
202
|
+
.pipe(
|
|
203
|
+
Effect.catchAll((error) => {
|
|
204
|
+
// Log the error but don't fail registration
|
|
205
|
+
console.error(
|
|
206
|
+
`Failed to create Linux user for ${linuxUsername}:`,
|
|
207
|
+
error
|
|
208
|
+
);
|
|
209
|
+
return Effect.succeed(void 0);
|
|
210
|
+
})
|
|
211
|
+
);
|
|
212
|
+
|
|
213
|
+
// Log audit event
|
|
214
|
+
yield* auditLog.log({
|
|
215
|
+
userId: user.id,
|
|
216
|
+
action: "USER_REGISTERED",
|
|
217
|
+
resourceType: "user",
|
|
218
|
+
resourceId: user.id,
|
|
219
|
+
ipAddress: ipAddress ?? null,
|
|
220
|
+
userAgent: userAgent ?? null,
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
return user;
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
const login: AuthServiceDeps["login"] = (
|
|
227
|
+
email,
|
|
228
|
+
password,
|
|
229
|
+
ipAddress = null,
|
|
230
|
+
userAgent = null
|
|
231
|
+
) =>
|
|
232
|
+
Effect.gen(function* () {
|
|
233
|
+
// Find user by email
|
|
234
|
+
const user = yield* userRepo.findByEmail(email);
|
|
235
|
+
if (!user) {
|
|
236
|
+
return yield* Effect.fail(
|
|
237
|
+
new AuthError("INVALID_CREDENTIALS", "Invalid email or password")
|
|
238
|
+
);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// Check if account is locked
|
|
242
|
+
if (isAccountLocked(user)) {
|
|
243
|
+
yield* auditLog.log({
|
|
244
|
+
userId: user.id,
|
|
245
|
+
action: "LOGIN_FAILED",
|
|
246
|
+
resourceType: "user",
|
|
247
|
+
resourceId: user.id,
|
|
248
|
+
ipAddress: ipAddress ?? null,
|
|
249
|
+
userAgent: userAgent ?? null,
|
|
250
|
+
details: "Account locked due to too many failed attempts",
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
return yield* Effect.fail(
|
|
254
|
+
new AuthError(
|
|
255
|
+
"ACCOUNT_LOCKED",
|
|
256
|
+
"Account is locked due to too many failed login attempts. Please try again in 15 minutes."
|
|
257
|
+
)
|
|
258
|
+
);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// Verify password
|
|
262
|
+
if (!user.passwordHash) {
|
|
263
|
+
return yield* Effect.fail(
|
|
264
|
+
new AuthError(
|
|
265
|
+
"INVALID_CREDENTIALS",
|
|
266
|
+
"Invalid email or password"
|
|
267
|
+
)
|
|
268
|
+
);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
const isPasswordValid = yield* passwordService.verify(
|
|
272
|
+
password,
|
|
273
|
+
user.passwordHash
|
|
274
|
+
);
|
|
275
|
+
|
|
276
|
+
if (!isPasswordValid) {
|
|
277
|
+
// Increment failed login attempts
|
|
278
|
+
yield* userRepo.incrementFailedLogins(user.id);
|
|
279
|
+
|
|
280
|
+
yield* auditLog.log({
|
|
281
|
+
userId: user.id,
|
|
282
|
+
action: "LOGIN_FAILED",
|
|
283
|
+
resourceType: "user",
|
|
284
|
+
resourceId: user.id,
|
|
285
|
+
ipAddress: ipAddress ?? null,
|
|
286
|
+
userAgent: userAgent ?? null,
|
|
287
|
+
details: "Invalid password",
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
return yield* Effect.fail(
|
|
291
|
+
new AuthError("INVALID_CREDENTIALS", "Invalid email or password")
|
|
292
|
+
);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// Reset failed login attempts on successful login
|
|
296
|
+
yield* userRepo.resetFailedLogins(user.id);
|
|
297
|
+
|
|
298
|
+
// Update last login time
|
|
299
|
+
const updatedUser = yield* userRepo.update(user.id, {
|
|
300
|
+
lastLoginAt: Date.now(),
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
// Generate a unique placeholder token to avoid constraint violations
|
|
304
|
+
const expiresAt = Date.now() + 7 * 24 * 60 * 60 * 1000; // 7 days
|
|
305
|
+
const placeholderToken = `placeholder_${user.id}_${Date.now()}_${Math.random()}`;
|
|
306
|
+
const placeholderHash = yield* tokenService.hashToken(placeholderToken);
|
|
307
|
+
|
|
308
|
+
// Create session with placeholder hash
|
|
309
|
+
const session = yield* sessionRepo.create({
|
|
310
|
+
userId: user.id,
|
|
311
|
+
tokenHash: placeholderHash,
|
|
312
|
+
ipAddress: ipAddress ?? null,
|
|
313
|
+
userAgent: userAgent ?? null,
|
|
314
|
+
expiresAt,
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
// Generate JWT token with the session ID
|
|
318
|
+
const token = yield* tokenService.generateToken({
|
|
319
|
+
sub: user.id,
|
|
320
|
+
username: user.username,
|
|
321
|
+
email: user.email,
|
|
322
|
+
jti: session.id,
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
// Update the session with the actual token hash
|
|
326
|
+
const actualTokenHash = yield* tokenService.hashToken(token);
|
|
327
|
+
const finalSession = yield* sessionRepo.updateTokenHash(session.id, actualTokenHash);
|
|
328
|
+
|
|
329
|
+
// Log audit event
|
|
330
|
+
yield* auditLog.log({
|
|
331
|
+
userId: user.id,
|
|
332
|
+
action: "USER_LOGIN",
|
|
333
|
+
resourceType: "session",
|
|
334
|
+
resourceId: finalSession.id,
|
|
335
|
+
ipAddress: ipAddress ?? null,
|
|
336
|
+
userAgent: userAgent ?? null,
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
return { user: updatedUser, token, session: finalSession };
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
const logout: AuthServiceDeps["logout"] = (sessionId, userId) =>
|
|
343
|
+
Effect.gen(function* () {
|
|
344
|
+
// Revoke session
|
|
345
|
+
yield* sessionRepo.revoke(sessionId);
|
|
346
|
+
|
|
347
|
+
// Log audit event
|
|
348
|
+
yield* auditLog.log({
|
|
349
|
+
userId,
|
|
350
|
+
action: "USER_LOGOUT",
|
|
351
|
+
resourceType: "session",
|
|
352
|
+
resourceId: sessionId,
|
|
353
|
+
ipAddress: null,
|
|
354
|
+
userAgent: null,
|
|
355
|
+
});
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
const verifyToken: AuthServiceDeps["verifyToken"] = (token) =>
|
|
359
|
+
Effect.gen(function* () {
|
|
360
|
+
// Verify JWT
|
|
361
|
+
const payload = yield* tokenService.verifyToken(token);
|
|
362
|
+
|
|
363
|
+
// Hash token to find session
|
|
364
|
+
const tokenHash = yield* tokenService.hashToken(token);
|
|
365
|
+
const session = yield* sessionRepo.findByTokenHash(tokenHash);
|
|
366
|
+
|
|
367
|
+
if (!session) {
|
|
368
|
+
return yield* Effect.fail(
|
|
369
|
+
new AuthError("INVALID_TOKEN", "Session not found")
|
|
370
|
+
);
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// Check if session is expired
|
|
374
|
+
if (session.expiresAt < Date.now()) {
|
|
375
|
+
return yield* Effect.fail(
|
|
376
|
+
new AuthError("TOKEN_EXPIRED", "Session has expired")
|
|
377
|
+
);
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
// Check if session is revoked
|
|
381
|
+
if (session.isRevoked) {
|
|
382
|
+
return yield* Effect.fail(
|
|
383
|
+
new AuthError("SESSION_REVOKED", "Session has been revoked")
|
|
384
|
+
);
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
// Find user
|
|
388
|
+
const user = yield* userRepo.findById(payload.sub);
|
|
389
|
+
if (!user) {
|
|
390
|
+
return yield* Effect.fail(
|
|
391
|
+
new AuthError("USER_NOT_FOUND", "User not found")
|
|
392
|
+
);
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
// Check if user is active
|
|
396
|
+
if (!user.isActive) {
|
|
397
|
+
return yield* Effect.fail(
|
|
398
|
+
new AuthError("USER_INACTIVE", "User account is inactive")
|
|
399
|
+
);
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
return { user, session };
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
return { register, login, logout, verifyToken };
|
|
406
|
+
})
|
|
407
|
+
);
|
|
408
|
+
}
|