myaidev-method 0.2.8 → 0.2.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/agents/wordpress-admin.md +271 -0
- package/.env.example +0 -1
- package/PACKAGE_FIXES_SUMMARY.md +319 -0
- package/PAYLOADCMS_AUTH_UPDATE.md +248 -0
- package/USER_GUIDE.md +260 -0
- package/bin/cli.js +70 -0
- package/dist/server/.tsbuildinfo +1 -0
- package/dist/server/auth/controllers/AuthController.d.ts +34 -0
- package/dist/server/auth/controllers/AuthController.d.ts.map +1 -0
- package/dist/server/auth/controllers/AuthController.js +43 -0
- package/dist/server/auth/controllers/AuthController.js.map +1 -0
- package/dist/server/auth/example-usage.d.ts +53 -0
- package/dist/server/auth/example-usage.d.ts.map +1 -0
- package/dist/server/auth/example-usage.js +129 -0
- package/dist/server/auth/example-usage.js.map +1 -0
- package/dist/server/auth/index.d.ts +11 -0
- package/dist/server/auth/index.d.ts.map +1 -0
- package/dist/server/auth/index.js +15 -0
- package/dist/server/auth/index.js.map +1 -0
- package/dist/server/auth/layers.d.ts +19 -0
- package/dist/server/auth/layers.d.ts.map +1 -0
- package/dist/server/auth/layers.js +33 -0
- package/dist/server/auth/layers.js.map +1 -0
- package/dist/server/auth/middleware/authMiddleware.d.ts +24 -0
- package/dist/server/auth/middleware/authMiddleware.d.ts.map +1 -0
- package/dist/server/auth/middleware/authMiddleware.js +65 -0
- package/dist/server/auth/middleware/authMiddleware.js.map +1 -0
- package/dist/server/auth/routes/authRoutes.d.ts +11 -0
- package/dist/server/auth/routes/authRoutes.d.ts.map +1 -0
- package/dist/server/auth/routes/authRoutes.js +213 -0
- package/dist/server/auth/routes/authRoutes.js.map +1 -0
- package/dist/server/auth/services/AuditLogService.d.ts +21 -0
- package/dist/server/auth/services/AuditLogService.d.ts.map +1 -0
- package/dist/server/auth/services/AuditLogService.js +28 -0
- package/dist/server/auth/services/AuditLogService.js.map +1 -0
- package/dist/server/auth/services/AuthService.d.ts +27 -0
- package/dist/server/auth/services/AuthService.d.ts.map +1 -0
- package/dist/server/auth/services/AuthService.js +246 -0
- package/dist/server/auth/services/AuthService.js.map +1 -0
- package/dist/server/auth/services/PasswordService.d.ts +12 -0
- package/dist/server/auth/services/PasswordService.d.ts.map +1 -0
- package/dist/server/auth/services/PasswordService.js +31 -0
- package/dist/server/auth/services/PasswordService.js.map +1 -0
- package/dist/server/auth/services/SessionRepository.d.ts +24 -0
- package/dist/server/auth/services/SessionRepository.d.ts.map +1 -0
- package/dist/server/auth/services/SessionRepository.js +101 -0
- package/dist/server/auth/services/SessionRepository.js.map +1 -0
- package/dist/server/auth/services/TokenService.d.ts +12 -0
- package/dist/server/auth/services/TokenService.d.ts.map +1 -0
- package/dist/server/auth/services/TokenService.js +86 -0
- package/dist/server/auth/services/TokenService.js.map +1 -0
- package/dist/server/auth/services/UserRepository.d.ts +23 -0
- package/dist/server/auth/services/UserRepository.d.ts.map +1 -0
- package/dist/server/auth/services/UserRepository.js +168 -0
- package/dist/server/auth/services/UserRepository.js.map +1 -0
- package/dist/server/auth/services/example.d.ts +26 -0
- package/dist/server/auth/services/example.d.ts.map +1 -0
- package/dist/server/auth/services/example.js +221 -0
- package/dist/server/auth/services/example.js.map +1 -0
- package/dist/server/auth/services/index.d.ts +6 -0
- package/dist/server/auth/services/index.d.ts.map +1 -0
- package/dist/server/auth/services/index.js +7 -0
- package/dist/server/auth/services/index.js.map +1 -0
- package/dist/server/database/db.d.ts +28 -0
- package/dist/server/database/db.d.ts.map +1 -0
- package/dist/server/database/db.js +91 -0
- package/dist/server/database/db.js.map +1 -0
- package/dist/server/database/schema.sql +95 -0
- package/dist/server/hono/app.d.ts +10 -0
- package/dist/server/hono/app.d.ts.map +1 -0
- package/dist/server/hono/app.js +26 -0
- package/dist/server/hono/app.js.map +1 -0
- package/dist/server/hono/routes.d.ts +12 -0
- package/dist/server/hono/routes.d.ts.map +1 -0
- package/dist/server/hono/routes.js +40 -0
- package/dist/server/hono/routes.js.map +1 -0
- package/dist/server/main.d.ts +2 -0
- package/dist/server/main.d.ts.map +1 -0
- package/dist/server/main.js +94 -0
- package/dist/server/main.js.map +1 -0
- package/dist/server/user-management/DirectoryService.d.ts +62 -0
- package/dist/server/user-management/DirectoryService.d.ts.map +1 -0
- package/dist/server/user-management/DirectoryService.js +201 -0
- package/dist/server/user-management/DirectoryService.js.map +1 -0
- package/dist/server/user-management/LinuxUserService.d.ts +71 -0
- package/dist/server/user-management/LinuxUserService.d.ts.map +1 -0
- package/dist/server/user-management/LinuxUserService.js +192 -0
- package/dist/server/user-management/LinuxUserService.js.map +1 -0
- package/dist/server/user-management/QuotaService.d.ts +59 -0
- package/dist/server/user-management/QuotaService.d.ts.map +1 -0
- package/dist/server/user-management/QuotaService.js +148 -0
- package/dist/server/user-management/QuotaService.js.map +1 -0
- package/dist/server/user-management/UserManagementService.d.ts +74 -0
- package/dist/server/user-management/UserManagementService.d.ts.map +1 -0
- package/dist/server/user-management/UserManagementService.js +122 -0
- package/dist/server/user-management/UserManagementService.js.map +1 -0
- package/dist/server/user-management/index.d.ts +26 -0
- package/dist/server/user-management/index.d.ts.map +1 -0
- package/dist/server/user-management/index.js +26 -0
- package/dist/server/user-management/index.js.map +1 -0
- package/dist/server/user-management/layers.d.ts +27 -0
- package/dist/server/user-management/layers.d.ts.map +1 -0
- package/dist/server/user-management/layers.js +37 -0
- package/dist/server/user-management/layers.js.map +1 -0
- package/dist/shared/types.d.ts +94 -0
- package/dist/shared/types.d.ts.map +1 -0
- package/dist/shared/types.js +32 -0
- package/dist/shared/types.js.map +1 -0
- package/package.json +25 -5
- package/src/lib/payloadcms-utils.js +5 -12
- package/src/server/auth/ARCHITECTURE.md +575 -0
- package/src/server/auth/IMPLEMENTATION_SUMMARY.md +287 -0
- package/src/server/auth/QUICK_START.md +283 -0
- package/src/server/auth/README.md +290 -0
- package/src/server/auth/controllers/AuthController.ts +129 -0
- package/src/server/auth/example-usage.ts +159 -0
- package/src/server/auth/index.ts +19 -0
- package/src/server/auth/layers.ts +57 -0
- package/src/server/auth/middleware/authMiddleware.ts +118 -0
- package/src/server/auth/routes/authRoutes.ts +319 -0
- package/src/server/auth/services/AuditLogService.ts +81 -0
- package/src/server/auth/services/AuthService.ts +408 -0
- package/src/server/auth/services/IMPLEMENTATION_SUMMARY.md +404 -0
- package/src/server/auth/services/PasswordService.ts +85 -0
- package/src/server/auth/services/README.md +361 -0
- package/src/server/auth/services/SessionRepository.ts +227 -0
- package/src/server/auth/services/TokenService.ts +174 -0
- package/src/server/auth/services/UserRepository.ts +318 -0
- package/src/server/auth/services/example.ts +346 -0
- package/src/server/auth/services/index.ts +6 -0
- package/src/server/database/db.ts +161 -0
- package/src/server/database/schema.sql +95 -0
- package/src/server/hono/app.ts +41 -0
- package/src/server/main.ts +115 -0
- package/src/server/user-management/DirectoryService.ts +348 -0
- package/src/server/user-management/LinuxUserService.ts +338 -0
- package/src/server/user-management/QuotaService.ts +256 -0
- package/src/server/user-management/README.md +333 -0
- package/src/server/user-management/UserManagementService.ts +335 -0
- package/src/server/user-management/index.ts +26 -0
- package/src/server/user-management/layers.ts +51 -0
- package/src/shared/types.ts +111 -0
- package/src/templates/claude/agents/coolify-deploy.md +50 -50
- package/src/templates/claude/agents/payloadcms-publish.md +46 -18
- package/src/templates/codex/commands/myai-astro-publish.md +8 -2
- package/src/templates/codex/commands/myai-content-writer.md +8 -2
- package/src/templates/codex/commands/myai-coolify-deploy.md +8 -2
- package/src/templates/codex/commands/myai-dev-architect.md +8 -2
- package/src/templates/codex/commands/myai-dev-code.md +8 -2
- package/src/templates/codex/commands/myai-dev-docs.md +8 -2
- package/src/templates/codex/commands/myai-dev-review.md +8 -2
- package/src/templates/codex/commands/myai-dev-test.md +8 -2
- package/src/templates/codex/commands/myai-docusaurus-publish.md +8 -2
- package/src/templates/codex/commands/myai-mintlify-publish.md +8 -2
- package/src/templates/codex/commands/myai-payloadcms-publish.md +17 -3
- package/src/templates/codex/commands/myai-sparc-workflow.md +8 -2
- package/src/templates/codex/commands/myai-wordpress-admin.md +8 -2
- package/src/templates/codex/commands/myai-wordpress-publish.md +8 -2
|
@@ -0,0 +1,338 @@
|
|
|
1
|
+
import { Effect, Context, Layer } from "effect";
|
|
2
|
+
import { execSync } from "child_process";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Linux User Management Service
|
|
6
|
+
*
|
|
7
|
+
* Handles creation, management, and deletion of Linux system users
|
|
8
|
+
* for multi-user isolation in MyAIDev Method web server.
|
|
9
|
+
*
|
|
10
|
+
* Security Features:
|
|
11
|
+
* - No sudo/root access granted to created users
|
|
12
|
+
* - Limited shell access (rbash or nologin)
|
|
13
|
+
* - Home directory isolation
|
|
14
|
+
* - Resource limits via ulimit
|
|
15
|
+
* - User groups for permission management
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
export interface LinuxUser {
|
|
19
|
+
username: string;
|
|
20
|
+
uid: number;
|
|
21
|
+
gid: number;
|
|
22
|
+
homeDir: string;
|
|
23
|
+
shell: string;
|
|
24
|
+
created: boolean;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface CreateLinuxUserOptions {
|
|
28
|
+
username: string;
|
|
29
|
+
shell?: "/bin/rbash" | "/usr/sbin/nologin" | "/bin/bash";
|
|
30
|
+
createHome?: boolean;
|
|
31
|
+
groups?: string[];
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface LinuxUserError {
|
|
35
|
+
readonly _tag: "LinuxUserError";
|
|
36
|
+
readonly message: string;
|
|
37
|
+
readonly cause?: unknown;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const LinuxUserError = (message: string, cause?: unknown): LinuxUserError => ({
|
|
41
|
+
_tag: "LinuxUserError",
|
|
42
|
+
message,
|
|
43
|
+
cause,
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
export class LinuxUserService extends Context.Tag("LinuxUserService")<
|
|
47
|
+
LinuxUserService,
|
|
48
|
+
{
|
|
49
|
+
/**
|
|
50
|
+
* Create a new Linux system user
|
|
51
|
+
*/
|
|
52
|
+
readonly createUser: (
|
|
53
|
+
options: CreateLinuxUserOptions
|
|
54
|
+
) => Effect.Effect<LinuxUser, LinuxUserError>;
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Check if a Linux user exists
|
|
58
|
+
*/
|
|
59
|
+
readonly userExists: (
|
|
60
|
+
username: string
|
|
61
|
+
) => Effect.Effect<boolean, LinuxUserError>;
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Get Linux user information
|
|
65
|
+
*/
|
|
66
|
+
readonly getUserInfo: (
|
|
67
|
+
username: string
|
|
68
|
+
) => Effect.Effect<LinuxUser, LinuxUserError>;
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Delete a Linux system user
|
|
72
|
+
*/
|
|
73
|
+
readonly deleteUser: (
|
|
74
|
+
username: string,
|
|
75
|
+
removeHome?: boolean
|
|
76
|
+
) => Effect.Effect<void, LinuxUserError>;
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Set resource limits for a user
|
|
80
|
+
*/
|
|
81
|
+
readonly setResourceLimits: (
|
|
82
|
+
username: string,
|
|
83
|
+
limits: ResourceLimits
|
|
84
|
+
) => Effect.Effect<void, LinuxUserError>;
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Sanitize username for Linux system use
|
|
88
|
+
* Converts email-based usernames to valid Linux usernames
|
|
89
|
+
*/
|
|
90
|
+
readonly sanitizeUsername: (
|
|
91
|
+
username: string
|
|
92
|
+
) => Effect.Effect<string, LinuxUserError>;
|
|
93
|
+
}
|
|
94
|
+
>() {
|
|
95
|
+
static Live = Layer.succeed(this, {
|
|
96
|
+
createUser: (options: CreateLinuxUserOptions) =>
|
|
97
|
+
Effect.gen(function* () {
|
|
98
|
+
const { username, shell = "/bin/rbash", createHome = true, groups = [] } = options;
|
|
99
|
+
|
|
100
|
+
// Validate username format (Linux username requirements)
|
|
101
|
+
if (!/^[a-z_][a-z0-9_-]{0,31}$/.test(username)) {
|
|
102
|
+
return yield* Effect.fail(
|
|
103
|
+
LinuxUserError(
|
|
104
|
+
`Invalid Linux username format: ${username}. Must start with lowercase letter or underscore, contain only lowercase letters, digits, underscores, and hyphens, and be 1-32 characters long.`
|
|
105
|
+
)
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Check if user already exists
|
|
110
|
+
const exists = yield* Effect.tryPromise({
|
|
111
|
+
try: async () => {
|
|
112
|
+
try {
|
|
113
|
+
execSync(`id -u ${username}`, { stdio: "ignore" });
|
|
114
|
+
return true;
|
|
115
|
+
} catch {
|
|
116
|
+
return false;
|
|
117
|
+
}
|
|
118
|
+
},
|
|
119
|
+
catch: (error) => LinuxUserError("Failed to check if user exists", error),
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
if (exists) {
|
|
123
|
+
return yield* Effect.fail(
|
|
124
|
+
LinuxUserError(`Linux user ${username} already exists`)
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Create user command
|
|
129
|
+
const createHomeFlag = createHome ? "--create-home" : "--no-create-home";
|
|
130
|
+
const groupsFlag = groups.length > 0 ? `--groups ${groups.join(",")}` : "";
|
|
131
|
+
const command = `sudo useradd ${createHomeFlag} --shell ${shell} ${groupsFlag} ${username}`;
|
|
132
|
+
|
|
133
|
+
yield* Effect.tryPromise({
|
|
134
|
+
try: async () => {
|
|
135
|
+
execSync(command, { stdio: "pipe" });
|
|
136
|
+
},
|
|
137
|
+
catch: (error) =>
|
|
138
|
+
LinuxUserError(`Failed to create Linux user ${username}`, error),
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
// Get user info
|
|
142
|
+
const userInfo = yield* Effect.tryPromise({
|
|
143
|
+
try: async () => {
|
|
144
|
+
const uidOutput = execSync(`id -u ${username}`, {
|
|
145
|
+
encoding: "utf-8",
|
|
146
|
+
}).trim();
|
|
147
|
+
const gidOutput = execSync(`id -g ${username}`, {
|
|
148
|
+
encoding: "utf-8",
|
|
149
|
+
}).trim();
|
|
150
|
+
const homeDir = createHome ? `/home/${username}` : "";
|
|
151
|
+
|
|
152
|
+
return {
|
|
153
|
+
username,
|
|
154
|
+
uid: parseInt(uidOutput, 10),
|
|
155
|
+
gid: parseInt(gidOutput, 10),
|
|
156
|
+
homeDir,
|
|
157
|
+
shell,
|
|
158
|
+
created: true,
|
|
159
|
+
};
|
|
160
|
+
},
|
|
161
|
+
catch: (error) =>
|
|
162
|
+
LinuxUserError(`Failed to get user info for ${username}`, error),
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
return userInfo;
|
|
166
|
+
}),
|
|
167
|
+
|
|
168
|
+
userExists: (username: string) =>
|
|
169
|
+
Effect.tryPromise({
|
|
170
|
+
try: async () => {
|
|
171
|
+
try {
|
|
172
|
+
execSync(`id -u ${username}`, { stdio: "ignore" });
|
|
173
|
+
return true;
|
|
174
|
+
} catch {
|
|
175
|
+
return false;
|
|
176
|
+
}
|
|
177
|
+
},
|
|
178
|
+
catch: (error) => LinuxUserError("Failed to check if user exists", error),
|
|
179
|
+
}),
|
|
180
|
+
|
|
181
|
+
getUserInfo: (username: string) =>
|
|
182
|
+
Effect.gen(function* () {
|
|
183
|
+
const exists = yield* Effect.tryPromise({
|
|
184
|
+
try: async () => {
|
|
185
|
+
try {
|
|
186
|
+
execSync(`id -u ${username}`, { stdio: "ignore" });
|
|
187
|
+
return true;
|
|
188
|
+
} catch {
|
|
189
|
+
return false;
|
|
190
|
+
}
|
|
191
|
+
},
|
|
192
|
+
catch: (error) => LinuxUserError("Failed to check if user exists", error),
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
if (!exists) {
|
|
196
|
+
return yield* Effect.fail(
|
|
197
|
+
LinuxUserError(`Linux user ${username} does not exist`)
|
|
198
|
+
);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const userInfo = yield* Effect.tryPromise({
|
|
202
|
+
try: async () => {
|
|
203
|
+
const uidOutput = execSync(`id -u ${username}`, {
|
|
204
|
+
encoding: "utf-8",
|
|
205
|
+
}).trim();
|
|
206
|
+
const gidOutput = execSync(`id -g ${username}`, {
|
|
207
|
+
encoding: "utf-8",
|
|
208
|
+
}).trim();
|
|
209
|
+
|
|
210
|
+
// Get shell from /etc/passwd
|
|
211
|
+
const passwdLine = execSync(`getent passwd ${username}`, {
|
|
212
|
+
encoding: "utf-8",
|
|
213
|
+
}).trim();
|
|
214
|
+
const shell = passwdLine.split(":")[6] || "/bin/bash";
|
|
215
|
+
|
|
216
|
+
// Get home directory
|
|
217
|
+
const homeDir = passwdLine.split(":")[5] || `/home/${username}`;
|
|
218
|
+
|
|
219
|
+
return {
|
|
220
|
+
username,
|
|
221
|
+
uid: parseInt(uidOutput, 10),
|
|
222
|
+
gid: parseInt(gidOutput, 10),
|
|
223
|
+
homeDir,
|
|
224
|
+
shell,
|
|
225
|
+
created: true,
|
|
226
|
+
};
|
|
227
|
+
},
|
|
228
|
+
catch: (error) =>
|
|
229
|
+
LinuxUserError(`Failed to get user info for ${username}`, error),
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
return userInfo;
|
|
233
|
+
}),
|
|
234
|
+
|
|
235
|
+
deleteUser: (username: string, removeHome: boolean = true) =>
|
|
236
|
+
Effect.gen(function* () {
|
|
237
|
+
// Check if user exists
|
|
238
|
+
const exists = yield* Effect.tryPromise({
|
|
239
|
+
try: async () => {
|
|
240
|
+
try {
|
|
241
|
+
execSync(`id -u ${username}`, { stdio: "ignore" });
|
|
242
|
+
return true;
|
|
243
|
+
} catch {
|
|
244
|
+
return false;
|
|
245
|
+
}
|
|
246
|
+
},
|
|
247
|
+
catch: (error) => LinuxUserError("Failed to check if user exists", error),
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
if (!exists) {
|
|
251
|
+
return yield* Effect.fail(
|
|
252
|
+
LinuxUserError(`Linux user ${username} does not exist`)
|
|
253
|
+
);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// Delete user
|
|
257
|
+
const removeHomeFlag = removeHome ? "--remove" : "";
|
|
258
|
+
const command = `sudo userdel ${removeHomeFlag} ${username}`;
|
|
259
|
+
|
|
260
|
+
yield* Effect.tryPromise({
|
|
261
|
+
try: async () => {
|
|
262
|
+
execSync(command, { stdio: "pipe" });
|
|
263
|
+
},
|
|
264
|
+
catch: (error) =>
|
|
265
|
+
LinuxUserError(`Failed to delete Linux user ${username}`, error),
|
|
266
|
+
});
|
|
267
|
+
}),
|
|
268
|
+
|
|
269
|
+
setResourceLimits: (username: string, limits: ResourceLimits) =>
|
|
270
|
+
Effect.gen(function* () {
|
|
271
|
+
// Create limits configuration file
|
|
272
|
+
const limitsContent = `
|
|
273
|
+
# Resource limits for ${username}
|
|
274
|
+
${username} soft nofile ${limits.maxOpenFiles || 1024}
|
|
275
|
+
${username} hard nofile ${limits.maxOpenFiles || 2048}
|
|
276
|
+
${username} soft nproc ${limits.maxProcesses || 256}
|
|
277
|
+
${username} hard nproc ${limits.maxProcesses || 512}
|
|
278
|
+
${username} soft as ${limits.maxMemoryKB || 2097152}
|
|
279
|
+
${username} hard as ${limits.maxMemoryKB || 4194304}
|
|
280
|
+
${username} soft cpu ${limits.maxCPUTime || 60}
|
|
281
|
+
${username} hard cpu ${limits.maxCPUTime || 120}
|
|
282
|
+
`.trim();
|
|
283
|
+
|
|
284
|
+
const limitsFilePath = `/etc/security/limits.d/${username}.conf`;
|
|
285
|
+
|
|
286
|
+
yield* Effect.tryPromise({
|
|
287
|
+
try: async () => {
|
|
288
|
+
// Write limits file (requires sudo)
|
|
289
|
+
execSync(`echo '${limitsContent}' | sudo tee ${limitsFilePath} > /dev/null`, {
|
|
290
|
+
stdio: "pipe",
|
|
291
|
+
});
|
|
292
|
+
},
|
|
293
|
+
catch: (error) =>
|
|
294
|
+
LinuxUserError(
|
|
295
|
+
`Failed to set resource limits for ${username}`,
|
|
296
|
+
error
|
|
297
|
+
),
|
|
298
|
+
});
|
|
299
|
+
}),
|
|
300
|
+
|
|
301
|
+
sanitizeUsername: (username: string) =>
|
|
302
|
+
Effect.gen(function* () {
|
|
303
|
+
// Convert to lowercase
|
|
304
|
+
let sanitized = username.toLowerCase();
|
|
305
|
+
|
|
306
|
+
// Replace invalid characters with underscores
|
|
307
|
+
sanitized = sanitized.replace(/[^a-z0-9_-]/g, "_");
|
|
308
|
+
|
|
309
|
+
// Ensure starts with letter or underscore
|
|
310
|
+
if (!/^[a-z_]/.test(sanitized)) {
|
|
311
|
+
sanitized = `u_${sanitized}`;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// Truncate to 32 characters
|
|
315
|
+
sanitized = sanitized.substring(0, 32);
|
|
316
|
+
|
|
317
|
+
// Remove trailing hyphens or underscores
|
|
318
|
+
sanitized = sanitized.replace(/[-_]+$/, "");
|
|
319
|
+
|
|
320
|
+
if (sanitized.length === 0) {
|
|
321
|
+
return yield* Effect.fail(
|
|
322
|
+
LinuxUserError(
|
|
323
|
+
`Could not sanitize username: ${username} results in empty string`
|
|
324
|
+
)
|
|
325
|
+
);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
return sanitized;
|
|
329
|
+
}),
|
|
330
|
+
});
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
export interface ResourceLimits {
|
|
334
|
+
maxOpenFiles?: number;
|
|
335
|
+
maxProcesses?: number;
|
|
336
|
+
maxMemoryKB?: number;
|
|
337
|
+
maxCPUTime?: number;
|
|
338
|
+
}
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
import { Effect, Context, Layer } from "effect";
|
|
2
|
+
import { execSync } from "child_process";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Quota Management Service
|
|
6
|
+
*
|
|
7
|
+
* Manages disk quotas and resource limits for Linux users.
|
|
8
|
+
* Provides storage limits to prevent individual users from
|
|
9
|
+
* consuming excessive disk space.
|
|
10
|
+
*
|
|
11
|
+
* Features:
|
|
12
|
+
* - Disk quota management (requires quota system on host)
|
|
13
|
+
* - Directory size tracking
|
|
14
|
+
* - Resource limit enforcement
|
|
15
|
+
* - Usage monitoring
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
export interface DiskQuota {
|
|
19
|
+
softLimitMB: number;
|
|
20
|
+
hardLimitMB: number;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface QuotaUsage {
|
|
24
|
+
usedMB: number;
|
|
25
|
+
softLimitMB: number;
|
|
26
|
+
hardLimitMB: number;
|
|
27
|
+
percentUsed: number;
|
|
28
|
+
isOverSoftLimit: boolean;
|
|
29
|
+
isOverHardLimit: boolean;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface QuotaError {
|
|
33
|
+
readonly _tag: "QuotaError";
|
|
34
|
+
readonly message: string;
|
|
35
|
+
readonly cause?: unknown;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const QuotaError = (message: string, cause?: unknown): QuotaError => ({
|
|
39
|
+
_tag: "QuotaError",
|
|
40
|
+
message,
|
|
41
|
+
cause,
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
export class QuotaService extends Context.Tag("QuotaService")<
|
|
45
|
+
QuotaService,
|
|
46
|
+
{
|
|
47
|
+
/**
|
|
48
|
+
* Set disk quota for a user
|
|
49
|
+
* Note: Requires quota support on the filesystem
|
|
50
|
+
*/
|
|
51
|
+
readonly setDiskQuota: (
|
|
52
|
+
username: string,
|
|
53
|
+
quota: DiskQuota
|
|
54
|
+
) => Effect.Effect<void, QuotaError>;
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Get current quota usage for a user
|
|
58
|
+
*/
|
|
59
|
+
readonly getQuotaUsage: (
|
|
60
|
+
username: string
|
|
61
|
+
) => Effect.Effect<QuotaUsage, QuotaError>;
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Check if quota system is available
|
|
65
|
+
*/
|
|
66
|
+
readonly isQuotaAvailable: () => Effect.Effect<boolean, QuotaError>;
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Get directory size for a user (fallback if quota not available)
|
|
70
|
+
*/
|
|
71
|
+
readonly getDirectorySize: (
|
|
72
|
+
dirPath: string
|
|
73
|
+
) => Effect.Effect<number, QuotaError>;
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Enforce storage limits (alternative to disk quotas)
|
|
77
|
+
*/
|
|
78
|
+
readonly checkStorageLimit: (
|
|
79
|
+
username: string,
|
|
80
|
+
homeDir: string,
|
|
81
|
+
limitMB: number
|
|
82
|
+
) => Effect.Effect<boolean, QuotaError>;
|
|
83
|
+
}
|
|
84
|
+
>() {
|
|
85
|
+
static Live = Layer.succeed(this, {
|
|
86
|
+
setDiskQuota: (username: string, quota: DiskQuota) =>
|
|
87
|
+
Effect.gen(function* () {
|
|
88
|
+
// Check if quota is available
|
|
89
|
+
const quotaAvailable = yield* Effect.tryPromise({
|
|
90
|
+
try: async () => {
|
|
91
|
+
try {
|
|
92
|
+
execSync("which setquota", { stdio: "ignore" });
|
|
93
|
+
return true;
|
|
94
|
+
} catch {
|
|
95
|
+
return false;
|
|
96
|
+
}
|
|
97
|
+
},
|
|
98
|
+
catch: (error) =>
|
|
99
|
+
QuotaError("Failed to check quota availability", error),
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
if (!quotaAvailable) {
|
|
103
|
+
return yield* Effect.fail(
|
|
104
|
+
QuotaError(
|
|
105
|
+
"Quota system not available. Install quota tools: sudo apt-get install quota"
|
|
106
|
+
)
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Set quota using setquota command
|
|
111
|
+
// Format: setquota -u username soft_blocks hard_blocks soft_inodes hard_inodes filesystem
|
|
112
|
+
const softBlocks = quota.softLimitMB * 1024; // Convert MB to blocks (1 block = 1KB)
|
|
113
|
+
const hardBlocks = quota.hardLimitMB * 1024;
|
|
114
|
+
const softInodes = 10000; // Reasonable inode limit
|
|
115
|
+
const hardInodes = 15000;
|
|
116
|
+
const filesystem = "/"; // Primary filesystem
|
|
117
|
+
|
|
118
|
+
yield* Effect.tryPromise({
|
|
119
|
+
try: async () => {
|
|
120
|
+
const command = `sudo setquota -u ${username} ${softBlocks} ${hardBlocks} ${softInodes} ${hardInodes} ${filesystem}`;
|
|
121
|
+
execSync(command, { stdio: "pipe" });
|
|
122
|
+
},
|
|
123
|
+
catch: (error) =>
|
|
124
|
+
QuotaError(`Failed to set disk quota for ${username}`, error),
|
|
125
|
+
});
|
|
126
|
+
}),
|
|
127
|
+
|
|
128
|
+
getQuotaUsage: (username: string) =>
|
|
129
|
+
Effect.gen(function* () {
|
|
130
|
+
// Check if quota is available
|
|
131
|
+
const quotaAvailable = yield* Effect.tryPromise({
|
|
132
|
+
try: async () => {
|
|
133
|
+
try {
|
|
134
|
+
execSync("which quota", { stdio: "ignore" });
|
|
135
|
+
return true;
|
|
136
|
+
} catch {
|
|
137
|
+
return false;
|
|
138
|
+
}
|
|
139
|
+
},
|
|
140
|
+
catch: (error) =>
|
|
141
|
+
QuotaError("Failed to check quota availability", error),
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
if (!quotaAvailable) {
|
|
145
|
+
// Fallback: Calculate directory size
|
|
146
|
+
const homeDir = `/home/${username}`;
|
|
147
|
+
const usedMB = yield* Effect.tryPromise({
|
|
148
|
+
try: async () => {
|
|
149
|
+
const output = execSync(`sudo du -sm ${homeDir}`, {
|
|
150
|
+
encoding: "utf-8",
|
|
151
|
+
});
|
|
152
|
+
return parseInt(output.split("\t")[0] || "0", 10);
|
|
153
|
+
},
|
|
154
|
+
catch: (error) =>
|
|
155
|
+
QuotaError(
|
|
156
|
+
`Failed to calculate directory size for ${username}`,
|
|
157
|
+
error
|
|
158
|
+
),
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
return {
|
|
162
|
+
usedMB,
|
|
163
|
+
softLimitMB: 1024, // Default 1GB soft limit
|
|
164
|
+
hardLimitMB: 2048, // Default 2GB hard limit
|
|
165
|
+
percentUsed: (usedMB / 2048) * 100,
|
|
166
|
+
isOverSoftLimit: usedMB > 1024,
|
|
167
|
+
isOverHardLimit: usedMB > 2048,
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Get quota usage using quota command
|
|
172
|
+
const usage = yield* Effect.tryPromise({
|
|
173
|
+
try: async () => {
|
|
174
|
+
const output = execSync(`sudo quota -u ${username} --show-mntpoint`, {
|
|
175
|
+
encoding: "utf-8",
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
// Parse quota output
|
|
179
|
+
// Expected format:
|
|
180
|
+
// Disk quotas for user username (uid 1001):
|
|
181
|
+
// Filesystem blocks quota limit grace files quota limit grace
|
|
182
|
+
// /dev/sda1 1024 1024000 2048000 100 10000 15000
|
|
183
|
+
|
|
184
|
+
const lines = output.trim().split("\n");
|
|
185
|
+
if (lines.length < 3) {
|
|
186
|
+
throw new Error("Invalid quota output format");
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const dataLine = (lines[2] || "").trim().split(/\s+/);
|
|
190
|
+
const usedBlocks = parseInt(dataLine[1] || "0", 10);
|
|
191
|
+
const softBlocks = parseInt(dataLine[2] || "0", 10);
|
|
192
|
+
const hardBlocks = parseInt(dataLine[3] || "0", 10);
|
|
193
|
+
|
|
194
|
+
const usedMB = Math.round(usedBlocks / 1024);
|
|
195
|
+
const softLimitMB = Math.round(softBlocks / 1024);
|
|
196
|
+
const hardLimitMB = Math.round(hardBlocks / 1024);
|
|
197
|
+
|
|
198
|
+
return {
|
|
199
|
+
usedMB,
|
|
200
|
+
softLimitMB,
|
|
201
|
+
hardLimitMB,
|
|
202
|
+
percentUsed: (usedMB / hardLimitMB) * 100,
|
|
203
|
+
isOverSoftLimit: usedMB > softLimitMB,
|
|
204
|
+
isOverHardLimit: usedMB > hardLimitMB,
|
|
205
|
+
};
|
|
206
|
+
},
|
|
207
|
+
catch: (error) =>
|
|
208
|
+
QuotaError(`Failed to get quota usage for ${username}`, error),
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
return usage;
|
|
212
|
+
}),
|
|
213
|
+
|
|
214
|
+
isQuotaAvailable: () =>
|
|
215
|
+
Effect.tryPromise({
|
|
216
|
+
try: async () => {
|
|
217
|
+
try {
|
|
218
|
+
execSync("which quota", { stdio: "ignore" });
|
|
219
|
+
execSync("which setquota", { stdio: "ignore" });
|
|
220
|
+
return true;
|
|
221
|
+
} catch {
|
|
222
|
+
return false;
|
|
223
|
+
}
|
|
224
|
+
},
|
|
225
|
+
catch: (error) => QuotaError("Failed to check quota availability", error),
|
|
226
|
+
}),
|
|
227
|
+
|
|
228
|
+
getDirectorySize: (dirPath: string) =>
|
|
229
|
+
Effect.tryPromise({
|
|
230
|
+
try: async () => {
|
|
231
|
+
const output = execSync(`sudo du -sm ${dirPath}`, {
|
|
232
|
+
encoding: "utf-8",
|
|
233
|
+
});
|
|
234
|
+
return parseInt(output.split("\t")[0] || "0", 10);
|
|
235
|
+
},
|
|
236
|
+
catch: (error) =>
|
|
237
|
+
QuotaError(`Failed to get directory size for ${dirPath}`, error),
|
|
238
|
+
}),
|
|
239
|
+
|
|
240
|
+
checkStorageLimit: (username: string, homeDir: string, limitMB: number) =>
|
|
241
|
+
Effect.gen(function* () {
|
|
242
|
+
const usedMB = yield* Effect.tryPromise({
|
|
243
|
+
try: async () => {
|
|
244
|
+
const output = execSync(`sudo du -sm ${homeDir}`, {
|
|
245
|
+
encoding: "utf-8",
|
|
246
|
+
});
|
|
247
|
+
return parseInt(output.split("\t")[0] || "0", 10);
|
|
248
|
+
},
|
|
249
|
+
catch: (error) =>
|
|
250
|
+
QuotaError(`Failed to check storage for ${username}`, error),
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
return usedMB <= limitMB;
|
|
254
|
+
}),
|
|
255
|
+
});
|
|
256
|
+
}
|