@yunsoft/yuncms-api 0.1.11 → 0.1.13

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yunsoft/yuncms-api",
3
- "version": "0.1.11",
3
+ "version": "0.1.13",
4
4
  "description": "Express API runtime and bundled Studio server for YunCMS.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -35,7 +35,7 @@
35
35
  "@modelcontextprotocol/node": "2.0.0",
36
36
  "@modelcontextprotocol/server": "2.0.0",
37
37
  "@node-saml/node-saml": "5.1.0",
38
- "@yunsoft/yuncms-core": "0.1.11",
38
+ "@yunsoft/yuncms-core": "0.1.13",
39
39
  "express": "5.2.1",
40
40
  "ldapts": "9.0.0",
41
41
  "openid-client": "6.8.7",
@@ -5,6 +5,7 @@ const STATUS_BY_CODE = new Map([
5
5
  ['UNAUTHORIZED', 401],
6
6
  ['FORBIDDEN', 403],
7
7
  ['FORBIDDEN_FIELD', 403],
8
+ ['EMAIL_NOT_VERIFIED', 403],
8
9
  ['SELF_ADMIN_MUTATION_FORBIDDEN', 403],
9
10
  ['PROTECTED_ROLE', 403],
10
11
  ['SYSTEM_SCHEMA_READ_ONLY', 403],
@@ -1,3 +1,4 @@
1
+ import { createSystemAccountability } from '@yunsoft/yuncms-core';
1
2
  import express from 'express';
2
3
 
3
4
  import { createFixedWindowRateLimit } from '../rate-limit.js';
@@ -7,10 +8,20 @@ function service(req, name) {
7
8
  const Service = req.context.services[name];
8
9
  return new Service(serviceOptionsFromRequest(req));
9
10
  }
11
+ function systemService(req, name) {
12
+ const Service = req.context.services[name];
13
+ return new Service({
14
+ ...serviceOptionsFromRequest(req),
15
+ accountability: createSystemAccountability(),
16
+ });
17
+ }
10
18
  function authService(req) { return service(req, 'AuthService'); }
11
19
  function authTokensService(req) { return service(req, 'AuthTokensService'); }
20
+ function systemAuthTokensService(req) { return systemService(req, 'AuthTokensService'); }
12
21
  function apiTokensService(req) { return service(req, 'ApiTokensService'); }
13
22
  function usersService(req) { return service(req, 'UsersService'); }
23
+ function systemUsersService(req) { return systemService(req, 'UsersService'); }
24
+ function studioSettingsService(req) { return service(req, 'StudioSettingsService'); }
14
25
  function externalAuthService(req, registry) {
15
26
  const Service = req.context.services.ExternalAuthService;
16
27
  return new Service({
@@ -45,6 +56,14 @@ function actionUrl(config, action, token) {
45
56
  return `${config.auth.publicUrl}/?auth_action=${encodeURIComponent(action)}&token=${encodeURIComponent(token)}`;
46
57
  }
47
58
 
59
+ function verificationMessage(config, token) {
60
+ const url = actionUrl(config, 'verify', token);
61
+ return {
62
+ subject: 'Verify your YunCMS email',
63
+ text: `Verify your YunCMS email address by opening this link:\n${url}\n\nIf you did not request this, you can ignore this message.`,
64
+ };
65
+ }
66
+
48
67
  function noStore(req, res, next) {
49
68
  res.set('cache-control', 'no-store');
50
69
  res.set('pragma', 'no-cache');
@@ -97,6 +116,43 @@ export function createAuthRouter({
97
116
  res.json({ data: externalAuthRegistry?.publicProviders?.() ?? [] });
98
117
  });
99
118
 
119
+ router.post('/register', actionLimit, async (req, res) => {
120
+ const settings = await studioSettingsService(req).readPublic();
121
+ const verificationRequired = settings.public_registration_require_email_verification === true;
122
+ const transport = verificationRequired ? requireMailer(mailer) : null;
123
+ const data = await usersService(req).registerPublic(req.body ?? {});
124
+
125
+ if (verificationRequired) {
126
+ try {
127
+ const result = await systemAuthTokensService(req).createEmailVerification(data.id);
128
+ const message = verificationMessage(config, result.token);
129
+ await transport.send({ to: data.email, ...message }, {
130
+ accountability: req.accountability,
131
+ requestId: req.id,
132
+ });
133
+ } catch (error) {
134
+ try {
135
+ await systemUsersService(req).deleteOne(data.id);
136
+ } catch (cleanupError) {
137
+ logger.error?.('YunCMS public registration cleanup failed', {
138
+ requestId: req.id,
139
+ user: data.id,
140
+ code: cleanupError?.code,
141
+ message: cleanupError?.message,
142
+ });
143
+ }
144
+ throw error;
145
+ }
146
+ }
147
+
148
+ res.status(201).json({
149
+ data: {
150
+ ...data,
151
+ email_verification_required: verificationRequired,
152
+ },
153
+ });
154
+ });
155
+
100
156
  router.post('/login', loginLimit, async (req, res) => {
101
157
  const result = await authService(req).login({ email: req.body?.email, password: req.body?.password, ip: req.ip ?? null, userAgent: req.get('user-agent') ?? null });
102
158
  res.json({ data: result });
@@ -210,12 +266,35 @@ export function createAuthRouter({
210
266
  res.status(204).end();
211
267
  });
212
268
  router.post('/email-verification/request', actionLimit, async (req, res) => {
213
- const transport = requireMailer(mailer);
214
269
  if (!req.accountability?.user) {
215
- const error = new Error('Authentication is required to request email verification');
216
- error.code = 'UNAUTHORIZED';
217
- throw error;
270
+ const settings = await studioSettingsService(req).readPublic();
271
+ if (settings.public_registration_require_email_verification !== true) {
272
+ res.status(202).json({ data: { accepted: true } });
273
+ return;
274
+ }
275
+
276
+ const transport = requireMailer(mailer);
277
+ const result = await systemAuthTokensService(req).requestEmailVerification(req.body?.email);
278
+ if (result) {
279
+ try {
280
+ const message = verificationMessage(config, result.token);
281
+ await transport.send({ to: result.user.email, ...message }, {
282
+ accountability: req.accountability,
283
+ requestId: req.id,
284
+ });
285
+ } catch (error) {
286
+ logger.error?.('YunCMS public email verification mail delivery failed', {
287
+ requestId: req.id,
288
+ code: error?.code,
289
+ message: error?.message,
290
+ });
291
+ }
292
+ }
293
+ res.status(202).json({ data: { accepted: true } });
294
+ return;
218
295
  }
296
+
297
+ const transport = requireMailer(mailer);
219
298
  const userId = req.body?.user ?? req.accountability.user;
220
299
  const user = await usersService(req).readOne(userId);
221
300
  if (!user) {
@@ -224,8 +303,11 @@ export function createAuthRouter({
224
303
  throw error;
225
304
  }
226
305
  const result = await authTokensService(req).createEmailVerification(userId);
227
- const url = actionUrl(config, 'verify', result.token);
228
- await transport.send({ to: user.email, subject: 'Verify your YunCMS email', text: `Verify your YunCMS email address by opening this link:\n${url}\n\nIf you did not request this, you can ignore this message.` }, { accountability: req.accountability, requestId: req.id });
306
+ const message = verificationMessage(config, result.token);
307
+ await transport.send({ to: user.email, ...message }, {
308
+ accountability: req.accountability,
309
+ requestId: req.id,
310
+ });
229
311
  res.status(202).json({ data: { accepted: true } });
230
312
  });
231
313
  router.post('/email-verification/confirm', actionLimit, async (req, res) => {
@@ -26,6 +26,10 @@ export function createStudioSettingsRouter() {
26
26
  sendImageAsset(res, await settingsService(req).readFaviconContent());
27
27
  });
28
28
 
29
+ router.get('/admin', async (req, res) => {
30
+ res.json({ data: await settingsService(req).readOne() });
31
+ });
32
+
29
33
  router.get('/', async (req, res) => {
30
34
  res.json({ data: await settingsService(req).readPublic() });
31
35
  });