@momentumcms/auth 0.1.0 → 0.1.2
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/CHANGELOG.md +23 -0
- package/index.js +1176 -0
- package/package.json +36 -36
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,26 @@
|
|
|
1
|
+
## 0.1.2 (2026-02-16)
|
|
2
|
+
|
|
3
|
+
### 🩹 Fixes
|
|
4
|
+
|
|
5
|
+
- **release:** centralize manifestRootsToUpdate to update both source and dist ([2b8f832](https://github.com/DonaldMurillo/momentum-cms/commit/2b8f832))
|
|
6
|
+
- **create-app:** fix Angular SSR, Analog builds, and CJS/ESM compatibility ([28d4d0a](https://github.com/DonaldMurillo/momentum-cms/commit/28d4d0a))
|
|
7
|
+
|
|
8
|
+
### ❤️ Thank You
|
|
9
|
+
|
|
10
|
+
- Claude Opus 4.6
|
|
11
|
+
- Donald Murillo @DonaldMurillo
|
|
12
|
+
|
|
13
|
+
## 0.1.1 (2026-02-16)
|
|
14
|
+
|
|
15
|
+
### 🩹 Fixes
|
|
16
|
+
|
|
17
|
+
- **create-app:** fix E2E test and template bugs for full pipeline validation ([4d7e3a9](https://github.com/DonaldMurillo/momentum-cms/commit/4d7e3a9))
|
|
18
|
+
|
|
19
|
+
### ❤️ Thank You
|
|
20
|
+
|
|
21
|
+
- Claude Opus 4.6
|
|
22
|
+
- Donald Murillo @DonaldMurillo
|
|
23
|
+
|
|
1
24
|
## 0.1.0 (2026-02-16)
|
|
2
25
|
|
|
3
26
|
### 🚀 Features
|
package/index.js
ADDED
|
@@ -0,0 +1,1176 @@
|
|
|
1
|
+
// libs/auth/src/lib/auth.ts
|
|
2
|
+
import { betterAuth } from "better-auth";
|
|
3
|
+
import { twoFactor } from "better-auth/plugins";
|
|
4
|
+
|
|
5
|
+
// libs/auth/src/lib/email.ts
|
|
6
|
+
import * as nodemailer from "nodemailer";
|
|
7
|
+
function getEnvConfig() {
|
|
8
|
+
const config = {};
|
|
9
|
+
if (process.env["SMTP_HOST"]) {
|
|
10
|
+
config.host = process.env["SMTP_HOST"];
|
|
11
|
+
}
|
|
12
|
+
if (process.env["SMTP_PORT"]) {
|
|
13
|
+
config.port = parseInt(process.env["SMTP_PORT"], 10);
|
|
14
|
+
}
|
|
15
|
+
if (process.env["SMTP_FROM"]) {
|
|
16
|
+
config.from = process.env["SMTP_FROM"];
|
|
17
|
+
}
|
|
18
|
+
if (process.env["SMTP_SECURE"]) {
|
|
19
|
+
config.secure = process.env["SMTP_SECURE"] === "true";
|
|
20
|
+
}
|
|
21
|
+
if (process.env["SMTP_USER"] && process.env["SMTP_PASS"]) {
|
|
22
|
+
config.auth = {
|
|
23
|
+
user: process.env["SMTP_USER"],
|
|
24
|
+
pass: process.env["SMTP_PASS"]
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
return config;
|
|
28
|
+
}
|
|
29
|
+
function createEmailService(config) {
|
|
30
|
+
const envConfig = getEnvConfig();
|
|
31
|
+
const finalConfig = {
|
|
32
|
+
host: config?.host ?? envConfig.host ?? "localhost",
|
|
33
|
+
port: config?.port ?? envConfig.port ?? 1025,
|
|
34
|
+
from: config?.from ?? envConfig.from ?? "noreply@momentum.local",
|
|
35
|
+
secure: config?.secure ?? envConfig.secure ?? false,
|
|
36
|
+
auth: config?.auth ?? envConfig.auth
|
|
37
|
+
};
|
|
38
|
+
const transportOptions = {
|
|
39
|
+
host: finalConfig.host,
|
|
40
|
+
port: finalConfig.port,
|
|
41
|
+
secure: finalConfig.secure
|
|
42
|
+
};
|
|
43
|
+
if (finalConfig.auth) {
|
|
44
|
+
transportOptions.auth = finalConfig.auth;
|
|
45
|
+
}
|
|
46
|
+
const transporter = nodemailer.createTransport(transportOptions);
|
|
47
|
+
return {
|
|
48
|
+
async sendEmail(options) {
|
|
49
|
+
await transporter.sendMail({
|
|
50
|
+
from: finalConfig.from,
|
|
51
|
+
to: options.to,
|
|
52
|
+
subject: options.subject,
|
|
53
|
+
text: options.text,
|
|
54
|
+
html: options.html
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// libs/auth/src/lib/email-templates.ts
|
|
61
|
+
function escapeHtml(unsafe) {
|
|
62
|
+
return unsafe.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
63
|
+
}
|
|
64
|
+
function wrapEmail(content, safeAppName) {
|
|
65
|
+
return `
|
|
66
|
+
<!DOCTYPE html>
|
|
67
|
+
<html lang="en">
|
|
68
|
+
<head>
|
|
69
|
+
<meta charset="UTF-8">
|
|
70
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
71
|
+
<title>${safeAppName}</title>
|
|
72
|
+
</head>
|
|
73
|
+
<body style="margin: 0; padding: 0; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; background-color: #f4f4f5; line-height: 1.6;">
|
|
74
|
+
<table role="presentation" width="100%" cellspacing="0" cellpadding="0" style="background-color: #f4f4f5;">
|
|
75
|
+
<tr>
|
|
76
|
+
<td style="padding: 40px 20px;">
|
|
77
|
+
<table role="presentation" width="100%" cellspacing="0" cellpadding="0" style="max-width: 480px; margin: 0 auto; background-color: #ffffff; border-radius: 8px; box-shadow: 0 1px 3px rgba(0,0,0,0.1);">
|
|
78
|
+
<tr>
|
|
79
|
+
<td style="padding: 40px;">
|
|
80
|
+
${content}
|
|
81
|
+
</td>
|
|
82
|
+
</tr>
|
|
83
|
+
</table>
|
|
84
|
+
<table role="presentation" width="100%" cellspacing="0" cellpadding="0" style="max-width: 480px; margin: 20px auto 0;">
|
|
85
|
+
<tr>
|
|
86
|
+
<td style="text-align: center; color: #71717a; font-size: 12px;">
|
|
87
|
+
<p style="margin: 0;">© ${(/* @__PURE__ */ new Date()).getFullYear()} ${safeAppName}. All rights reserved.</p>
|
|
88
|
+
</td>
|
|
89
|
+
</tr>
|
|
90
|
+
</table>
|
|
91
|
+
</td>
|
|
92
|
+
</tr>
|
|
93
|
+
</table>
|
|
94
|
+
</body>
|
|
95
|
+
</html>
|
|
96
|
+
`.trim();
|
|
97
|
+
}
|
|
98
|
+
function getPasswordResetEmail(options) {
|
|
99
|
+
const { name, url, appName = "Momentum CMS", expiresIn = "1 hour" } = options;
|
|
100
|
+
const greeting = name ? `Hi ${name},` : "Hi,";
|
|
101
|
+
const subject = `Reset your password - ${appName}`;
|
|
102
|
+
const text2 = `
|
|
103
|
+
${greeting}
|
|
104
|
+
|
|
105
|
+
We received a request to reset your password. Click the link below to choose a new password:
|
|
106
|
+
|
|
107
|
+
${url}
|
|
108
|
+
|
|
109
|
+
This link will expire in ${expiresIn}.
|
|
110
|
+
|
|
111
|
+
If you didn't request a password reset, you can safely ignore this email. Your password will remain unchanged.
|
|
112
|
+
|
|
113
|
+
Thanks,
|
|
114
|
+
The ${appName} Team
|
|
115
|
+
`.trim();
|
|
116
|
+
const safeGreeting = name ? `Hi ${escapeHtml(name)},` : "Hi,";
|
|
117
|
+
const safeUrl = escapeHtml(url);
|
|
118
|
+
const safeAppName = escapeHtml(appName);
|
|
119
|
+
const safeExpiresIn = escapeHtml(expiresIn);
|
|
120
|
+
const html = wrapEmail(
|
|
121
|
+
`
|
|
122
|
+
<h1 style="margin: 0 0 24px; font-size: 24px; font-weight: 600; color: #18181b;">Reset your password</h1>
|
|
123
|
+
<p style="margin: 0 0 16px; color: #3f3f46;">${safeGreeting}</p>
|
|
124
|
+
<p style="margin: 0 0 24px; color: #3f3f46;">We received a request to reset your password. Click the button below to choose a new password:</p>
|
|
125
|
+
<table role="presentation" width="100%" cellspacing="0" cellpadding="0">
|
|
126
|
+
<tr>
|
|
127
|
+
<td style="padding: 0 0 24px;">
|
|
128
|
+
<a href="${safeUrl}" style="display: inline-block; padding: 12px 24px; background-color: #18181b; color: #ffffff; text-decoration: none; border-radius: 6px; font-weight: 500;">Reset Password</a>
|
|
129
|
+
</td>
|
|
130
|
+
</tr>
|
|
131
|
+
</table>
|
|
132
|
+
<p style="margin: 0 0 8px; color: #71717a; font-size: 14px;">This link will expire in ${safeExpiresIn}.</p>
|
|
133
|
+
<p style="margin: 0 0 24px; color: #71717a; font-size: 14px;">If you didn't request a password reset, you can safely ignore this email.</p>
|
|
134
|
+
<hr style="border: none; border-top: 1px solid #e4e4e7; margin: 24px 0;">
|
|
135
|
+
<p style="margin: 0; color: #71717a; font-size: 12px;">If the button doesn't work, copy and paste this URL into your browser:</p>
|
|
136
|
+
<p style="margin: 8px 0 0; color: #71717a; font-size: 12px; word-break: break-all;">${safeUrl}</p>
|
|
137
|
+
`,
|
|
138
|
+
safeAppName
|
|
139
|
+
);
|
|
140
|
+
return { subject, text: text2, html };
|
|
141
|
+
}
|
|
142
|
+
function getVerificationEmail(options) {
|
|
143
|
+
const { name, url, appName = "Momentum CMS", expiresIn = "24 hours" } = options;
|
|
144
|
+
const greeting = name ? `Hi ${name},` : "Hi,";
|
|
145
|
+
const subject = `Verify your email - ${appName}`;
|
|
146
|
+
const text2 = `
|
|
147
|
+
${greeting}
|
|
148
|
+
|
|
149
|
+
Welcome to ${appName}! Please verify your email address by clicking the link below:
|
|
150
|
+
|
|
151
|
+
${url}
|
|
152
|
+
|
|
153
|
+
This link will expire in ${expiresIn}.
|
|
154
|
+
|
|
155
|
+
If you didn't create an account, you can safely ignore this email.
|
|
156
|
+
|
|
157
|
+
Thanks,
|
|
158
|
+
The ${appName} Team
|
|
159
|
+
`.trim();
|
|
160
|
+
const safeGreeting = name ? `Hi ${escapeHtml(name)},` : "Hi,";
|
|
161
|
+
const safeUrl = escapeHtml(url);
|
|
162
|
+
const safeAppName = escapeHtml(appName);
|
|
163
|
+
const safeExpiresIn = escapeHtml(expiresIn);
|
|
164
|
+
const html = wrapEmail(
|
|
165
|
+
`
|
|
166
|
+
<h1 style="margin: 0 0 24px; font-size: 24px; font-weight: 600; color: #18181b;">Verify your email</h1>
|
|
167
|
+
<p style="margin: 0 0 16px; color: #3f3f46;">${safeGreeting}</p>
|
|
168
|
+
<p style="margin: 0 0 24px; color: #3f3f46;">Welcome to ${safeAppName}! Please verify your email address by clicking the button below:</p>
|
|
169
|
+
<table role="presentation" width="100%" cellspacing="0" cellpadding="0">
|
|
170
|
+
<tr>
|
|
171
|
+
<td style="padding: 0 0 24px;">
|
|
172
|
+
<a href="${safeUrl}" style="display: inline-block; padding: 12px 24px; background-color: #18181b; color: #ffffff; text-decoration: none; border-radius: 6px; font-weight: 500;">Verify Email</a>
|
|
173
|
+
</td>
|
|
174
|
+
</tr>
|
|
175
|
+
</table>
|
|
176
|
+
<p style="margin: 0 0 8px; color: #71717a; font-size: 14px;">This link will expire in ${safeExpiresIn}.</p>
|
|
177
|
+
<p style="margin: 0 0 24px; color: #71717a; font-size: 14px;">If you didn't create an account, you can safely ignore this email.</p>
|
|
178
|
+
<hr style="border: none; border-top: 1px solid #e4e4e7; margin: 24px 0;">
|
|
179
|
+
<p style="margin: 0; color: #71717a; font-size: 12px;">If the button doesn't work, copy and paste this URL into your browser:</p>
|
|
180
|
+
<p style="margin: 8px 0 0; color: #71717a; font-size: 12px; word-break: break-all;">${safeUrl}</p>
|
|
181
|
+
`,
|
|
182
|
+
safeAppName
|
|
183
|
+
);
|
|
184
|
+
return { subject, text: text2, html };
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// libs/logger/src/lib/log-level.ts
|
|
188
|
+
var LOG_LEVEL_VALUES = {
|
|
189
|
+
debug: 0,
|
|
190
|
+
info: 1,
|
|
191
|
+
warn: 2,
|
|
192
|
+
error: 3,
|
|
193
|
+
fatal: 4,
|
|
194
|
+
silent: 5
|
|
195
|
+
};
|
|
196
|
+
function shouldLog(messageLevel, configuredLevel) {
|
|
197
|
+
return LOG_LEVEL_VALUES[messageLevel] >= LOG_LEVEL_VALUES[configuredLevel];
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// libs/logger/src/lib/ansi-colors.ts
|
|
201
|
+
var ANSI = {
|
|
202
|
+
reset: "\x1B[0m",
|
|
203
|
+
bold: "\x1B[1m",
|
|
204
|
+
dim: "\x1B[2m",
|
|
205
|
+
// Foreground colors
|
|
206
|
+
red: "\x1B[31m",
|
|
207
|
+
green: "\x1B[32m",
|
|
208
|
+
yellow: "\x1B[33m",
|
|
209
|
+
blue: "\x1B[34m",
|
|
210
|
+
magenta: "\x1B[35m",
|
|
211
|
+
cyan: "\x1B[36m",
|
|
212
|
+
white: "\x1B[37m",
|
|
213
|
+
gray: "\x1B[90m",
|
|
214
|
+
// Background colors
|
|
215
|
+
bgRed: "\x1B[41m",
|
|
216
|
+
bgYellow: "\x1B[43m"
|
|
217
|
+
};
|
|
218
|
+
function colorize(text2, ...codes) {
|
|
219
|
+
if (codes.length === 0)
|
|
220
|
+
return text2;
|
|
221
|
+
return `${codes.join("")}${text2}${ANSI.reset}`;
|
|
222
|
+
}
|
|
223
|
+
function supportsColor() {
|
|
224
|
+
if (process.env["FORCE_COLOR"] === "1")
|
|
225
|
+
return true;
|
|
226
|
+
if (process.env["NO_COLOR"] !== void 0)
|
|
227
|
+
return false;
|
|
228
|
+
if (process.env["TERM"] === "dumb")
|
|
229
|
+
return false;
|
|
230
|
+
return process.stdout.isTTY === true;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// libs/logger/src/lib/formatters.ts
|
|
234
|
+
var LEVEL_COLORS = {
|
|
235
|
+
debug: [ANSI.dim, ANSI.gray],
|
|
236
|
+
info: [ANSI.cyan],
|
|
237
|
+
warn: [ANSI.yellow],
|
|
238
|
+
error: [ANSI.red],
|
|
239
|
+
fatal: [ANSI.bold, ANSI.white, ANSI.bgRed]
|
|
240
|
+
};
|
|
241
|
+
function padLevel(level) {
|
|
242
|
+
return level.toUpperCase().padEnd(5);
|
|
243
|
+
}
|
|
244
|
+
function formatTimestamp(date2) {
|
|
245
|
+
const y = date2.getFullYear();
|
|
246
|
+
const mo = String(date2.getMonth() + 1).padStart(2, "0");
|
|
247
|
+
const d = String(date2.getDate()).padStart(2, "0");
|
|
248
|
+
const h = String(date2.getHours()).padStart(2, "0");
|
|
249
|
+
const mi = String(date2.getMinutes()).padStart(2, "0");
|
|
250
|
+
const s = String(date2.getSeconds()).padStart(2, "0");
|
|
251
|
+
const ms = String(date2.getMilliseconds()).padStart(3, "0");
|
|
252
|
+
return `${y}-${mo}-${d} ${h}:${mi}:${s}.${ms}`;
|
|
253
|
+
}
|
|
254
|
+
function formatData(data) {
|
|
255
|
+
const entries = Object.entries(data);
|
|
256
|
+
if (entries.length === 0)
|
|
257
|
+
return "";
|
|
258
|
+
return " " + entries.map(([k, v]) => `${k}=${typeof v === "string" ? v : JSON.stringify(v)}`).join(" ");
|
|
259
|
+
}
|
|
260
|
+
function prettyFormatter(entry) {
|
|
261
|
+
const useColor = supportsColor();
|
|
262
|
+
const level = entry.level;
|
|
263
|
+
const ts = formatTimestamp(entry.timestamp);
|
|
264
|
+
const levelStr = padLevel(entry.level);
|
|
265
|
+
const ctx = `[${entry.context}]`;
|
|
266
|
+
const msg = entry.message;
|
|
267
|
+
const enrichmentStr = entry.enrichments ? formatData(entry.enrichments) : "";
|
|
268
|
+
const dataStr = entry.data ? formatData(entry.data) : "";
|
|
269
|
+
const extra = `${enrichmentStr}${dataStr}`;
|
|
270
|
+
if (useColor) {
|
|
271
|
+
const colors = LEVEL_COLORS[level];
|
|
272
|
+
const coloredLevel = colorize(levelStr, ...colors);
|
|
273
|
+
const coloredCtx = colorize(ctx, ANSI.magenta);
|
|
274
|
+
const coloredTs = colorize(ts, ANSI.gray);
|
|
275
|
+
return `${coloredTs} ${coloredLevel} ${coloredCtx} ${msg}${extra}
|
|
276
|
+
`;
|
|
277
|
+
}
|
|
278
|
+
return `${ts} ${levelStr} ${ctx} ${msg}${extra}
|
|
279
|
+
`;
|
|
280
|
+
}
|
|
281
|
+
function jsonFormatter(entry) {
|
|
282
|
+
const output = {
|
|
283
|
+
timestamp: entry.timestamp.toISOString(),
|
|
284
|
+
level: entry.level,
|
|
285
|
+
context: entry.context,
|
|
286
|
+
message: entry.message
|
|
287
|
+
};
|
|
288
|
+
if (entry.enrichments && Object.keys(entry.enrichments).length > 0) {
|
|
289
|
+
Object.assign(output, entry.enrichments);
|
|
290
|
+
}
|
|
291
|
+
if (entry.data && Object.keys(entry.data).length > 0) {
|
|
292
|
+
output["data"] = entry.data;
|
|
293
|
+
}
|
|
294
|
+
return JSON.stringify(output) + "\n";
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// libs/logger/src/lib/logger-config.types.ts
|
|
298
|
+
function resolveLoggingConfig(config) {
|
|
299
|
+
return {
|
|
300
|
+
level: config?.level ?? "info",
|
|
301
|
+
format: config?.format ?? "pretty",
|
|
302
|
+
timestamps: config?.timestamps ?? true,
|
|
303
|
+
output: config?.output ?? ((msg) => {
|
|
304
|
+
process.stdout.write(msg);
|
|
305
|
+
}),
|
|
306
|
+
errorOutput: config?.errorOutput ?? ((msg) => {
|
|
307
|
+
process.stderr.write(msg);
|
|
308
|
+
})
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// libs/logger/src/lib/logger.ts
|
|
313
|
+
var ERROR_LEVELS = /* @__PURE__ */ new Set(["warn", "error", "fatal"]);
|
|
314
|
+
var MomentumLogger = class _MomentumLogger {
|
|
315
|
+
static {
|
|
316
|
+
this.enrichers = [];
|
|
317
|
+
}
|
|
318
|
+
constructor(context, config) {
|
|
319
|
+
this.context = context;
|
|
320
|
+
this.config = isResolvedConfig(config) ? config : resolveLoggingConfig(config);
|
|
321
|
+
this.formatter = this.config.format === "json" ? jsonFormatter : prettyFormatter;
|
|
322
|
+
}
|
|
323
|
+
debug(message, data) {
|
|
324
|
+
this.log("debug", message, data);
|
|
325
|
+
}
|
|
326
|
+
info(message, data) {
|
|
327
|
+
this.log("info", message, data);
|
|
328
|
+
}
|
|
329
|
+
warn(message, data) {
|
|
330
|
+
this.log("warn", message, data);
|
|
331
|
+
}
|
|
332
|
+
error(message, data) {
|
|
333
|
+
this.log("error", message, data);
|
|
334
|
+
}
|
|
335
|
+
fatal(message, data) {
|
|
336
|
+
this.log("fatal", message, data);
|
|
337
|
+
}
|
|
338
|
+
/**
|
|
339
|
+
* Creates a child logger with a sub-context.
|
|
340
|
+
* e.g., `Momentum:DB` → `Momentum:DB:Migrate`
|
|
341
|
+
*/
|
|
342
|
+
child(subContext) {
|
|
343
|
+
return new _MomentumLogger(`${this.context}:${subContext}`, this.config);
|
|
344
|
+
}
|
|
345
|
+
/**
|
|
346
|
+
* Registers a global enricher that adds extra fields to all log entries.
|
|
347
|
+
*/
|
|
348
|
+
static registerEnricher(enricher) {
|
|
349
|
+
_MomentumLogger.enrichers.push(enricher);
|
|
350
|
+
}
|
|
351
|
+
/**
|
|
352
|
+
* Removes a previously registered enricher.
|
|
353
|
+
*/
|
|
354
|
+
static removeEnricher(enricher) {
|
|
355
|
+
const index = _MomentumLogger.enrichers.indexOf(enricher);
|
|
356
|
+
if (index >= 0) {
|
|
357
|
+
_MomentumLogger.enrichers.splice(index, 1);
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
/**
|
|
361
|
+
* Clears all registered enrichers. Primarily for testing.
|
|
362
|
+
*/
|
|
363
|
+
static clearEnrichers() {
|
|
364
|
+
_MomentumLogger.enrichers.length = 0;
|
|
365
|
+
}
|
|
366
|
+
log(level, message, data) {
|
|
367
|
+
if (!shouldLog(level, this.config.level))
|
|
368
|
+
return;
|
|
369
|
+
const enrichments = this.collectEnrichments();
|
|
370
|
+
const entry = {
|
|
371
|
+
timestamp: /* @__PURE__ */ new Date(),
|
|
372
|
+
level,
|
|
373
|
+
context: this.context,
|
|
374
|
+
message,
|
|
375
|
+
data,
|
|
376
|
+
enrichments: Object.keys(enrichments).length > 0 ? enrichments : void 0
|
|
377
|
+
};
|
|
378
|
+
const formatted = this.formatter(entry);
|
|
379
|
+
if (ERROR_LEVELS.has(level)) {
|
|
380
|
+
this.config.errorOutput(formatted);
|
|
381
|
+
} else {
|
|
382
|
+
this.config.output(formatted);
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
collectEnrichments() {
|
|
386
|
+
const result = {};
|
|
387
|
+
for (const enricher of _MomentumLogger.enrichers) {
|
|
388
|
+
Object.assign(result, enricher.enrich());
|
|
389
|
+
}
|
|
390
|
+
return result;
|
|
391
|
+
}
|
|
392
|
+
};
|
|
393
|
+
function isResolvedConfig(config) {
|
|
394
|
+
if (!config)
|
|
395
|
+
return false;
|
|
396
|
+
return typeof config.level === "string" && typeof config.format === "string" && typeof config.timestamps === "boolean" && // eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- type guard narrows union
|
|
397
|
+
typeof config.output === "function" && // eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- type guard narrows union
|
|
398
|
+
typeof config.errorOutput === "function";
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
// libs/logger/src/lib/logger-singleton.ts
|
|
402
|
+
var loggerInstance = null;
|
|
403
|
+
var ROOT_CONTEXT = "Momentum";
|
|
404
|
+
function getMomentumLogger() {
|
|
405
|
+
if (!loggerInstance) {
|
|
406
|
+
loggerInstance = new MomentumLogger(ROOT_CONTEXT);
|
|
407
|
+
}
|
|
408
|
+
return loggerInstance;
|
|
409
|
+
}
|
|
410
|
+
function createLogger(context) {
|
|
411
|
+
return getMomentumLogger().child(context);
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
// libs/auth/src/lib/auth.ts
|
|
415
|
+
function isLegacyConfig(config) {
|
|
416
|
+
return "database" in config && !("db" in config);
|
|
417
|
+
}
|
|
418
|
+
function buildSocialProviders(config, baseURL) {
|
|
419
|
+
const providers = {};
|
|
420
|
+
const resolvedBaseURL = baseURL ?? "http://localhost:4000";
|
|
421
|
+
const googleClientId = config?.google?.clientId ?? process.env["GOOGLE_CLIENT_ID"];
|
|
422
|
+
const googleClientSecret = config?.google?.clientSecret ?? process.env["GOOGLE_CLIENT_SECRET"];
|
|
423
|
+
if (googleClientId && googleClientSecret) {
|
|
424
|
+
providers["google"] = {
|
|
425
|
+
clientId: googleClientId,
|
|
426
|
+
clientSecret: googleClientSecret,
|
|
427
|
+
redirectURI: config?.google?.redirectURI ?? `${resolvedBaseURL}/api/auth/callback/google`
|
|
428
|
+
};
|
|
429
|
+
}
|
|
430
|
+
const githubClientId = config?.github?.clientId ?? process.env["GITHUB_CLIENT_ID"];
|
|
431
|
+
const githubClientSecret = config?.github?.clientSecret ?? process.env["GITHUB_CLIENT_SECRET"];
|
|
432
|
+
if (githubClientId && githubClientSecret) {
|
|
433
|
+
providers["github"] = {
|
|
434
|
+
clientId: githubClientId,
|
|
435
|
+
clientSecret: githubClientSecret,
|
|
436
|
+
redirectURI: config?.github?.redirectURI ?? `${resolvedBaseURL}/api/auth/callback/github`
|
|
437
|
+
};
|
|
438
|
+
}
|
|
439
|
+
return Object.keys(providers).length > 0 ? providers : void 0;
|
|
440
|
+
}
|
|
441
|
+
function getEnabledOAuthProviders(config) {
|
|
442
|
+
const providers = [];
|
|
443
|
+
const googleClientId = config?.google?.clientId ?? process.env["GOOGLE_CLIENT_ID"];
|
|
444
|
+
const googleClientSecret = config?.google?.clientSecret ?? process.env["GOOGLE_CLIENT_SECRET"];
|
|
445
|
+
if (googleClientId && googleClientSecret) {
|
|
446
|
+
providers.push("google");
|
|
447
|
+
}
|
|
448
|
+
const githubClientId = config?.github?.clientId ?? process.env["GITHUB_CLIENT_ID"];
|
|
449
|
+
const githubClientSecret = config?.github?.clientSecret ?? process.env["GITHUB_CLIENT_SECRET"];
|
|
450
|
+
if (githubClientId && githubClientSecret) {
|
|
451
|
+
providers.push("github");
|
|
452
|
+
}
|
|
453
|
+
return providers;
|
|
454
|
+
}
|
|
455
|
+
function convertFieldsToAdditionalFields(fields) {
|
|
456
|
+
const result = {};
|
|
457
|
+
for (const field of fields) {
|
|
458
|
+
let baType;
|
|
459
|
+
switch (field.type) {
|
|
460
|
+
case "checkbox":
|
|
461
|
+
baType = "boolean";
|
|
462
|
+
break;
|
|
463
|
+
case "number":
|
|
464
|
+
baType = "number";
|
|
465
|
+
break;
|
|
466
|
+
case "date":
|
|
467
|
+
baType = "string";
|
|
468
|
+
break;
|
|
469
|
+
default:
|
|
470
|
+
baType = "string";
|
|
471
|
+
break;
|
|
472
|
+
}
|
|
473
|
+
result[field.name] = {
|
|
474
|
+
type: baType,
|
|
475
|
+
required: field.required ?? false,
|
|
476
|
+
input: false
|
|
477
|
+
// Sub-plugin fields are not user-settable by default
|
|
478
|
+
};
|
|
479
|
+
}
|
|
480
|
+
return result;
|
|
481
|
+
}
|
|
482
|
+
function createMomentumAuth(config) {
|
|
483
|
+
const dbConfig = isLegacyConfig(config) ? { type: "sqlite", database: config.database } : config.db;
|
|
484
|
+
const {
|
|
485
|
+
baseURL,
|
|
486
|
+
secret,
|
|
487
|
+
trustedOrigins,
|
|
488
|
+
email: emailConfig,
|
|
489
|
+
socialProviders,
|
|
490
|
+
twoFactorAuth
|
|
491
|
+
} = config;
|
|
492
|
+
const extraPlugins = !isLegacyConfig(config) ? config.plugins ?? [] : [];
|
|
493
|
+
const extraUserFields = !isLegacyConfig(config) ? config.userFields ?? [] : [];
|
|
494
|
+
const databaseOption = dbConfig.type === "sqlite" ? dbConfig.database : dbConfig.pool;
|
|
495
|
+
const emailEnabled = emailConfig?.enabled ?? !!process.env["SMTP_HOST"];
|
|
496
|
+
const appName = emailConfig?.appName ?? "Momentum CMS";
|
|
497
|
+
let emailService = null;
|
|
498
|
+
if (emailEnabled) {
|
|
499
|
+
emailService = createEmailService(emailConfig);
|
|
500
|
+
}
|
|
501
|
+
const emailAndPasswordConfig = {
|
|
502
|
+
enabled: true,
|
|
503
|
+
minPasswordLength: 8
|
|
504
|
+
};
|
|
505
|
+
if (emailService) {
|
|
506
|
+
emailAndPasswordConfig.sendResetPassword = async ({ user, url }) => {
|
|
507
|
+
const { subject, text: text2, html } = getPasswordResetEmail({
|
|
508
|
+
name: user.name,
|
|
509
|
+
url,
|
|
510
|
+
appName,
|
|
511
|
+
expiresIn: "1 hour"
|
|
512
|
+
});
|
|
513
|
+
emailService.sendEmail({
|
|
514
|
+
to: user.email,
|
|
515
|
+
subject,
|
|
516
|
+
text: text2,
|
|
517
|
+
html
|
|
518
|
+
}).catch((err) => {
|
|
519
|
+
createLogger("Auth").error(
|
|
520
|
+
`Failed to send password reset email: ${err instanceof Error ? err.message : String(err)}`
|
|
521
|
+
);
|
|
522
|
+
});
|
|
523
|
+
};
|
|
524
|
+
}
|
|
525
|
+
const requireVerification = emailConfig?.requireEmailVerification ?? false;
|
|
526
|
+
const emailVerificationConfig = emailService ? {
|
|
527
|
+
sendOnSignUp: true,
|
|
528
|
+
autoSignInAfterVerification: true,
|
|
529
|
+
expiresIn: 86400,
|
|
530
|
+
// 24 hours
|
|
531
|
+
sendVerificationEmail: async ({
|
|
532
|
+
user,
|
|
533
|
+
url
|
|
534
|
+
}) => {
|
|
535
|
+
const { subject, text: text2, html } = getVerificationEmail({
|
|
536
|
+
name: user.name,
|
|
537
|
+
url,
|
|
538
|
+
appName,
|
|
539
|
+
expiresIn: "24 hours"
|
|
540
|
+
});
|
|
541
|
+
emailService.sendEmail({
|
|
542
|
+
to: user.email,
|
|
543
|
+
subject,
|
|
544
|
+
text: text2,
|
|
545
|
+
html
|
|
546
|
+
}).catch((err) => {
|
|
547
|
+
createLogger("Auth").error(
|
|
548
|
+
`Failed to send verification email: ${err instanceof Error ? err.message : String(err)}`
|
|
549
|
+
);
|
|
550
|
+
});
|
|
551
|
+
}
|
|
552
|
+
} : void 0;
|
|
553
|
+
if (requireVerification && emailAndPasswordConfig) {
|
|
554
|
+
emailAndPasswordConfig.requireEmailVerification = true;
|
|
555
|
+
}
|
|
556
|
+
const socialProvidersConfig = buildSocialProviders(socialProviders, baseURL);
|
|
557
|
+
const plugins = [];
|
|
558
|
+
if (twoFactorAuth) {
|
|
559
|
+
plugins.push(twoFactor());
|
|
560
|
+
}
|
|
561
|
+
for (const p of extraPlugins) {
|
|
562
|
+
if (p !== void 0) {
|
|
563
|
+
plugins.push(p);
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
return betterAuth({
|
|
567
|
+
database: databaseOption,
|
|
568
|
+
baseURL: baseURL ?? "http://localhost:4000",
|
|
569
|
+
secret: secret ?? process.env["AUTH_SECRET"] ?? "momentum-cms-dev-secret-change-in-production",
|
|
570
|
+
trustedOrigins: trustedOrigins ?? [baseURL ?? "http://localhost:4000"],
|
|
571
|
+
// Enable email/password authentication with optional password reset
|
|
572
|
+
emailAndPassword: emailAndPasswordConfig,
|
|
573
|
+
// Email verification (only if email is enabled)
|
|
574
|
+
...emailVerificationConfig && { emailVerification: emailVerificationConfig },
|
|
575
|
+
// Social login providers (only if configured)
|
|
576
|
+
...socialProvidersConfig && { socialProviders: socialProvidersConfig },
|
|
577
|
+
// Plugins (2FA, etc.)
|
|
578
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/consistent-type-assertions -- Better Auth plugin types are opaque from sub-plugins
|
|
579
|
+
...plugins.length > 0 && { plugins },
|
|
580
|
+
// Add custom role field to users + any extra user fields from sub-plugins.
|
|
581
|
+
// Base role is spread AFTER sub-plugin fields so it cannot be overwritten
|
|
582
|
+
// (a sub-plugin field named 'role' would lose defaultValue and input protection).
|
|
583
|
+
user: {
|
|
584
|
+
additionalFields: {
|
|
585
|
+
// Convert Momentum Field definitions to Better Auth additionalFields format
|
|
586
|
+
...convertFieldsToAdditionalFields(extraUserFields.filter((f) => f.name !== "role")),
|
|
587
|
+
role: {
|
|
588
|
+
type: "string",
|
|
589
|
+
required: false,
|
|
590
|
+
defaultValue: "user",
|
|
591
|
+
input: false
|
|
592
|
+
// Don't allow users to set their own role
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
},
|
|
596
|
+
// Session configuration
|
|
597
|
+
// Note: cookieCache is intentionally disabled. It caches session data
|
|
598
|
+
// (including role) in a signed cookie, which causes stale role issues
|
|
599
|
+
// when roles are updated after session creation (e.g., setup flow).
|
|
600
|
+
session: {
|
|
601
|
+
expiresIn: 60 * 60 * 24 * 7,
|
|
602
|
+
// 7 days
|
|
603
|
+
updateAge: 60 * 60 * 24
|
|
604
|
+
// Update session every 24 hours
|
|
605
|
+
}
|
|
606
|
+
});
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
// libs/core/src/lib/collections/define-collection.ts
|
|
610
|
+
function defineCollection(config) {
|
|
611
|
+
const collection = {
|
|
612
|
+
timestamps: true,
|
|
613
|
+
// Enable timestamps by default
|
|
614
|
+
...config
|
|
615
|
+
};
|
|
616
|
+
if (!collection.slug) {
|
|
617
|
+
throw new Error("Collection must have a slug");
|
|
618
|
+
}
|
|
619
|
+
if (!collection.fields || collection.fields.length === 0) {
|
|
620
|
+
throw new Error(`Collection "${collection.slug}" must have at least one field`);
|
|
621
|
+
}
|
|
622
|
+
if (!/^[a-z][a-z0-9-]*$/.test(collection.slug)) {
|
|
623
|
+
throw new Error(
|
|
624
|
+
`Collection slug "${collection.slug}" must be kebab-case (lowercase letters, numbers, and hyphens, starting with a letter)`
|
|
625
|
+
);
|
|
626
|
+
}
|
|
627
|
+
return collection;
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
// libs/core/src/lib/fields/field-builders.ts
|
|
631
|
+
function text(name, options = {}) {
|
|
632
|
+
return {
|
|
633
|
+
name,
|
|
634
|
+
type: "text",
|
|
635
|
+
...options
|
|
636
|
+
};
|
|
637
|
+
}
|
|
638
|
+
function number(name, options = {}) {
|
|
639
|
+
return {
|
|
640
|
+
name,
|
|
641
|
+
type: "number",
|
|
642
|
+
...options
|
|
643
|
+
};
|
|
644
|
+
}
|
|
645
|
+
function date(name, options = {}) {
|
|
646
|
+
return {
|
|
647
|
+
name,
|
|
648
|
+
type: "date",
|
|
649
|
+
...options
|
|
650
|
+
};
|
|
651
|
+
}
|
|
652
|
+
function checkbox(name, options = {}) {
|
|
653
|
+
return {
|
|
654
|
+
name,
|
|
655
|
+
type: "checkbox",
|
|
656
|
+
...options,
|
|
657
|
+
defaultValue: options.defaultValue ?? false
|
|
658
|
+
};
|
|
659
|
+
}
|
|
660
|
+
function select(name, options) {
|
|
661
|
+
return {
|
|
662
|
+
name,
|
|
663
|
+
type: "select",
|
|
664
|
+
...options
|
|
665
|
+
};
|
|
666
|
+
}
|
|
667
|
+
function email(name, options = {}) {
|
|
668
|
+
return {
|
|
669
|
+
name,
|
|
670
|
+
type: "email",
|
|
671
|
+
...options
|
|
672
|
+
};
|
|
673
|
+
}
|
|
674
|
+
function relationship(name, options) {
|
|
675
|
+
return {
|
|
676
|
+
name,
|
|
677
|
+
type: "relationship",
|
|
678
|
+
...options
|
|
679
|
+
};
|
|
680
|
+
}
|
|
681
|
+
function json(name, options = {}) {
|
|
682
|
+
return {
|
|
683
|
+
name,
|
|
684
|
+
type: "json",
|
|
685
|
+
...options
|
|
686
|
+
};
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
// libs/core/src/lib/collections/media.collection.ts
|
|
690
|
+
var MediaCollection = defineCollection({
|
|
691
|
+
slug: "media",
|
|
692
|
+
labels: {
|
|
693
|
+
singular: "Media",
|
|
694
|
+
plural: "Media"
|
|
695
|
+
},
|
|
696
|
+
admin: {
|
|
697
|
+
useAsTitle: "filename",
|
|
698
|
+
defaultColumns: ["filename", "mimeType", "filesize", "createdAt"]
|
|
699
|
+
},
|
|
700
|
+
fields: [
|
|
701
|
+
text("filename", {
|
|
702
|
+
required: true,
|
|
703
|
+
label: "Filename",
|
|
704
|
+
description: "Original filename of the uploaded file"
|
|
705
|
+
}),
|
|
706
|
+
text("mimeType", {
|
|
707
|
+
required: true,
|
|
708
|
+
label: "MIME Type",
|
|
709
|
+
description: "File MIME type (e.g., image/jpeg, application/pdf)"
|
|
710
|
+
}),
|
|
711
|
+
number("filesize", {
|
|
712
|
+
label: "File Size",
|
|
713
|
+
description: "File size in bytes"
|
|
714
|
+
}),
|
|
715
|
+
text("path", {
|
|
716
|
+
required: true,
|
|
717
|
+
label: "Storage Path",
|
|
718
|
+
description: "Path/key where the file is stored",
|
|
719
|
+
admin: {
|
|
720
|
+
hidden: true
|
|
721
|
+
}
|
|
722
|
+
}),
|
|
723
|
+
text("url", {
|
|
724
|
+
label: "URL",
|
|
725
|
+
description: "Public URL to access the file"
|
|
726
|
+
}),
|
|
727
|
+
text("alt", {
|
|
728
|
+
label: "Alt Text",
|
|
729
|
+
description: "Alternative text for accessibility"
|
|
730
|
+
}),
|
|
731
|
+
number("width", {
|
|
732
|
+
label: "Width",
|
|
733
|
+
description: "Image width in pixels (for images only)"
|
|
734
|
+
}),
|
|
735
|
+
number("height", {
|
|
736
|
+
label: "Height",
|
|
737
|
+
description: "Image height in pixels (for images only)"
|
|
738
|
+
}),
|
|
739
|
+
json("focalPoint", {
|
|
740
|
+
label: "Focal Point",
|
|
741
|
+
description: "Focal point coordinates for image cropping",
|
|
742
|
+
admin: {
|
|
743
|
+
hidden: true
|
|
744
|
+
}
|
|
745
|
+
})
|
|
746
|
+
],
|
|
747
|
+
access: {
|
|
748
|
+
// Media is readable by anyone by default
|
|
749
|
+
read: () => true,
|
|
750
|
+
// Only authenticated users can create/update/delete
|
|
751
|
+
create: ({ req }) => !!req?.user,
|
|
752
|
+
update: ({ req }) => !!req?.user,
|
|
753
|
+
delete: ({ req }) => !!req?.user
|
|
754
|
+
}
|
|
755
|
+
});
|
|
756
|
+
|
|
757
|
+
// libs/auth/src/lib/auth-collections.ts
|
|
758
|
+
var AUTH_ROLES = [
|
|
759
|
+
{ label: "Admin", value: "admin" },
|
|
760
|
+
{ label: "Editor", value: "editor" },
|
|
761
|
+
{ label: "User", value: "user" },
|
|
762
|
+
{ label: "Viewer", value: "viewer" }
|
|
763
|
+
];
|
|
764
|
+
var AuthUserCollection = defineCollection({
|
|
765
|
+
slug: "auth-user",
|
|
766
|
+
dbName: "user",
|
|
767
|
+
timestamps: true,
|
|
768
|
+
labels: { singular: "User", plural: "Users" },
|
|
769
|
+
fields: [
|
|
770
|
+
text("name", { required: true }),
|
|
771
|
+
email("email", { required: true }),
|
|
772
|
+
checkbox("emailVerified"),
|
|
773
|
+
text("image"),
|
|
774
|
+
select("role", {
|
|
775
|
+
options: AUTH_ROLES,
|
|
776
|
+
defaultValue: "user"
|
|
777
|
+
})
|
|
778
|
+
],
|
|
779
|
+
indexes: [{ columns: ["email"], unique: true }],
|
|
780
|
+
admin: {
|
|
781
|
+
group: "Authentication",
|
|
782
|
+
useAsTitle: "email",
|
|
783
|
+
defaultColumns: ["name", "email", "role", "createdAt"],
|
|
784
|
+
description: "Users authenticated via Better Auth"
|
|
785
|
+
},
|
|
786
|
+
access: {
|
|
787
|
+
admin: ({ req }) => req.user?.role === "admin",
|
|
788
|
+
read: ({ req }) => req.user?.role === "admin",
|
|
789
|
+
create: ({ req }) => req.user?.role === "admin",
|
|
790
|
+
update: ({ req }) => req.user?.role === "admin",
|
|
791
|
+
delete: ({ req }) => req.user?.role === "admin"
|
|
792
|
+
}
|
|
793
|
+
});
|
|
794
|
+
var AuthSessionCollection = defineCollection({
|
|
795
|
+
slug: "auth-session",
|
|
796
|
+
dbName: "session",
|
|
797
|
+
managed: true,
|
|
798
|
+
timestamps: true,
|
|
799
|
+
fields: [
|
|
800
|
+
text("userId", { required: true }),
|
|
801
|
+
text("token", { required: true }),
|
|
802
|
+
date("expiresAt", { required: true }),
|
|
803
|
+
text("ipAddress"),
|
|
804
|
+
text("userAgent")
|
|
805
|
+
],
|
|
806
|
+
indexes: [{ columns: ["userId"] }, { columns: ["token"], unique: true }],
|
|
807
|
+
admin: {
|
|
808
|
+
group: "Authentication",
|
|
809
|
+
hidden: true,
|
|
810
|
+
description: "Active user sessions"
|
|
811
|
+
},
|
|
812
|
+
access: {
|
|
813
|
+
admin: ({ req }) => req.user?.role === "admin",
|
|
814
|
+
read: ({ req }) => req.user?.role === "admin",
|
|
815
|
+
create: () => false,
|
|
816
|
+
update: () => false,
|
|
817
|
+
delete: ({ req }) => req.user?.role === "admin"
|
|
818
|
+
}
|
|
819
|
+
});
|
|
820
|
+
var AuthAccountCollection = defineCollection({
|
|
821
|
+
slug: "auth-account",
|
|
822
|
+
dbName: "account",
|
|
823
|
+
managed: true,
|
|
824
|
+
timestamps: true,
|
|
825
|
+
fields: [
|
|
826
|
+
text("userId", { required: true }),
|
|
827
|
+
text("accountId", { required: true }),
|
|
828
|
+
text("providerId", { required: true }),
|
|
829
|
+
text("accessToken"),
|
|
830
|
+
text("refreshToken"),
|
|
831
|
+
date("accessTokenExpiresAt"),
|
|
832
|
+
date("refreshTokenExpiresAt"),
|
|
833
|
+
text("scope"),
|
|
834
|
+
text("idToken"),
|
|
835
|
+
text("password")
|
|
836
|
+
],
|
|
837
|
+
indexes: [{ columns: ["userId"] }],
|
|
838
|
+
admin: {
|
|
839
|
+
group: "Authentication",
|
|
840
|
+
hidden: true,
|
|
841
|
+
description: "OAuth and credential accounts"
|
|
842
|
+
},
|
|
843
|
+
access: {
|
|
844
|
+
admin: ({ req }) => req.user?.role === "admin",
|
|
845
|
+
read: () => false,
|
|
846
|
+
// Never expose OAuth tokens/password hashes via API — Better Auth owns this data
|
|
847
|
+
create: () => false,
|
|
848
|
+
update: () => false,
|
|
849
|
+
delete: () => false
|
|
850
|
+
}
|
|
851
|
+
});
|
|
852
|
+
var AuthVerificationCollection = defineCollection({
|
|
853
|
+
slug: "auth-verification",
|
|
854
|
+
dbName: "verification",
|
|
855
|
+
managed: true,
|
|
856
|
+
timestamps: true,
|
|
857
|
+
fields: [
|
|
858
|
+
text("identifier", { required: true }),
|
|
859
|
+
text("value", { required: true }),
|
|
860
|
+
date("expiresAt", { required: true })
|
|
861
|
+
],
|
|
862
|
+
admin: {
|
|
863
|
+
group: "Authentication",
|
|
864
|
+
hidden: true,
|
|
865
|
+
description: "Email verification and password reset tokens"
|
|
866
|
+
},
|
|
867
|
+
access: {
|
|
868
|
+
admin: ({ req }) => req.user?.role === "admin",
|
|
869
|
+
read: () => false,
|
|
870
|
+
create: () => false,
|
|
871
|
+
update: () => false,
|
|
872
|
+
delete: () => false
|
|
873
|
+
}
|
|
874
|
+
});
|
|
875
|
+
var AuthApiKeysCollection = defineCollection({
|
|
876
|
+
slug: "auth-api-keys",
|
|
877
|
+
dbName: "_api_keys",
|
|
878
|
+
timestamps: true,
|
|
879
|
+
fields: [
|
|
880
|
+
text("name", { required: true }),
|
|
881
|
+
text("keyHash", { required: true, admin: { hidden: true }, access: { read: () => false } }),
|
|
882
|
+
text("keyPrefix", { required: true }),
|
|
883
|
+
relationship("createdBy", {
|
|
884
|
+
required: true,
|
|
885
|
+
collection: () => AuthUserCollection,
|
|
886
|
+
label: "Created By"
|
|
887
|
+
}),
|
|
888
|
+
select("role", {
|
|
889
|
+
options: AUTH_ROLES,
|
|
890
|
+
defaultValue: "user"
|
|
891
|
+
}),
|
|
892
|
+
date("expiresAt"),
|
|
893
|
+
date("lastUsedAt")
|
|
894
|
+
],
|
|
895
|
+
indexes: [{ columns: ["keyHash"], unique: true }, { columns: ["createdBy"] }],
|
|
896
|
+
admin: {
|
|
897
|
+
group: "Authentication",
|
|
898
|
+
useAsTitle: "name",
|
|
899
|
+
defaultColumns: ["name", "keyPrefix", "role", "createdBy", "createdAt", "lastUsedAt"],
|
|
900
|
+
description: "API keys for programmatic access",
|
|
901
|
+
headerActions: [
|
|
902
|
+
{ id: "generate-key", label: "Generate API Key", endpoint: "/api/auth/api-keys" }
|
|
903
|
+
]
|
|
904
|
+
},
|
|
905
|
+
access: {
|
|
906
|
+
admin: ({ req }) => !!req.user,
|
|
907
|
+
read: ({ req }) => !!req.user,
|
|
908
|
+
create: () => false,
|
|
909
|
+
// API keys must be created through dedicated /api/auth/api-keys endpoint
|
|
910
|
+
update: () => false,
|
|
911
|
+
delete: () => false
|
|
912
|
+
// Deletion only via dedicated /api/auth/api-keys/:id (has ownership checks)
|
|
913
|
+
},
|
|
914
|
+
defaultWhere: (req) => {
|
|
915
|
+
if (!req.user)
|
|
916
|
+
return { createdBy: "__none__" };
|
|
917
|
+
if (req.user.role === "admin")
|
|
918
|
+
return void 0;
|
|
919
|
+
return { createdBy: req.user.id };
|
|
920
|
+
}
|
|
921
|
+
});
|
|
922
|
+
var BASE_AUTH_COLLECTIONS = [
|
|
923
|
+
AuthUserCollection,
|
|
924
|
+
AuthSessionCollection,
|
|
925
|
+
AuthAccountCollection,
|
|
926
|
+
AuthVerificationCollection,
|
|
927
|
+
AuthApiKeysCollection
|
|
928
|
+
];
|
|
929
|
+
|
|
930
|
+
// libs/auth/src/lib/auth-plugin.ts
|
|
931
|
+
function momentumAuth(config) {
|
|
932
|
+
let authInstance = null;
|
|
933
|
+
const subPlugins = config.plugins ?? [];
|
|
934
|
+
const allCollections = [];
|
|
935
|
+
const allUserFields = [...config.userFields ?? []];
|
|
936
|
+
const allSessionFields = [];
|
|
937
|
+
const allBetterAuthPlugins = [];
|
|
938
|
+
for (const sp of subPlugins) {
|
|
939
|
+
if (sp.collections)
|
|
940
|
+
allCollections.push(...sp.collections);
|
|
941
|
+
if (sp.userFields)
|
|
942
|
+
allUserFields.push(...sp.userFields);
|
|
943
|
+
if (sp.sessionFields)
|
|
944
|
+
allSessionFields.push(...sp.sessionFields);
|
|
945
|
+
if (sp.betterAuthPlugin !== void 0)
|
|
946
|
+
allBetterAuthPlugins.push(sp.betterAuthPlugin);
|
|
947
|
+
}
|
|
948
|
+
const authUserWithFields = {
|
|
949
|
+
...AuthUserCollection,
|
|
950
|
+
fields: [...AuthUserCollection.fields, ...allUserFields]
|
|
951
|
+
};
|
|
952
|
+
const authSessionWithFields = {
|
|
953
|
+
...AuthSessionCollection,
|
|
954
|
+
fields: [...AuthSessionCollection.fields, ...allSessionFields]
|
|
955
|
+
};
|
|
956
|
+
const finalAuthCollections = [
|
|
957
|
+
authUserWithFields,
|
|
958
|
+
authSessionWithFields,
|
|
959
|
+
// All base collections except user and session (which we replaced above)
|
|
960
|
+
...BASE_AUTH_COLLECTIONS.filter((c) => c.slug !== "auth-user" && c.slug !== "auth-session"),
|
|
961
|
+
// Sub-plugin collections
|
|
962
|
+
...allCollections
|
|
963
|
+
];
|
|
964
|
+
const showInAdmin = config.admin?.showCollections ?? true;
|
|
965
|
+
if (!showInAdmin) {
|
|
966
|
+
for (const c of finalAuthCollections) {
|
|
967
|
+
c.admin = { ...c.admin, hidden: true };
|
|
968
|
+
}
|
|
969
|
+
}
|
|
970
|
+
return {
|
|
971
|
+
name: "momentum-auth",
|
|
972
|
+
// Static collections for admin UI route data (read at config time)
|
|
973
|
+
collections: finalAuthCollections,
|
|
974
|
+
getAuth() {
|
|
975
|
+
if (!authInstance) {
|
|
976
|
+
throw new Error("Auth not initialized. Call onInit first (via initializeMomentum).");
|
|
977
|
+
}
|
|
978
|
+
return authInstance;
|
|
979
|
+
},
|
|
980
|
+
tryGetAuth() {
|
|
981
|
+
return authInstance;
|
|
982
|
+
},
|
|
983
|
+
getPluginConfig() {
|
|
984
|
+
return {
|
|
985
|
+
db: config.db,
|
|
986
|
+
socialProviders: config.socialProviders
|
|
987
|
+
};
|
|
988
|
+
},
|
|
989
|
+
async onInit(context) {
|
|
990
|
+
const { logger } = context;
|
|
991
|
+
context.collections.push(...finalAuthCollections);
|
|
992
|
+
logger.info(`Injected ${finalAuthCollections.length} auth collections`);
|
|
993
|
+
authInstance = createMomentumAuth({
|
|
994
|
+
db: config.db,
|
|
995
|
+
baseURL: config.baseURL,
|
|
996
|
+
secret: config.secret,
|
|
997
|
+
trustedOrigins: config.trustedOrigins,
|
|
998
|
+
email: config.email,
|
|
999
|
+
socialProviders: config.socialProviders,
|
|
1000
|
+
plugins: allBetterAuthPlugins,
|
|
1001
|
+
userFields: allUserFields
|
|
1002
|
+
});
|
|
1003
|
+
logger.info("Better Auth instance created");
|
|
1004
|
+
}
|
|
1005
|
+
};
|
|
1006
|
+
}
|
|
1007
|
+
|
|
1008
|
+
// libs/auth/src/lib/plugins/two-factor.ts
|
|
1009
|
+
import { twoFactor as twoFactor2 } from "better-auth/plugins";
|
|
1010
|
+
var AuthTwoFactorCollection = defineCollection({
|
|
1011
|
+
slug: "auth-two-factor",
|
|
1012
|
+
dbName: "twoFactor",
|
|
1013
|
+
managed: true,
|
|
1014
|
+
timestamps: false,
|
|
1015
|
+
fields: [
|
|
1016
|
+
text("secret", { required: true }),
|
|
1017
|
+
text("backupCodes", { required: true }),
|
|
1018
|
+
text("userId", { required: true })
|
|
1019
|
+
],
|
|
1020
|
+
indexes: [{ columns: ["secret"] }, { columns: ["userId"] }],
|
|
1021
|
+
admin: {
|
|
1022
|
+
group: "Authentication",
|
|
1023
|
+
hidden: true,
|
|
1024
|
+
description: "Two-factor authentication secrets"
|
|
1025
|
+
},
|
|
1026
|
+
access: {
|
|
1027
|
+
read: () => false,
|
|
1028
|
+
create: () => false,
|
|
1029
|
+
update: () => false,
|
|
1030
|
+
delete: () => false
|
|
1031
|
+
}
|
|
1032
|
+
});
|
|
1033
|
+
function authTwoFactor() {
|
|
1034
|
+
return {
|
|
1035
|
+
name: "two-factor",
|
|
1036
|
+
betterAuthPlugin: twoFactor2(),
|
|
1037
|
+
collections: [AuthTwoFactorCollection],
|
|
1038
|
+
userFields: [checkbox("twoFactorEnabled")]
|
|
1039
|
+
};
|
|
1040
|
+
}
|
|
1041
|
+
|
|
1042
|
+
// libs/auth/src/lib/plugins/admin.ts
|
|
1043
|
+
function authAdmin() {
|
|
1044
|
+
return {
|
|
1045
|
+
name: "admin",
|
|
1046
|
+
// Stub: Better Auth admin plugin will be added here
|
|
1047
|
+
betterAuthPlugin: void 0,
|
|
1048
|
+
userFields: [checkbox("banned"), text("banReason"), date("banExpires")],
|
|
1049
|
+
sessionFields: [text("impersonatedBy")]
|
|
1050
|
+
};
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1053
|
+
// libs/auth/src/lib/plugins/organization.ts
|
|
1054
|
+
var AuthOrganizationCollection = defineCollection({
|
|
1055
|
+
slug: "auth-organization",
|
|
1056
|
+
dbName: "organization",
|
|
1057
|
+
managed: true,
|
|
1058
|
+
timestamps: true,
|
|
1059
|
+
fields: [
|
|
1060
|
+
text("name", { required: true }),
|
|
1061
|
+
text("slug", { required: true }),
|
|
1062
|
+
text("logo"),
|
|
1063
|
+
text("metadata")
|
|
1064
|
+
],
|
|
1065
|
+
indexes: [{ columns: ["slug"], unique: true }],
|
|
1066
|
+
admin: {
|
|
1067
|
+
group: "Authentication",
|
|
1068
|
+
useAsTitle: "name",
|
|
1069
|
+
description: "Organizations for multi-tenant access"
|
|
1070
|
+
},
|
|
1071
|
+
access: {
|
|
1072
|
+
read: ({ req }) => req.user?.role === "admin",
|
|
1073
|
+
create: ({ req }) => req.user?.role === "admin",
|
|
1074
|
+
update: ({ req }) => req.user?.role === "admin",
|
|
1075
|
+
delete: ({ req }) => req.user?.role === "admin"
|
|
1076
|
+
}
|
|
1077
|
+
});
|
|
1078
|
+
var AuthMemberCollection = defineCollection({
|
|
1079
|
+
slug: "auth-member",
|
|
1080
|
+
dbName: "member",
|
|
1081
|
+
managed: true,
|
|
1082
|
+
timestamps: true,
|
|
1083
|
+
fields: [
|
|
1084
|
+
text("userId", { required: true }),
|
|
1085
|
+
text("organizationId", { required: true }),
|
|
1086
|
+
select("role", {
|
|
1087
|
+
options: [
|
|
1088
|
+
{ label: "Owner", value: "owner" },
|
|
1089
|
+
{ label: "Admin", value: "admin" },
|
|
1090
|
+
{ label: "Member", value: "member" }
|
|
1091
|
+
],
|
|
1092
|
+
defaultValue: "member"
|
|
1093
|
+
})
|
|
1094
|
+
],
|
|
1095
|
+
indexes: [
|
|
1096
|
+
{ columns: ["userId"] },
|
|
1097
|
+
{ columns: ["organizationId"] },
|
|
1098
|
+
{ columns: ["userId", "organizationId"], unique: true }
|
|
1099
|
+
],
|
|
1100
|
+
admin: {
|
|
1101
|
+
group: "Authentication",
|
|
1102
|
+
hidden: true,
|
|
1103
|
+
description: "Organization membership"
|
|
1104
|
+
},
|
|
1105
|
+
access: {
|
|
1106
|
+
read: ({ req }) => req.user?.role === "admin",
|
|
1107
|
+
create: () => false,
|
|
1108
|
+
update: () => false,
|
|
1109
|
+
delete: () => false
|
|
1110
|
+
}
|
|
1111
|
+
});
|
|
1112
|
+
var AuthInvitationCollection = defineCollection({
|
|
1113
|
+
slug: "auth-invitation",
|
|
1114
|
+
dbName: "invitation",
|
|
1115
|
+
managed: true,
|
|
1116
|
+
timestamps: true,
|
|
1117
|
+
fields: [
|
|
1118
|
+
text("email", { required: true }),
|
|
1119
|
+
text("organizationId", { required: true }),
|
|
1120
|
+
text("inviterId", { required: true }),
|
|
1121
|
+
select("role", {
|
|
1122
|
+
options: [
|
|
1123
|
+
{ label: "Admin", value: "admin" },
|
|
1124
|
+
{ label: "Member", value: "member" }
|
|
1125
|
+
],
|
|
1126
|
+
defaultValue: "member"
|
|
1127
|
+
}),
|
|
1128
|
+
select("status", {
|
|
1129
|
+
options: [
|
|
1130
|
+
{ label: "Pending", value: "pending" },
|
|
1131
|
+
{ label: "Accepted", value: "accepted" },
|
|
1132
|
+
{ label: "Rejected", value: "rejected" },
|
|
1133
|
+
{ label: "Cancelled", value: "cancelled" }
|
|
1134
|
+
],
|
|
1135
|
+
defaultValue: "pending"
|
|
1136
|
+
}),
|
|
1137
|
+
date("expiresAt", { required: true })
|
|
1138
|
+
],
|
|
1139
|
+
indexes: [{ columns: ["organizationId"] }, { columns: ["email"] }],
|
|
1140
|
+
admin: {
|
|
1141
|
+
group: "Authentication",
|
|
1142
|
+
hidden: true,
|
|
1143
|
+
description: "Pending organization invitations"
|
|
1144
|
+
},
|
|
1145
|
+
access: {
|
|
1146
|
+
read: ({ req }) => req.user?.role === "admin",
|
|
1147
|
+
create: () => false,
|
|
1148
|
+
update: () => false,
|
|
1149
|
+
delete: () => false
|
|
1150
|
+
}
|
|
1151
|
+
});
|
|
1152
|
+
function authOrganization() {
|
|
1153
|
+
return {
|
|
1154
|
+
name: "organization",
|
|
1155
|
+
// Stub: Better Auth organization plugin will be added here
|
|
1156
|
+
betterAuthPlugin: void 0,
|
|
1157
|
+
collections: [AuthOrganizationCollection, AuthMemberCollection, AuthInvitationCollection]
|
|
1158
|
+
};
|
|
1159
|
+
}
|
|
1160
|
+
export {
|
|
1161
|
+
AuthAccountCollection,
|
|
1162
|
+
AuthApiKeysCollection,
|
|
1163
|
+
AuthSessionCollection,
|
|
1164
|
+
AuthUserCollection,
|
|
1165
|
+
AuthVerificationCollection,
|
|
1166
|
+
BASE_AUTH_COLLECTIONS,
|
|
1167
|
+
authAdmin,
|
|
1168
|
+
authOrganization,
|
|
1169
|
+
authTwoFactor,
|
|
1170
|
+
createEmailService,
|
|
1171
|
+
createMomentumAuth,
|
|
1172
|
+
getEnabledOAuthProviders,
|
|
1173
|
+
getPasswordResetEmail,
|
|
1174
|
+
getVerificationEmail,
|
|
1175
|
+
momentumAuth
|
|
1176
|
+
};
|
package/package.json
CHANGED
|
@@ -1,37 +1,37 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
}
|
|
2
|
+
"name": "@momentumcms/auth",
|
|
3
|
+
"version": "0.1.2",
|
|
4
|
+
"description": "Better Auth integration for Momentum CMS",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"author": "Momentum CMS Contributors",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "https://github.com/momentum-cms/momentum-cms.git",
|
|
10
|
+
"directory": "libs/auth"
|
|
11
|
+
},
|
|
12
|
+
"homepage": "https://github.com/momentum-cms/momentum-cms#readme",
|
|
13
|
+
"bugs": {
|
|
14
|
+
"url": "https://github.com/momentum-cms/momentum-cms/issues"
|
|
15
|
+
},
|
|
16
|
+
"keywords": [
|
|
17
|
+
"cms",
|
|
18
|
+
"momentum-cms",
|
|
19
|
+
"authentication",
|
|
20
|
+
"better-auth",
|
|
21
|
+
"auth"
|
|
22
|
+
],
|
|
23
|
+
"engines": {
|
|
24
|
+
"node": ">=18"
|
|
25
|
+
},
|
|
26
|
+
"main": "./index.cjs",
|
|
27
|
+
"types": "./src/index.d.ts",
|
|
28
|
+
"peerDependencies": {
|
|
29
|
+
"@momentumcms/core": ">=0.0.1",
|
|
30
|
+
"@momentumcms/logger": ">=0.0.1",
|
|
31
|
+
"better-auth": "^1.4.0",
|
|
32
|
+
"better-sqlite3": "^12.0.0",
|
|
33
|
+
"nodemailer": "^8.0.0",
|
|
34
|
+
"pg": "^8.0.0"
|
|
35
|
+
},
|
|
36
|
+
"module": "./index.js"
|
|
37
|
+
}
|