@yunsoft/yuncms-core 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.
@@ -3,6 +3,11 @@ import { randomUUID } from 'node:crypto';
3
3
  import { assertPermissionValidationRule } from '../permission-validation.js';
4
4
  import { compileFilter } from '../query.js';
5
5
  import { SchemaCache } from '../schema.js';
6
+ import {
7
+ assertActionOnlyPermissionPayload,
8
+ assertSystemResourceAction,
9
+ isPermissionManagedSystemResource,
10
+ } from '../system-permissions.js';
6
11
  import { BaseService } from './base-service.js';
7
12
 
8
13
  const ACTIONS = new Set(['create', 'read', 'update', 'delete']);
@@ -81,6 +86,17 @@ export class PermissionsService extends BaseService {
81
86
  this.schemaCache = options.schemaCache ?? defaultSchemaCache;
82
87
  }
83
88
 
89
+ async #collectionSchema(collection) {
90
+ const snapshot = this.schema ?? await this.schemaCache.get(this.database);
91
+ const collectionSchema = snapshot.collections?.[collection];
92
+ if (!collectionSchema) {
93
+ const error = new Error(`Unknown permission collection: ${collection}`);
94
+ error.code = 'COLLECTION_NOT_FOUND';
95
+ throw error;
96
+ }
97
+ return collectionSchema;
98
+ }
99
+
84
100
  async resolve(action, collection) {
85
101
  assertAction(action);
86
102
  const key = cacheKey(this.accountability, action, collection);
@@ -102,6 +118,19 @@ export class PermissionsService extends BaseService {
102
118
  throw forbidden(`No role is available for ${action} access to ${collection}`);
103
119
  }
104
120
 
121
+ const collectionSchema = await this.#collectionSchema(collection);
122
+ if (collectionSchema.system) {
123
+ if (!isPermissionManagedSystemResource(collectionSchema)) {
124
+ throw forbidden(`System resource is not delegatable: ${collection}`);
125
+ }
126
+ try {
127
+ assertSystemResourceAction(collectionSchema, action);
128
+ } catch (error) {
129
+ if (error.code === 'SYSTEM_PERMISSION_ACTION_PROTECTED') throw forbidden(error.message);
130
+ throw error;
131
+ }
132
+ }
133
+
105
134
  const [rows] = await this.database.query(
106
135
  `SELECT id, role, collection, action, fields, filter, validation
107
136
  FROM yuncms_permissions
@@ -154,17 +183,6 @@ export class PermissionsService extends BaseService {
154
183
  return normalizePermissionRow(rows[0]);
155
184
  }
156
185
 
157
- async #collectionSchema(collection) {
158
- const snapshot = this.schema ?? await this.schemaCache.get(this.database);
159
- const collectionSchema = snapshot.collections?.[collection];
160
- if (!collectionSchema) {
161
- const error = new Error(`Unknown permission collection: ${collection}`);
162
- error.code = 'COLLECTION_NOT_FOUND';
163
- throw error;
164
- }
165
- return collectionSchema;
166
- }
167
-
168
186
  async createOne(input = {}) {
169
187
  assertPermissionManager(this.accountability);
170
188
  const action = assertAction(input.action);
@@ -180,6 +198,16 @@ export class PermissionsService extends BaseService {
180
198
  }
181
199
 
182
200
  const collectionSchema = await this.#collectionSchema(input.collection);
201
+ if (collectionSchema.system) {
202
+ if (!isPermissionManagedSystemResource(collectionSchema)) {
203
+ const error = new Error(`System resource permissions are protected: ${input.collection}`);
204
+ error.code = 'SYSTEM_PERMISSION_RESOURCE_PROTECTED';
205
+ throw error;
206
+ }
207
+ assertSystemResourceAction(collectionSchema, action);
208
+ assertActionOnlyPermissionPayload(collectionSchema, input);
209
+ }
210
+
183
211
  const fields = normalizePermissionFields(input.fields ?? null, collectionSchema);
184
212
  const filter = input.filter ?? null;
185
213
  const validation = input.validation ?? null;
@@ -187,14 +215,20 @@ export class PermissionsService extends BaseService {
187
215
  assertPermissionValidationRule(validation, collectionSchema);
188
216
 
189
217
  const [roleRows] = await this.database.query(
190
- 'SELECT id FROM yuncms_roles WHERE id = ? LIMIT 1',
218
+ 'SELECT id, admin, public FROM yuncms_roles WHERE id = ? LIMIT 1',
191
219
  [input.role],
192
220
  );
193
- if (!roleRows[0]) {
221
+ const role = roleRows[0];
222
+ if (!role) {
194
223
  const error = new Error(`Unknown permission role: ${input.role}`);
195
224
  error.code = 'ROLE_NOT_FOUND';
196
225
  throw error;
197
226
  }
227
+ if (collectionSchema.system && role.public) {
228
+ const error = new Error('Public role cannot be granted access to protected system resources');
229
+ error.code = 'PUBLIC_SYSTEM_ACCESS_FORBIDDEN';
230
+ throw error;
231
+ }
198
232
 
199
233
  const id = randomUUID();
200
234
  await this.database.query(
@@ -235,6 +269,7 @@ export class PermissionsService extends BaseService {
235
269
  throw error;
236
270
  }
237
271
  const collectionSchema = await this.#collectionSchema(existing.collection);
272
+ if (collectionSchema.system) assertActionOnlyPermissionPayload(collectionSchema, patch);
238
273
 
239
274
  const assignments = [];
240
275
  const params = [];
@@ -1,6 +1,7 @@
1
1
  import { randomUUID } from 'node:crypto';
2
2
 
3
3
  import { BaseService } from './base-service.js';
4
+ import { resolveSystemResourceAccess } from './system-resource-access.js';
4
5
 
5
6
  function assertRoleManager(accountability) {
6
7
  if (accountability.admin === true || accountability.system === true) return;
@@ -26,7 +27,7 @@ function normalizeRoleName(name) {
26
27
 
27
28
  export class RolesService extends BaseService {
28
29
  async readMany() {
29
- assertRoleManager(this.accountability);
30
+ await resolveSystemResourceAccess(this, 'read', 'yuncms_roles');
30
31
  const [rows] = await this.database.query(
31
32
  `SELECT id, name, description, admin, public, created_at, updated_at
32
33
  FROM yuncms_roles
@@ -36,7 +37,7 @@ export class RolesService extends BaseService {
36
37
  }
37
38
 
38
39
  async readOne(id) {
39
- assertRoleManager(this.accountability);
40
+ await resolveSystemResourceAccess(this, 'read', 'yuncms_roles');
40
41
  const [rows] = await this.database.query(
41
42
  `SELECT id, name, description, admin, public, created_at, updated_at
42
43
  FROM yuncms_roles
@@ -157,4 +158,4 @@ export class RolesService extends BaseService {
157
158
  }
158
159
  return true;
159
160
  }
160
- }
161
+ }
@@ -20,6 +20,7 @@ function identityFromRow(row) {
20
20
  return {
21
21
  user: row.user,
22
22
  role: row.role ?? null,
23
+ role_name: row.role_name ?? null,
23
24
  admin: Boolean(row.role_admin),
24
25
  email: row.email,
25
26
  session: row.session_id,
@@ -71,7 +72,7 @@ export class SessionsService extends BaseService {
71
72
  const hash = hashToken(token);
72
73
  const [rows] = await this.database.query(
73
74
  `SELECT s.id AS session_id, s.user, u.email, u.role, u.status,
74
- r.admin AS role_admin
75
+ r.name AS role_name, r.admin AS role_admin
75
76
  FROM yuncms_sessions s
76
77
  INNER JOIN yuncms_users u ON u.id = s.user
77
78
  LEFT JOIN yuncms_roles r ON r.id = u.role
@@ -105,7 +106,7 @@ export class SessionsService extends BaseService {
105
106
  const oldHash = hashToken(token);
106
107
  const [rows] = await this.database.query(
107
108
  `SELECT s.id AS session_id, s.user, u.email, u.role, u.status,
108
- r.admin AS role_admin
109
+ r.name AS role_name, r.admin AS role_admin
109
110
  FROM yuncms_sessions s
110
111
  INNER JOIN yuncms_users u ON u.id = s.user
111
112
  LEFT JOIN yuncms_roles r ON r.id = u.role
@@ -164,7 +165,6 @@ export class SessionsService extends BaseService {
164
165
  error.code = 'FORBIDDEN';
165
166
  throw error;
166
167
  }
167
-
168
168
  const [result] = await this.database.query(
169
169
  'DELETE FROM yuncms_sessions WHERE user = ?',
170
170
  [userId],
@@ -0,0 +1,140 @@
1
+ import { BaseService } from './base-service.js';
2
+
3
+ const THEMES = new Set(['system', 'light', 'dark']);
4
+ const LOCALES = new Set(['en', 'tr']);
5
+ const ACCENT_PATTERN = /^#[0-9a-f]{6}$/i;
6
+
7
+ function invalid(message) {
8
+ const error = new Error(message);
9
+ error.code = 'INVALID_PAYLOAD';
10
+ return error;
11
+ }
12
+
13
+ function assertManager(accountability) {
14
+ if (accountability.admin === true || accountability.system === true) return;
15
+ const error = new Error('Studio settings require administrator accountability');
16
+ error.code = 'FORBIDDEN';
17
+ throw error;
18
+ }
19
+
20
+ function normalizeBrandName(value) {
21
+ if (typeof value !== 'string' || !value.trim()) throw invalid('Brand name is required');
22
+ const normalized = value.trim();
23
+ if (normalized.length > 100) throw invalid('Brand name cannot exceed 100 characters');
24
+ return normalized;
25
+ }
26
+
27
+ function normalizeLogoUrl(value) {
28
+ if (typeof value !== 'string' || !value.trim()) throw invalid('Logo URL is required');
29
+ const normalized = value.trim();
30
+ if (normalized.length > 512) throw invalid('Logo URL cannot exceed 512 characters');
31
+ let url;
32
+ try {
33
+ url = new URL(normalized);
34
+ } catch {
35
+ throw invalid('Logo URL must be a valid URL');
36
+ }
37
+ if (!['http:', 'https:'].includes(url.protocol)) throw invalid('Logo URL must use HTTP or HTTPS');
38
+ return normalized;
39
+ }
40
+
41
+ function normalizeAccent(value) {
42
+ const normalized = String(value ?? '').trim();
43
+ if (!ACCENT_PATTERN.test(normalized)) throw invalid('Accent color must be a six-digit hex color');
44
+ return normalized.toLowerCase();
45
+ }
46
+
47
+ function normalizeTheme(value) {
48
+ const normalized = String(value ?? '').trim().toLowerCase();
49
+ if (!THEMES.has(normalized)) throw invalid('Theme must be system, light or dark');
50
+ return normalized;
51
+ }
52
+
53
+ function normalizeLocale(value) {
54
+ const normalized = String(value ?? '').trim().toLowerCase();
55
+ if (!LOCALES.has(normalized)) throw invalid('Default locale must be en or tr');
56
+ return normalized;
57
+ }
58
+
59
+ function publicSettings(row) {
60
+ return {
61
+ brand_name: row.brand_name,
62
+ logo_url: row.logo_url,
63
+ accent_color: row.accent_color,
64
+ theme: row.theme,
65
+ default_locale: row.default_locale,
66
+ updated_at: row.updated_at ?? null,
67
+ };
68
+ }
69
+
70
+ export class StudioSettingsService extends BaseService {
71
+ async readPublic() {
72
+ const [rows] = await this.database.query(
73
+ `SELECT brand_name, logo_url, accent_color, theme, default_locale, updated_at
74
+ FROM yuncms_studio_settings
75
+ WHERE id = 1
76
+ LIMIT 1`,
77
+ );
78
+ const row = rows[0];
79
+ if (!row) {
80
+ const error = new Error('Studio settings are missing; run YunCMS bootstrap');
81
+ error.code = 'DATABASE_MIGRATION_REQUIRED';
82
+ throw error;
83
+ }
84
+ return publicSettings(row);
85
+ }
86
+
87
+ async readOne() {
88
+ assertManager(this.accountability);
89
+ return this.readPublic();
90
+ }
91
+
92
+ async updateOne(patch = {}) {
93
+ assertManager(this.accountability);
94
+ if (!patch || typeof patch !== 'object' || Array.isArray(patch)) throw invalid('Studio settings patch must be an object');
95
+
96
+ const keys = Object.keys(patch);
97
+ const allowed = new Set(['brand_name', 'logo_url', 'accent_color', 'theme', 'default_locale']);
98
+ if (keys.length === 0 || keys.some((key) => !allowed.has(key))) {
99
+ throw invalid('Studio settings patch contains unsupported properties');
100
+ }
101
+
102
+ const assignments = [];
103
+ const params = [];
104
+ if (Object.hasOwn(patch, 'brand_name')) {
105
+ assignments.push('brand_name = ?');
106
+ params.push(normalizeBrandName(patch.brand_name));
107
+ }
108
+ if (Object.hasOwn(patch, 'logo_url')) {
109
+ assignments.push('logo_url = ?');
110
+ params.push(normalizeLogoUrl(patch.logo_url));
111
+ }
112
+ if (Object.hasOwn(patch, 'accent_color')) {
113
+ assignments.push('accent_color = ?');
114
+ params.push(normalizeAccent(patch.accent_color));
115
+ }
116
+ if (Object.hasOwn(patch, 'theme')) {
117
+ assignments.push('theme = ?');
118
+ params.push(normalizeTheme(patch.theme));
119
+ }
120
+ if (Object.hasOwn(patch, 'default_locale')) {
121
+ assignments.push('default_locale = ?');
122
+ params.push(normalizeLocale(patch.default_locale));
123
+ }
124
+
125
+ params.push(1);
126
+ await this.database.query(
127
+ `UPDATE yuncms_studio_settings SET ${assignments.join(', ')} WHERE id = ?`,
128
+ params,
129
+ );
130
+ return this.readPublic();
131
+ }
132
+ }
133
+
134
+ export const STUDIO_SETTING_DEFAULTS = Object.freeze({
135
+ brand_name: 'YunCMS',
136
+ logo_url: 'https://yunsoft.com/light-logo.png',
137
+ accent_color: '#2563eb',
138
+ theme: 'system',
139
+ default_locale: 'en',
140
+ });
@@ -0,0 +1,23 @@
1
+ import { PermissionsService } from './permissions-service.js';
2
+
3
+ export async function resolveSystemResourceAccess(service, action, collection) {
4
+ if (service.accountability.admin === true || service.accountability.system === true) {
5
+ return {
6
+ fullAccess: true,
7
+ action,
8
+ collection,
9
+ role: service.accountability.role ?? null,
10
+ };
11
+ }
12
+
13
+ const permissions = new PermissionsService({
14
+ accountability: service.accountability,
15
+ database: service.database,
16
+ schema: service.schema,
17
+ emitter: service.emitter,
18
+ logger: service.logger,
19
+ permissionCache: service.permissionCache,
20
+ requestId: service.requestId,
21
+ });
22
+ return permissions.resolve(action, collection);
23
+ }
@@ -3,6 +3,7 @@ import { randomUUID } from 'node:crypto';
3
3
  import { hashPassword } from '../auth/password.js';
4
4
  import { withTransaction } from '../transaction.js';
5
5
  import { BaseService } from './base-service.js';
6
+ import { resolveSystemResourceAccess } from './system-resource-access.js';
6
7
 
7
8
  const USER_STATUSES = new Set(['active', 'suspended', 'disabled']);
8
9
  const USER_UPDATE_KEYS = new Set(['email', 'role', 'status']);
@@ -22,11 +23,10 @@ function normalizeEmail(email) {
22
23
  return normalized;
23
24
  }
24
25
 
25
- function assertUserManager(accountability) {
26
- if (accountability.admin === true || accountability.system === true) return;
27
- const error = new Error('User management requires administrator accountability');
26
+ function forbidden(message) {
27
+ const error = new Error(message);
28
28
  error.code = 'FORBIDDEN';
29
- throw error;
29
+ return error;
30
30
  }
31
31
 
32
32
  function assertStatus(status) {
@@ -38,19 +38,65 @@ function assertStatus(status) {
38
38
  return status;
39
39
  }
40
40
 
41
- async function assertRoleExists(database, role) {
41
+ function assertCredentialManager(accountability) {
42
+ if (accountability.admin === true || accountability.system === true) return;
43
+ throw forbidden('Changing another user password requires administrator accountability');
44
+ }
45
+
46
+ async function assertRoleAssignable(database, role, accountability) {
42
47
  if (role == null) return;
43
- const [roleRows] = await database.query('SELECT id FROM yuncms_roles WHERE id = ? LIMIT 1', [role]);
44
- if (!roleRows[0]) {
48
+ const [roleRows] = await database.query(
49
+ 'SELECT id, admin, public FROM yuncms_roles WHERE id = ? LIMIT 1',
50
+ [role],
51
+ );
52
+ const targetRole = roleRows[0];
53
+ if (!targetRole) {
45
54
  const error = new Error(`Unknown role: ${role}`);
46
55
  error.code = 'ROLE_NOT_FOUND';
47
56
  throw error;
48
57
  }
58
+ if (targetRole.public) {
59
+ const error = new Error('The public role cannot be assigned to an authenticated user');
60
+ error.code = 'INVALID_ROLE';
61
+ throw error;
62
+ }
63
+ if (targetRole.admin && accountability.admin !== true && accountability.system !== true) {
64
+ throw forbidden('Only an administrator can assign the administrator role');
65
+ }
66
+ }
67
+
68
+ async function assertTargetManageable(database, id, accountability) {
69
+ if (accountability.admin === true || accountability.system === true) return;
70
+ const [rows] = await database.query(
71
+ `SELECT u.id, r.admin AS role_admin
72
+ FROM yuncms_users u
73
+ LEFT JOIN yuncms_roles r ON r.id = u.role
74
+ WHERE u.id = ?
75
+ LIMIT 1`,
76
+ [id],
77
+ );
78
+ if (!rows[0]) {
79
+ const error = new Error(`Unknown user: ${id}`);
80
+ error.code = 'USER_NOT_FOUND';
81
+ throw error;
82
+ }
83
+ if (rows[0].role_admin) throw forbidden('Delegated user managers cannot modify administrator accounts');
49
84
  }
50
85
 
51
86
  export class UsersService extends BaseService {
87
+ async #readOneUnsafe(id) {
88
+ const [rows] = await this.database.query(
89
+ `SELECT id, email, role, status, email_verified_at, last_access, created_at, updated_at
90
+ FROM yuncms_users
91
+ WHERE id = ?
92
+ LIMIT 1`,
93
+ [id],
94
+ );
95
+ return rows[0] ?? null;
96
+ }
97
+
52
98
  async readMany() {
53
- assertUserManager(this.accountability);
99
+ await resolveSystemResourceAccess(this, 'read', 'yuncms_users');
54
100
  const [rows] = await this.database.query(
55
101
  `SELECT id, email, role, status, email_verified_at, last_access, created_at, updated_at
56
102
  FROM yuncms_users
@@ -61,26 +107,19 @@ export class UsersService extends BaseService {
61
107
 
62
108
  async readOne(id) {
63
109
  const self = this.accountability.user === id;
64
- if (!self) assertUserManager(this.accountability);
65
-
66
- const [rows] = await this.database.query(
67
- `SELECT id, email, role, status, email_verified_at, last_access, created_at, updated_at
68
- FROM yuncms_users
69
- WHERE id = ?
70
- LIMIT 1`,
71
- [id],
72
- );
73
- return rows[0] ?? null;
110
+ if (!self) await resolveSystemResourceAccess(this, 'read', 'yuncms_users');
111
+ return this.#readOneUnsafe(id);
74
112
  }
75
113
 
76
114
  async createOne(input = {}) {
77
- assertUserManager(this.accountability);
115
+ await resolveSystemResourceAccess(this, 'create', 'yuncms_users');
78
116
  const email = normalizeEmail(input.email);
79
117
  const status = assertStatus(input.status ?? 'active');
80
- await assertRoleExists(this.database, input.role ?? null);
118
+ await assertRoleAssignable(this.database, input.role ?? null, this.accountability);
81
119
 
82
120
  const passwordHash = await hashPassword(input.password);
83
121
  const id = randomUUID();
122
+ const verifiedAt = new Date();
84
123
  await this.database.query(
85
124
  `INSERT INTO yuncms_users (id, email, password_hash, role, status, email_verified_at)
86
125
  VALUES (?, ?, ?, ?, ?, ?)`,
@@ -90,15 +129,16 @@ export class UsersService extends BaseService {
90
129
  passwordHash,
91
130
  input.role ?? null,
92
131
  status,
93
- input.emailVerified === true ? new Date() : null,
132
+ verifiedAt,
94
133
  ],
95
134
  );
96
135
 
97
- return this.readOne(id);
136
+ return this.#readOneUnsafe(id);
98
137
  }
99
138
 
100
139
  async updateOne(id, patch = {}) {
101
- assertUserManager(this.accountability);
140
+ await resolveSystemResourceAccess(this, 'update', 'yuncms_users');
141
+ await assertTargetManageable(this.database, id, this.accountability);
102
142
  if (!patch || typeof patch !== 'object' || Array.isArray(patch)) {
103
143
  const error = new Error('User patch must be an object');
104
144
  error.code = 'INVALID_PAYLOAD';
@@ -114,12 +154,14 @@ export class UsersService extends BaseService {
114
154
  if (Object.hasOwn(patch, 'status')) {
115
155
  assertStatus(patch.status);
116
156
  if (this.accountability.user === id && patch.status !== 'active') {
117
- const error = new Error('An administrator cannot suspend or disable their own active session');
157
+ const error = new Error('A user cannot suspend or disable their own active session');
118
158
  error.code = 'SELF_ADMIN_MUTATION_FORBIDDEN';
119
159
  throw error;
120
160
  }
121
161
  }
122
- if (Object.hasOwn(patch, 'role')) await assertRoleExists(this.database, patch.role);
162
+ if (Object.hasOwn(patch, 'role')) {
163
+ await assertRoleAssignable(this.database, patch.role, this.accountability);
164
+ }
123
165
 
124
166
  return withTransaction(this.database, async (connection) => {
125
167
  const assignments = [];
@@ -163,9 +205,10 @@ export class UsersService extends BaseService {
163
205
  }
164
206
 
165
207
  async deleteOne(id) {
166
- assertUserManager(this.accountability);
208
+ await resolveSystemResourceAccess(this, 'delete', 'yuncms_users');
209
+ await assertTargetManageable(this.database, id, this.accountability);
167
210
  if (this.accountability.user === id) {
168
- const error = new Error('An administrator cannot delete their own user account');
211
+ const error = new Error('A user cannot delete their own account from an active session');
169
212
  error.code = 'SELF_ADMIN_MUTATION_FORBIDDEN';
170
213
  throw error;
171
214
  }
@@ -180,7 +223,7 @@ export class UsersService extends BaseService {
180
223
 
181
224
  async updatePassword(id, password) {
182
225
  const self = this.accountability.user === id;
183
- if (!self) assertUserManager(this.accountability);
226
+ if (!self) assertCredentialManager(this.accountability);
184
227
 
185
228
  const passwordHash = await hashPassword(password);
186
229
  const connection = await this.database.getConnection();
package/src/setup.js CHANGED
@@ -1,8 +1,12 @@
1
+ import { randomUUID } from 'node:crypto';
2
+
1
3
  import { createSystemAccountability } from './accountability.js';
2
4
  import { withTransaction } from './transaction.js';
3
5
  import { RolesService } from './services/roles-service.js';
4
6
  import { UsersService } from './services/users-service.js';
5
7
 
8
+ const PUBLIC_ROLE_NAMES = Object.freeze(['Public', 'Public API', 'Anonymous']);
9
+
6
10
  export async function findExistingAdmin(database) {
7
11
  if (!database) throw new Error('Database handle is required');
8
12
  const [rows] = await database.query(
@@ -16,6 +20,51 @@ export async function findExistingAdmin(database) {
16
20
  return rows[0] ?? null;
17
21
  }
18
22
 
23
+ export async function findPublicRole(database) {
24
+ if (!database) throw new Error('Database handle is required');
25
+ const [rows] = await database.query(
26
+ `SELECT id, name, description, admin, public
27
+ FROM yuncms_roles
28
+ WHERE public = 1
29
+ ORDER BY created_at ASC
30
+ LIMIT 2`,
31
+ );
32
+ if (rows.length > 1) {
33
+ const error = new Error('Multiple public roles are configured');
34
+ error.code = 'PUBLIC_ROLE_AMBIGUOUS';
35
+ throw error;
36
+ }
37
+ return rows[0] ?? null;
38
+ }
39
+
40
+ async function availablePublicRoleName(database) {
41
+ for (const candidate of PUBLIC_ROLE_NAMES) {
42
+ const [rows] = await database.query(
43
+ 'SELECT id FROM yuncms_roles WHERE name = ? LIMIT 1',
44
+ [candidate],
45
+ );
46
+ if (!rows[0]) return candidate;
47
+ }
48
+ return `Public ${randomUUID().slice(0, 8)}`;
49
+ }
50
+
51
+ export async function ensurePublicRole(database) {
52
+ if (!database) throw new Error('Database handle is required');
53
+ const existing = await findPublicRole(database);
54
+ if (existing) return { ...existing, created: false };
55
+
56
+ const roles = new RolesService({
57
+ accountability: createSystemAccountability(),
58
+ database,
59
+ });
60
+ const role = await roles.createOne({
61
+ name: await availablePublicRoleName(database),
62
+ description: 'Unauthenticated public API access. No collection access is granted by default.',
63
+ public: true,
64
+ });
65
+ return { ...role, created: true };
66
+ }
67
+
19
68
  export async function createInitialAdmin(pool, { email, password } = {}) {
20
69
  if (!pool) throw new Error('Database pool is required');
21
70
  const accountability = createSystemAccountability();
@@ -54,7 +103,6 @@ export async function createInitialAdmin(pool, { email, password } = {}) {
54
103
  password,
55
104
  role: roleId,
56
105
  status: 'active',
57
- emailVerified: true,
58
106
  });
59
107
 
60
108
  return {
@@ -63,4 +111,4 @@ export async function createInitialAdmin(pool, { email, password } = {}) {
63
111
  role: roleId,
64
112
  };
65
113
  });
66
- }
114
+ }