pgkiln 0.57.0 → 0.58.0
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/docs/apex-feature-parity.md +1 -1
- package/docs/guide/03-builder.md +6 -3
- package/docs/guide/12-development.md +2 -1
- package/package.json +1 -1
- package/src/builder/i18n/de.ts +340 -0
- package/src/builder/i18n/en.ts +340 -0
- package/src/builder/i18n/es.ts +340 -0
- package/src/builder/i18n/fr.ts +340 -0
- package/src/builder/i18n/nl.ts +340 -0
- package/src/builder/i18n/pt.ts +340 -0
- package/src/builder/routes.ts +88 -87
- package/src/builder/shared.ts +86 -77
package/src/builder/routes.ts
CHANGED
|
@@ -408,12 +408,13 @@ export async function builderRoutes(app: FastifyInstance) {
|
|
|
408
408
|
app.get(`${BASE}/apps/:id/settings`, async (req: Req, reply) => {
|
|
409
409
|
const s = await developer(req, reply);
|
|
410
410
|
if (!s) return;
|
|
411
|
+
const tx = tr(s);
|
|
411
412
|
const a = await appOr404(req.params.id);
|
|
412
413
|
if (!a) return reply.code(404).send('Not found');
|
|
413
414
|
const providers = (await owner.query('select name, display_name, enabled from meta.auth_provider order by display_name')).rows;
|
|
414
415
|
const directories = (await owner.query('select name, display_name, enabled from meta.ldap_directory order by display_name')).rows;
|
|
415
416
|
const lists = (await owner.query('select name from meta.list where app_id = $1 order by name', [a.id])).rows.map((r) => r.name as string);
|
|
416
|
-
const listChoices = (v: string | null, none: string): [string, string][] => [['', none], ...lists.map((n): [string, string] => [n, n]), ...(v && !lists.includes(v) ? [[v,
|
|
417
|
+
const listChoices = (v: string | null, none: string): [string, string][] => [['', none], ...lists.map((n): [string, string] => [n, n]), ...(v && !lists.includes(v) ? [[v, tx('st.missing', { name: v })] as [string, string]] : [])];
|
|
417
418
|
// (sprint 74) tabs like APEX's Edit Application Definition and Security Attributes: every tab of the
|
|
418
419
|
// main form is in the page (the others hidden), so Save keeps all of them; without JavaScript the tabs are links
|
|
419
420
|
const tab = SETTINGS_TABS.some(([k]) => k === req.query?.tab) ? String(req.query!.tab) : 'definition';
|
|
@@ -421,123 +422,123 @@ export async function builderRoutes(app: FastifyInstance) {
|
|
|
421
422
|
const end = raw('</section>');
|
|
422
423
|
const authPart = (k: string) => raw(`<div class="auth-part" data-auth="${k}"${a.authentication === k ? '' : ' hidden'}>`);
|
|
423
424
|
const main = html`${appHeader(a, 'settings', s)}
|
|
424
|
-
<nav class="settings-tabs" aria-label="
|
|
425
|
+
<nav class="settings-tabs" aria-label="${tx('st.settings')}">${SETTINGS_TABS.map(([k, label]) => html`<a href="?tab=${k}" data-tab-link="${k}"${k === tab ? raw(' aria-current="page"') : ''}>${tx.dyn(`st.tab.${k}`, label)}</a>`)}</nav>
|
|
425
426
|
<div class="settings-body">
|
|
426
427
|
<form method="post" action="${BASE}/apps/${a.id}/settings" class="settings-form">${csrf(s)}<input type="hidden" name="tab" value="${tab}" data-settings-tab>
|
|
427
|
-
${panel('definition')}${region('
|
|
428
|
+
${panel('definition')}${region(tx('st.tab.definition'), html`
|
|
428
429
|
<div class="form-grid">
|
|
429
|
-
${input('name', '
|
|
430
|
-
${input('alias', '
|
|
431
|
-
${input('version', '
|
|
432
|
-
${input('home_page', '
|
|
433
|
-
${select('app_type', '
|
|
434
|
-
'
|
|
430
|
+
${input('name', tx('st.name'), a.name, { required: true })}
|
|
431
|
+
${input('alias', tx('st.alias'), a.alias, { required: true })}
|
|
432
|
+
${input('version', tx('st.version'), a.version ?? '', { placeholder: tx('st.version_ph'), help: tx('st.version_help') })}
|
|
433
|
+
${input('home_page', tx('st.home_page'), a.home_page, { type: 'number' })}
|
|
434
|
+
${select('app_type', tx('st.app_type'), a.app_type ?? 'standard', APP_TYPES.map((t): [string, string] => [t, tx.dyn(`st.type.${t}`, APP_TYPE_LABELS[t])]),
|
|
435
|
+
tx('st.app_type_help'))}
|
|
435
436
|
</div>`)}${end}
|
|
436
|
-
${panel('security')}${region('
|
|
437
|
+
${panel('security')}${region(tx('st.authentication'), html`
|
|
437
438
|
<div class="form-grid">
|
|
438
|
-
${select('authentication', '
|
|
439
|
-
${input('db_role', '
|
|
440
|
-
<div class="field"><span class="label" aria-hidden="true"></span><label class="check"><input type="checkbox" name="debug" value="true"${a.debug ? raw(' checked') : ''}>
|
|
441
|
-
<small class="help"
|
|
442
|
-
<div class="field"><span class="label"
|
|
443
|
-
<small class="help"
|
|
439
|
+
${select('authentication', tx('st.authentication'), a.authentication, (['app_users', 'header', 'database', 'custom', 'none'] as const).map((k): [string, string] => [k, tx(`st.auth.${k}`)]))}
|
|
440
|
+
${input('db_role', tx('st.db_role'), a.db_role, { help: tx('st.db_role_help') })}
|
|
441
|
+
<div class="field"><span class="label" aria-hidden="true"></span><label class="check"><input type="checkbox" name="debug" value="true"${a.debug ? raw(' checked') : ''}> ${tx('st.debug')}</label>
|
|
442
|
+
<small class="help">${tx('st.debug_help')}</small></div>
|
|
443
|
+
<div class="field"><span class="label">${tx('st.debug_msgs')}</span><span>${a.debug_level ? html`<b>${tx('st.debug_on', { level: a.debug_level })}</b>` : tx('st.off')} · <a href="${BASE}/apps/${a.id}/debug">${tx('st.debug_msgs')}</a></span>
|
|
444
|
+
<small class="help">${tx.rich('st.debug_msgs_help', { fn: html`<code>meta.debug(level, text)</code>`, days: a.debug_retention_days })}</small></div>
|
|
444
445
|
</div>
|
|
445
|
-
${authPart('header')}<h3
|
|
446
|
-
<p class="muted"
|
|
446
|
+
${authPart('header')}<h3>${tx('st.header_title')}</h3>
|
|
447
|
+
<p class="muted">${tx.rich('st.header_intro', { env: html`<code>PGKILN_AUTH_HEADER_PROXIES</code>` })}${headerProxiesConfigured() ? '' : html` (<b>${tx('st.header_unset')}</b>)`}.</p>
|
|
447
448
|
<div class="form-grid">
|
|
448
|
-
${input('header_name', '
|
|
449
|
-
${input('logout_url', '
|
|
449
|
+
${input('header_name', tx('st.header_name'), a.header_name ?? '', { placeholder: DEFAULT_HEADER, help: tx('st.header_name_help') })}
|
|
450
|
+
${input('logout_url', tx('st.logout_url'), a.logout_url ?? '', { placeholder: tx('st.logout_url_ph'), help: tx('st.logout_url_help') })}
|
|
450
451
|
</div>
|
|
451
|
-
<div class="field"><label class="check"><input type="checkbox" name="header_auto_create" value="true"${a.header_auto_create ? raw(' checked') : ''}>
|
|
452
|
-
<small class="help"
|
|
453
|
-
</div>${authPart('database')}<h3
|
|
454
|
-
<p class="muted"
|
|
452
|
+
<div class="field"><label class="check"><input type="checkbox" name="header_auto_create" value="true"${a.header_auto_create ? raw(' checked') : ''}> ${tx('st.header_auto')}</label>
|
|
453
|
+
<small class="help">${tx('st.header_auto_help')}</small></div>
|
|
454
|
+
</div>${authPart('database')}<h3>${tx('st.db_title')}</h3>
|
|
455
|
+
<p class="muted">${tx('st.db_intro')}</p>
|
|
455
456
|
<div class="form-grid">
|
|
456
|
-
${input('db_auth_roles', '
|
|
457
|
-
${input('db_auth_member_of', '
|
|
457
|
+
${input('db_auth_roles', tx('st.db_roles'), (a.db_auth_roles ?? []).join(', '), { placeholder: tx('st.db_roles_ph'), help: tx('st.db_roles_help') })}
|
|
458
|
+
${input('db_auth_member_of', tx('st.db_member'), a.db_auth_member_of ?? '', { placeholder: tx('st.db_member_ph'), help: tx('st.db_member_help') })}
|
|
458
459
|
</div>
|
|
459
|
-
</div>${authPart('custom')}<h3
|
|
460
|
-
<p class="muted"
|
|
460
|
+
</div>${authPart('custom')}<h3>${tx('st.custom_title')}</h3>
|
|
461
|
+
<p class="muted">${tx('st.custom_intro')}</p>
|
|
461
462
|
<div class="form-grid">
|
|
462
|
-
${input('custom_auth_function', '
|
|
463
|
+
${input('custom_auth_function', tx('st.custom_fn'), a.custom_auth_function ?? '', { placeholder: tx('st.custom_fn_ph'), help: tx('st.custom_fn_help') })}
|
|
463
464
|
</div>
|
|
464
|
-
<div class="field" data-wide><label class="label" for="f_custom_auth_code"
|
|
465
|
+
<div class="field" data-wide><label class="label" for="f_custom_auth_code">${tx('st.custom_body')}</label>
|
|
465
466
|
<textarea id="f_custom_auth_code" name="custom_auth_code" class="code" rows="6" spellcheck="false" data-code="plpgsql">${a.custom_auth_code ?? ''}</textarea>
|
|
466
|
-
<small class="help"
|
|
467
|
-
<div class="field" data-wide><label class="label" for="f_custom_auth_post_code"
|
|
467
|
+
<small class="help">${tx.rich('st.custom_body_help', { example: html`<code>return exists (select 1 from app.users where name = p_username and pw_hash = crypt(p_password, pw_hash));</code>` })}</small></div>
|
|
468
|
+
<div class="field" data-wide><label class="label" for="f_custom_auth_post_code">${tx('st.custom_post')}</label>
|
|
468
469
|
<textarea id="f_custom_auth_post_code" name="custom_auth_post_code" class="code" rows="4" spellcheck="false" data-code="plpgsql">${a.custom_auth_post_code ?? ''}</textarea>
|
|
469
|
-
<small class="help"
|
|
470
|
+
<small class="help">${tx('st.custom_post_help')}</small></div>
|
|
470
471
|
</div>`)}${end}
|
|
471
|
-
${panel('signin')}${region('
|
|
472
|
-
<h3
|
|
473
|
-
<div class="field"><label class="check"><input type="checkbox" name="local_login" value="true"${a.local_login ? raw(' checked') : ''}>
|
|
472
|
+
${panel('signin')}${region(tx('st.signin'), html`
|
|
473
|
+
<h3>${tx('st.methods')}</h3>
|
|
474
|
+
<div class="field"><label class="check"><input type="checkbox" name="local_login" value="true"${a.local_login ? raw(' checked') : ''}> ${tx('st.local')}</label></div>
|
|
474
475
|
${directories.length
|
|
475
|
-
? directories.map((d) => html`<div class="field"><label class="check"><input type="checkbox" name="ldap_directories" value="${d.name}"${a.ldap_directories.includes(d.name) ? raw(' checked') : ''}>
|
|
476
|
-
: html`<p class="muted"
|
|
477
|
-
<small class="help"
|
|
478
|
-
<div class="form-grid">${input('session_group', '
|
|
476
|
+
? directories.map((d) => html`<div class="field"><label class="check"><input type="checkbox" name="ldap_directories" value="${d.name}"${a.ldap_directories.includes(d.name) ? raw(' checked') : ''}> ${tx('st.ldap', { name: d.display_name })}${d.enabled ? '' : ` ${tx('st.disabled')}`}</label></div>`)
|
|
477
|
+
: html`<p class="muted">${tx.rich('st.no_ldap', { add: html`<a href="${BASE}/users/directories">${tx('st.add_one')}</a>` })}</p>`}
|
|
478
|
+
<small class="help">${tx('st.ldap_help')}</small>
|
|
479
|
+
<div class="form-grid">${input('session_group', tx('st.session_group'), a.session_group ?? '', { placeholder: tx('st.session_group_ph'), help: tx('st.session_group_help') })}</div>
|
|
479
480
|
${await loginSettingsFields(a)}
|
|
480
|
-
<div class="form-grid">${input('remember_me_days', '
|
|
481
|
+
<div class="form-grid">${input('remember_me_days', tx('st.remember'), a.remember_me_days ?? '', { type: 'number', placeholder: tx('st.remember_ph'), help: tx('st.remember_help') })}</div>
|
|
481
482
|
${providers.length
|
|
482
|
-
? providers.map((pr) => html`<div class="field"><label class="check"><input type="checkbox" name="sso_providers" value="${pr.name}"${a.sso_providers.includes(pr.name) ? raw(' checked') : ''}>
|
|
483
|
-
: html`<p class="muted"
|
|
483
|
+
? providers.map((pr) => html`<div class="field"><label class="check"><input type="checkbox" name="sso_providers" value="${pr.name}"${a.sso_providers.includes(pr.name) ? raw(' checked') : ''}> ${tx('st.sso', { name: pr.display_name })}${pr.enabled ? '' : ` ${tx('st.disabled')}`}</label></div>`)
|
|
484
|
+
: html`<p class="muted">${tx.rich('st.no_sso', { add: html`<a href="${BASE}/users/providers">${tx('st.add_one')}</a>` })}</p>`}
|
|
484
485
|
`)}${end}
|
|
485
|
-
${panel('ui')}${region('
|
|
486
|
-
<h3
|
|
486
|
+
${panel('ui')}${region(tx('st.tab.ui'), html`
|
|
487
|
+
<h3>${tx('st.theme')}</h3>
|
|
487
488
|
<div class="form-grid">
|
|
488
|
-
${select('base', '
|
|
489
|
-
${input('accent', '
|
|
490
|
-
${input('header', '
|
|
491
|
-
${(['accent_dark', 'header_dark'] as const).map((k) => html`<div class="field"><label class="label" for="f_${k}">${k === 'accent_dark' ? '
|
|
489
|
+
${select('base', tx('st.base'), baseStyleOf(a.theme), (['iris', 'lavender', 'standard'] as const).map((k): [string, string] => [k, tx(`st.base.${k}`)]), tx('st.base_help'))}
|
|
490
|
+
${input('accent', tx('st.accent'), a.theme?.accent ?? BASE_STYLES[baseStyleOf(a.theme)].accent, { type: 'color' })}
|
|
491
|
+
${input('header', tx('st.header_colour'), a.theme?.header ?? BASE_STYLES[baseStyleOf(a.theme)].header, { type: 'color' })}
|
|
492
|
+
${(['accent_dark', 'header_dark'] as const).map((k) => html`<div class="field"><label class="label" for="f_${k}">${tx(k === 'accent_dark' ? 'st.accent_dark' : 'st.header_dark')}</label>
|
|
492
493
|
<input id="f_${k}" name="${k}" type="color" value="${a.theme?.[k] ?? (k === 'accent_dark' ? '#a59cff' : '#0d0c1a')}">
|
|
493
|
-
<label class="check"><input type="checkbox" name="${k}_own" value="true"${a.theme?.[k] ? raw(' checked') : ''}>
|
|
494
|
-
${select('nav', '
|
|
495
|
-
${select('nav_list', '
|
|
496
|
-
${select('navbar_list', '
|
|
497
|
-
${select('mode', '
|
|
494
|
+
<label class="check"><input type="checkbox" name="${k}_own" value="true"${a.theme?.[k] ? raw(' checked') : ''}> ${tx('st.own_colour')}</label></div>`)}
|
|
495
|
+
${select('nav', tx('st.nav'), a.theme?.nav ?? 'side', [['side', tx('st.nav.side')], ['top', tx('st.nav.top')]], tx('st.nav_help'))}
|
|
496
|
+
${select('nav_list', tx('st.nav_list'), a.nav_list ?? '', listChoices(a.nav_list, tx('st.nav_entries')), tx('st.nav_list_help'))}
|
|
497
|
+
${select('navbar_list', tx('st.navbar_list'), a.navbar_list ?? '', listChoices(a.navbar_list, tx('st.none_option')), tx('st.navbar_help'))}
|
|
498
|
+
${select('mode', tx('st.mode'), a.theme?.mode ?? 'auto', (['auto', 'light', 'dark'] as const).map((k): [string, string] => [k, tx(`st.mode.${k}`)]))}
|
|
498
499
|
</div>
|
|
499
|
-
<div class="field"><label class="check"><input type="checkbox" name="user_choice" value="true"${a.theme?.user_choice !== false ? raw(' checked') : ''}>
|
|
500
|
-
<small class="help"
|
|
501
|
-
<p><a class="btn" href="${BASE}/apps/${a.id}/theme">${icon('settings')}
|
|
502
|
-
<span class="muted">${(() => { const n = appStyles(a.theme).length; return n ?
|
|
500
|
+
<div class="field"><label class="check"><input type="checkbox" name="user_choice" value="true"${a.theme?.user_choice !== false ? raw(' checked') : ''}> ${tx('st.user_choice')}</label>
|
|
501
|
+
<small class="help">${tx('st.user_choice_help')}</small></div>
|
|
502
|
+
<p><a class="btn" href="${BASE}/apps/${a.id}/theme">${icon('settings')} ${tx('st.roller')}</a>
|
|
503
|
+
<span class="muted">${(() => { const n = appStyles(a.theme).length; return n ? tx(n === 1 ? 'st.styles_one' : 'st.styles', { n }) : tx('st.no_styles'); })()}</span></p>
|
|
503
504
|
`)}${end}
|
|
504
|
-
${panel('globalization')}${region('
|
|
505
|
+
${panel('globalization')}${region(tx('st.tab.globalization'), html`
|
|
505
506
|
<div class="form-grid">
|
|
506
|
-
${input('language', '
|
|
507
|
-
${input('languages', '
|
|
508
|
-
${select('language_from', '
|
|
509
|
-
${input('date_format', '
|
|
510
|
-
${input('timestamp_format', '
|
|
507
|
+
${input('language', tx('st.language'), a.language, { help: tx('st.language_help') })}
|
|
508
|
+
${input('languages', tx('st.languages'), (a.languages ?? []).join(', '), { placeholder: tx('st.languages_ph'), help: tx('st.languages_help') })}
|
|
509
|
+
${select('language_from', tx('st.language_from'), a.language_from, (['browser', 'user', 'primary'] as const).map((k): [string, string] => [k, tx(`st.from.${k}`)]), tx('st.language_from_help'))}
|
|
510
|
+
${input('date_format', tx('st.date_format'), a.date_format, { placeholder: tx('st.date_format_ph'), help: tx('st.date_format_help') })}
|
|
511
|
+
${input('timestamp_format', tx('st.ts_format'), a.timestamp_format, { placeholder: tx('st.ts_format_ph') })}
|
|
511
512
|
</div>
|
|
512
513
|
${await timeZoneSettings(a)}`)}${end}
|
|
513
|
-
<div class="buttons settings-save"${['errors', 'pwa'].includes(tab) ? raw(' hidden') : ''}><button class="btn btn-hot"
|
|
514
|
+
<div class="buttons settings-save"${['errors', 'pwa'].includes(tab) ? raw(' hidden') : ''}><button class="btn btn-hot">${tx('st.save_settings')}</button></div>
|
|
514
515
|
</form>
|
|
515
516
|
${panel('definition')}<form method="post" action="${BASE}/apps/${a.id}/delete" class="danger-zone">${csrf(s)}
|
|
516
|
-
<button class="btn btn-danger" data-confirm="
|
|
517
|
+
<button class="btn btn-danger" data-confirm="${tx('st.delete_confirm', { app: a.name })}">${tx('st.delete')}</button>
|
|
517
518
|
</form>${end}
|
|
518
|
-
${panel('errors')}${region('
|
|
519
|
+
${panel('errors')}${region(tx('st.tab.errors'), html`<form method="post" action="${BASE}/apps/${a.id}/settings/behaviour">${csrf(s)}
|
|
519
520
|
<div class="form-grid">
|
|
520
|
-
${input('error_function', '
|
|
521
|
-
help: '
|
|
522
|
-
${input('session_warning_seconds', '
|
|
523
|
-
help: '
|
|
521
|
+
${input('error_function', tx('st.error_fn'), a.error_function ?? '', { placeholder: tx('st.error_fn_ph'),
|
|
522
|
+
help: tx('st.error_fn_help') })}
|
|
523
|
+
${input('session_warning_seconds', tx('st.warning'), a.session_warning_seconds ?? 120, { type: 'number',
|
|
524
|
+
help: tx('st.warning_help') })}
|
|
524
525
|
</div>
|
|
525
|
-
<div class="buttons"><button class="btn"
|
|
526
|
-
${panel('pwa')}${region('
|
|
527
|
-
${panel('security')}${region('
|
|
528
|
-
<li>${a.db_role ? '✓' : '✗'}
|
|
529
|
-
<li>${a.authentication !== 'none' ? '✓' : '•'} ${a.authentication !== 'none' ? '
|
|
526
|
+
<div class="buttons"><button class="btn">${tx('st.save')}</button></div></form>`)}${end}
|
|
527
|
+
${panel('pwa')}${region(tx('st.tab.pwa'), await pwaSection({ ...a, has_icon: !!(await owner.one('select pwa_icon is not null as h from meta.app where id = $1', [a.id]))?.h }, s))}${end}
|
|
528
|
+
${panel('security')}${region(tx('st.checklist'), html`<ul class="checklist">
|
|
529
|
+
<li>${a.db_role ? '✓' : '✗'} ${tx('st.ck.role')} ${a.db_role ? html`(<code>${a.db_role}</code>)` : html`<b>${tx('st.ck.runtime')}</b>`}</li>
|
|
530
|
+
<li>${a.authentication !== 'none' ? '✓' : '•'} ${a.authentication !== 'none' ? tx('st.ck.signin') : tx('st.ck.public')}</li>
|
|
530
531
|
${a.authentication === 'header' ? html`<li>${headerProxiesConfigured() ? '✓' : '✗'} Sign-in: HTTP header <code>${a.header_name || DEFAULT_HEADER}</code> ${headerProxiesConfigured() ? 'from the proxies in PGKILN_AUTH_HEADER_PROXIES' : html`<b>refused: PGKILN_AUTH_HEADER_PROXIES is not set</b>`}; access: ${a.access_control === 'any_user' ? 'any active account' : 'listed accounts only'}${a.header_auto_create ? ', new accounts created automatically' : ''}</li>` : ''}
|
|
531
532
|
${a.authentication === 'database' ? html`<li>${a.db_auth_roles?.length || a.db_auth_member_of ? '✓' : '✗'} Sign-in: database accounts (${[a.db_auth_roles?.length ? `roles ${a.db_auth_roles.join(', ')}` : '', a.db_auth_member_of ? `members of ${a.db_auth_member_of}` : ''].filter(Boolean).join('; ') || html`<b>no roles allowed</b>`})</li>` : ''}
|
|
532
533
|
${a.authentication === 'custom' ? html`<li>${a.custom_auth_function || a.custom_auth_code ? '✓' : '✗'} Sign-in: custom ${a.custom_auth_function ? html`function <code>${a.custom_auth_function}</code>` : a.custom_auth_code ? 'function body' : html`<b>no check configured: nobody can sign in</b>`}${a.custom_auth_post_code ? ', with post-authentication code' : ''}</li>` : ''}
|
|
533
534
|
${a.authentication === 'app_users' ? html`<li>Sign-in: ${[a.local_login ? 'password' : '', ...a.ldap_directories.map((d: string) => `LDAP ${d}`), ...a.sso_providers].filter(Boolean).join(', ') || html`<b>no method enabled</b>`}; access: ${a.access_control === 'any_user' ? 'any active account' : 'listed accounts only'}</li>` : ''}
|
|
534
|
-
<li>${a.debug ? '
|
|
535
|
-
<li>${a.debug_level ? `✗
|
|
536
|
-
<li
|
|
537
|
-
<li
|
|
535
|
+
<li>${a.debug ? `✗ ${tx('st.ck.debug_on')}` : `✓ ${tx('st.ck.debug_off')}`}</li>
|
|
536
|
+
<li>${a.debug_level ? `✗ ${tx('st.ck.msgs_on', { level: a.debug_level })}` : `✓ ${tx('st.ck.msgs_off')}`}</li>
|
|
537
|
+
<li>${tx('st.ck.unprotected', { n: (await owner.one("select count(*)::int as n from meta.page where app_id = $1 and protection = 'unrestricted'", [a.id])).n })}</li>
|
|
538
|
+
<li>${tx('st.ck.public_pages', { n: (await owner.one('select count(*)::int as n from meta.page where app_id = $1 and not requires_auth', [a.id])).n })}</li>
|
|
538
539
|
</ul>`)}${end}
|
|
539
540
|
</div>`;
|
|
540
|
-
return send(reply, s, shell(s,
|
|
541
|
+
return send(reply, s, shell(s, tx('st.title', { app: a.name }), [['App Builder', BASE], [a.name, `${BASE}/apps/${a.id}`], ['Settings']], main));
|
|
541
542
|
});
|
|
542
543
|
|
|
543
544
|
// (085) error handling function and session timeout warning
|
|
@@ -548,13 +549,13 @@ export async function builderRoutes(app: FastifyInstance) {
|
|
|
548
549
|
const fn = String(b.error_function ?? '').trim();
|
|
549
550
|
const seconds = Number(b.session_warning_seconds ?? 120);
|
|
550
551
|
try {
|
|
551
|
-
if (fn && !/^[A-Za-z_][A-Za-z0-9_$]{0,62}\.[A-Za-z_][A-Za-z0-9_$]{0,62}$/.test(fn)) throw new Error('
|
|
552
|
+
if (fn && !/^[A-Za-z_][A-Za-z0-9_$]{0,62}\.[A-Za-z_][A-Za-z0-9_$]{0,62}$/.test(fn)) throw new Error(tr(s)('st.err.fn_shape'));
|
|
552
553
|
if (fn && !(await owner.one(`select 1 as ok from pg_proc p join pg_namespace n on n.oid = p.pronamespace
|
|
553
554
|
where n.nspname = $1 and p.proname = $2 and p.pronargs = 1 and p.proargtypes[0] = 'jsonb'::regtype and p.prorettype = 'jsonb'::regtype`, fn.split('.'))))
|
|
554
|
-
throw new Error(
|
|
555
|
-
if (!Number.isInteger(seconds) || seconds < 0 || seconds > 3600) throw new Error('
|
|
555
|
+
throw new Error(tr(s)('st.err.no_fn', { fn }));
|
|
556
|
+
if (!Number.isInteger(seconds) || seconds < 0 || seconds > 3600) throw new Error(tr(s)('st.err.warning'));
|
|
556
557
|
await owner.query('update meta.app set error_function = $2, session_warning_seconds = $3, updated_at = now() where id = $1', [req.params.id, fn || null, seconds]);
|
|
557
|
-
flash(s, '
|
|
558
|
+
flash(s, tr(s)('st.saved'));
|
|
558
559
|
} catch (e) {
|
|
559
560
|
flash(s, (e as Error).message, 'error');
|
|
560
561
|
}
|
|
@@ -612,7 +613,7 @@ export async function builderRoutes(app: FastifyInstance) {
|
|
|
612
613
|
);
|
|
613
614
|
await saveTimeZoneSettings(req.params.id, b);
|
|
614
615
|
await saveLoginSettings(req.params.id, b);
|
|
615
|
-
flash(s, '
|
|
616
|
+
flash(s, tr(s)('st.settings_saved'));
|
|
616
617
|
} catch (e) {
|
|
617
618
|
flash(s, (e as Error).message, 'error');
|
|
618
619
|
}
|
|
@@ -625,7 +626,7 @@ export async function builderRoutes(app: FastifyInstance) {
|
|
|
625
626
|
const s = await developer(req, reply);
|
|
626
627
|
if (!s) return;
|
|
627
628
|
await owner.query('delete from meta.app where id = $1', [req.params.id]);
|
|
628
|
-
flash(s, '
|
|
629
|
+
flash(s, tr(s)('st.app_deleted'));
|
|
629
630
|
return back(reply, s, BASE);
|
|
630
631
|
});
|
|
631
632
|
|