From 72cd58ac521612b7a57b2eb8daba0b0c4ff03177 Mon Sep 17 00:00:00 2001 From: Aidan Jones Date: Mon, 24 Aug 2026 15:42:19 -0500 Subject: [PATCH 1/8] Fix typo on request sent page --- .../request-access-for-organization/success/+page.svelte | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/routes/(unauthenticated)/request-access-for-organization/success/+page.svelte b/src/routes/(unauthenticated)/request-access-for-organization/success/+page.svelte index d2eeb93df5..5b4313a5fa 100644 --- a/src/routes/(unauthenticated)/request-access-for-organization/success/+page.svelte +++ b/src/routes/(unauthenticated)/request-access-for-organization/success/+page.svelte @@ -3,6 +3,6 @@

Request sent!

- An email has been sent to the Scriporia team, and you'll receive an invitation after your request + An email has been sent to the Scriptoria team, and you'll receive an invitation after your request has been reviewed.

From 3986957440b36e094fbbdf016edd34dacaca7331 Mon Sep 17 00:00:00 2001 From: Aidan Jones Date: Mon, 24 Aug 2026 15:53:58 -0500 Subject: [PATCH 2/8] Add org Visible to Public setting --- src/lib/locales/en-US.json | 2 ++ src/lib/locales/es-419.json | 2 ++ src/lib/locales/fr-FR.json | 2 ++ src/lib/organizations/index.ts | 3 ++- .../prisma/migrations/41_public_orgs/migration.sql | 2 ++ src/lib/prisma/schema.prisma | 1 + .../[id=number]/settings/info/+page.server.ts | 6 ++++-- .../[id=number]/settings/info/+page.svelte | 12 ++++++++++++ 8 files changed, 27 insertions(+), 3 deletions(-) create mode 100644 src/lib/prisma/migrations/41_public_orgs/migration.sql diff --git a/src/lib/locales/en-US.json b/src/lib/locales/en-US.json index 0a76b47a89..54b0cab535 100644 --- a/src/lib/locales/en-US.json +++ b/src/lib/locales/en-US.json @@ -213,6 +213,8 @@ "org_accessToken": "Build Engine API Access Token", "org_emptyBuildEngineURL": "A URL must be provided when not using the Default Build Engine", "org_emptyAccessToken": "An API token must be provided when not using the Default Build Engine", + "org_visible": "Visible to Public", + "org_visibleWarning": "Warning: if your organization is visible to the public, all visitors of Scriptoria, including non-users, will be able to see your name, contact, and logo!", "products_definition": "Useful files that result from a series of steps.", "products_storeSelect": "Select a store for {name}", "products_noStoresAvailable": "There are no stores available for the selected product. Please contact your organization administrator.", diff --git a/src/lib/locales/es-419.json b/src/lib/locales/es-419.json index 4949a8d834..0eef5a3301 100644 --- a/src/lib/locales/es-419.json +++ b/src/lib/locales/es-419.json @@ -213,6 +213,8 @@ "org_accessToken": "Token de acceso del motor de construcción", "org_emptyBuildEngineURL": "Se debe proporcionar una URL cuando no se utiliza el motor de compilación predeterminado", "org_emptyAccessToken": "Se debe proporcionar un token de API cuando no se utiliza el motor de compilación predeterminado", + "org_visible": "Visible para el público", + "org_visibleWarning": "Advertencia: si su organización es visible para el público, todos los visitantes de Scriptoria —incluidas las personas que no son usuarias— podrán ver su nombre, datos de contacto y logotipo.", "products_definition": "Archivos útiles que resultan de una serie de pasos.", "products_storeSelect": "Seleccione una tienda para {name}", "products_noStoresAvailable": "No hay tiendas disponibles para el producto seleccionado. Por favor, contacte con el administrador de su organización.", diff --git a/src/lib/locales/fr-FR.json b/src/lib/locales/fr-FR.json index 24f4f0ce24..2dc0d37ed8 100644 --- a/src/lib/locales/fr-FR.json +++ b/src/lib/locales/fr-FR.json @@ -213,6 +213,8 @@ "org_accessToken": "Build Engine API Access Token", "org_emptyBuildEngineURL": "A URL must be provided when not using the Default Build Engine", "org_emptyAccessToken": "An API token must be provided when not using the Default Build Engine", + "org_visible": "Visible to Public", + "org_visibleWarning": "Warning: if your organization is visible to the public, all visitors of Scriptoria, including non-users, will be able to see your name, contact, and logo!", "products_definition": "Useful files that result from a series of steps.", "products_storeSelect": "Select a store for {name}", "products_noStoresAvailable": "There are no stores available for the selected product. Please contact your organization administrator.", diff --git a/src/lib/organizations/index.ts b/src/lib/organizations/index.ts index 3dc3307246..d2fd719f94 100644 --- a/src/lib/organizations/index.ts +++ b/src/lib/organizations/index.ts @@ -12,7 +12,8 @@ export const infoSchema = v.object({ ), v.pipe(v.string(), v.email()) ]) - ) + ), + visibleToPublic: v.boolean() }); export const infrastructureSchema = v.object({ diff --git a/src/lib/prisma/migrations/41_public_orgs/migration.sql b/src/lib/prisma/migrations/41_public_orgs/migration.sql new file mode 100644 index 0000000000..96ff80526f --- /dev/null +++ b/src/lib/prisma/migrations/41_public_orgs/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "Organizations" ADD COLUMN "VisibleToPublic" BOOLEAN NOT NULL DEFAULT false; diff --git a/src/lib/prisma/schema.prisma b/src/lib/prisma/schema.prisma index 7122724a7b..dccddcb92a 100644 --- a/src/lib/prisma/schema.prisma +++ b/src/lib/prisma/schema.prisma @@ -70,6 +70,7 @@ model Organizations { UseDefaultBuildEngine Boolean @default(true) PublicByDefault Boolean @default(true) ContactEmail String? + VisibleToPublic Boolean @default(false) Groups Groups[] OrganizationMembershipInvites OrganizationMembershipInvites[] Users Users[] diff --git a/src/routes/(authenticated)/organizations/[id=number]/settings/info/+page.server.ts b/src/routes/(authenticated)/organizations/[id=number]/settings/info/+page.server.ts index 9d7da1266b..bea1096032 100644 --- a/src/routes/(authenticated)/organizations/[id=number]/settings/info/+page.server.ts +++ b/src/routes/(authenticated)/organizations/[id=number]/settings/info/+page.server.ts @@ -11,7 +11,8 @@ export const load = (async (event) => { { name: organization.Name, logoUrl: organization.LogoUrl, - contact: organization.ContactEmail + contact: organization.ContactEmail, + visibleToPublic: organization.VisibleToPublic }, valibot(infoSchema) ); @@ -26,7 +27,8 @@ export const actions = { await DatabaseWrites.organizations.update(parseInt(event.params.id), { Name: form.data.name, LogoUrl: form.data.logoUrl, - ContactEmail: form.data.contact + ContactEmail: form.data.contact, + VisibleToPublic: form.data.visibleToPublic }); return { form, ok: true }; } diff --git a/src/routes/(authenticated)/organizations/[id=number]/settings/info/+page.svelte b/src/routes/(authenticated)/organizations/[id=number]/settings/info/+page.svelte index 64c56c5ba4..db52ae68df 100644 --- a/src/routes/(authenticated)/organizations/[id=number]/settings/info/+page.svelte +++ b/src/routes/(authenticated)/organizations/[id=number]/settings/info/+page.svelte @@ -3,6 +3,7 @@ import type { PageData } from './$types'; import LabeledFormInput from '$lib/components/settings/LabeledFormInput.svelte'; import SubmitButton from '$lib/components/settings/SubmitButton.svelte'; + import Toggle from '$lib/components/settings/Toggle.svelte'; import { Icons } from '$lib/icons'; import { m } from '$lib/paraglide/messages'; import { toast } from '$lib/utils'; @@ -54,6 +55,17 @@ }} bind:value={$form.logoUrl} /> +
Logo
From 06a56cc143df7393f44514170be39280fb014ea3 Mon Sep 17 00:00:00 2001 From: Aidan Jones Date: Tue, 25 Aug 2026 13:15:09 -0500 Subject: [PATCH 3/8] Create Our Users index page --- src/lib/locales/en-US.json | 2 + src/lib/locales/es-419.json | 2 + src/lib/locales/fr-FR.json | 2 + .../(unauthenticated)/(auth)/+layout@.svelte | 13 ----- .../(auth)/login/LoginScreen.svelte | 3 +- src/routes/(unauthenticated)/+layout.svelte | 2 +- .../our-users/+page.server.ts | 16 ++++++ .../(unauthenticated)/our-users/+page.svelte | 51 +++++++++++++++++++ .../+page.server.ts | 16 +++++- .../+page.svelte | 20 +++++++- 10 files changed, 110 insertions(+), 17 deletions(-) delete mode 100644 src/routes/(unauthenticated)/(auth)/+layout@.svelte create mode 100644 src/routes/(unauthenticated)/our-users/+page.server.ts create mode 100644 src/routes/(unauthenticated)/our-users/+page.svelte diff --git a/src/lib/locales/en-US.json b/src/lib/locales/en-US.json index 54b0cab535..af74832ae2 100644 --- a/src/lib/locales/en-US.json +++ b/src/lib/locales/en-US.json @@ -164,6 +164,8 @@ "invitations_orgSubmit": "Add Organization", "invitations_requestOrgInvite": "Request Organization Invite", "invitations_orgAdminEmail": "Organization Admin Email", + "invitations_ourUsers": "Our Users", + "invitations_verifyUser": "Before submitting a request, please verify that your organization is not already a user of Scriptoria.", "newOrganization_title": "Add organization", "org_title": "Organizations", "org_add": "Add Organization", diff --git a/src/lib/locales/es-419.json b/src/lib/locales/es-419.json index 0eef5a3301..29e6fd6305 100644 --- a/src/lib/locales/es-419.json +++ b/src/lib/locales/es-419.json @@ -164,6 +164,8 @@ "invitations_orgSubmit": "Agregar organización", "invitations_requestOrgInvite": "Solicitar invitación de la organización", "invitations_orgAdminEmail": "Correo electrónico del administrador", + "invitations_ourUsers": "Nuestras Usuarias", + "invitations_verifyUser": "Antes de enviar una solicitud, por favor verifique que su organización no sea ya usuaria de Scriptoria.", "newOrganization_title": "Agregar organización", "org_title": "Organizaciones", "org_add": "Añadir organización", diff --git a/src/lib/locales/fr-FR.json b/src/lib/locales/fr-FR.json index 2dc0d37ed8..ecd36c8779 100644 --- a/src/lib/locales/fr-FR.json +++ b/src/lib/locales/fr-FR.json @@ -164,6 +164,8 @@ "invitations_orgSubmit": "Add Organization", "invitations_requestOrgInvite": "Request Organization Invite", "invitations_orgAdminEmail": "Organization Admin Email", + "invitations_ourUsers": "Our Users", + "invitations_verifyUser": "Before submitting a request, please verify that your organization is not already a user of Scriptoria.", "newOrganization_title": "Add organization", "org_title": "Organizations", "org_add": "Add Organization", diff --git a/src/routes/(unauthenticated)/(auth)/+layout@.svelte b/src/routes/(unauthenticated)/(auth)/+layout@.svelte deleted file mode 100644 index 745bd915ba..0000000000 --- a/src/routes/(unauthenticated)/(auth)/+layout@.svelte +++ /dev/null @@ -1,13 +0,0 @@ - - -
- {@render children?.()} -
diff --git a/src/routes/(unauthenticated)/(auth)/login/LoginScreen.svelte b/src/routes/(unauthenticated)/(auth)/login/LoginScreen.svelte index b7ff04b647..e050a95720 100644 --- a/src/routes/(unauthenticated)/(auth)/login/LoginScreen.svelte +++ b/src/routes/(unauthenticated)/(auth)/login/LoginScreen.svelte @@ -4,6 +4,7 @@ import { browser } from '$app/environment'; import ScriptoriaIcon from '$lib/icons/ScriptoriaIcon.svelte'; import * as m from '$lib/paraglide/messages'; + import { localizeHref } from '$lib/paraglide/runtime'; let timeout: ReturnType | null = null; @@ -71,7 +72,7 @@ {m.invitations_orgPrompt()} - + {m.contactUs()} diff --git a/src/routes/(unauthenticated)/+layout.svelte b/src/routes/(unauthenticated)/+layout.svelte index 4ebb696be4..dcf9e4e31f 100644 --- a/src/routes/(unauthenticated)/+layout.svelte +++ b/src/routes/(unauthenticated)/+layout.svelte @@ -10,7 +10,7 @@
{@render children?.()}
diff --git a/src/routes/(unauthenticated)/our-users/+page.server.ts b/src/routes/(unauthenticated)/our-users/+page.server.ts new file mode 100644 index 0000000000..2533dd5ac0 --- /dev/null +++ b/src/routes/(unauthenticated)/our-users/+page.server.ts @@ -0,0 +1,16 @@ +import type { PageServerLoad } from './$types'; +import { DatabaseReads } from '$lib/server/database'; + +export const load = (async (event) => { + event.locals.security.requireNothing(); + return { + organizations: await DatabaseReads.organizations.findMany({ + where: { VisibleToPublic: true }, + select: { + Name: true, + LogoUrl: true, + ContactEmail: true + } + }) + }; +}) satisfies PageServerLoad; diff --git a/src/routes/(unauthenticated)/our-users/+page.svelte b/src/routes/(unauthenticated)/our-users/+page.svelte new file mode 100644 index 0000000000..29bfa0adbb --- /dev/null +++ b/src/routes/(unauthenticated)/our-users/+page.svelte @@ -0,0 +1,51 @@ + + +

{m.invitations_ourUsers()}

+
+ + + + + + + + + {#each data.organizations.toSorted((a, b) => byName(a, b, getLocale())) as org} + + + + + {/each} + +
{m.project_org()}{m.project_orgContact()}
+ {#if org.LogoUrl} + Logo + {:else} +
+
+
+ {/if} + + {org.Name} + +
+ {#if org.ContactEmail} + + {org.ContactEmail} + + {:else} + {m.common_notAvailable()} + {/if} +
+
diff --git a/src/routes/(unauthenticated)/request-access-for-organization/+page.server.ts b/src/routes/(unauthenticated)/request-access-for-organization/+page.server.ts index 25b8b1a557..1a69178fa2 100644 --- a/src/routes/(unauthenticated)/request-access-for-organization/+page.server.ts +++ b/src/routes/(unauthenticated)/request-access-for-organization/+page.server.ts @@ -1,8 +1,9 @@ import { fail, superValidate } from 'sveltekit-superforms'; import { valibot } from 'sveltekit-superforms/adapters'; import * as v from 'valibot'; -import type { Actions } from './$types'; +import type { Actions, PageServerLoad } from './$types'; import { BullMQ, getQueues } from '$lib/server/bullmq'; +import { DatabaseReads } from '$lib/server/database'; const requestSchema = v.object({ organizationName: v.pipe(v.string(), v.nonEmpty()), @@ -10,6 +11,19 @@ const requestSchema = v.object({ url: v.pipe(v.string(), v.nonEmpty()) }); +export const load = (async ({ locals }) => { + locals.security.requireNothing(); + + return { + publicOrgExists: !!(await DatabaseReads.organizations.findFirst({ + where: { VisibleToPublic: true }, + select: { + Id: true + } + })) + }; +}) satisfies PageServerLoad; + export const actions = { async request(event) { event.locals.security.requireNothing(); diff --git a/src/routes/(unauthenticated)/request-access-for-organization/+page.svelte b/src/routes/(unauthenticated)/request-access-for-organization/+page.svelte index 733fa7ed30..43a00244aa 100644 --- a/src/routes/(unauthenticated)/request-access-for-organization/+page.svelte +++ b/src/routes/(unauthenticated)/request-access-for-organization/+page.svelte @@ -1,11 +1,20 @@

{m.invitations_requestOrgInvite()}

+ {#if data.publicOrgExists} +

+ {m.invitations_verifyUser()} + + {m.invitations_ourUsers()} + + +

+ {/if} From 3b53a6b2a7dc1df23b3eae5ec77acf29291e6ef6 Mon Sep 17 00:00:00 2001 From: Aidan Jones Date: Wed, 26 Aug 2026 13:52:02 -0500 Subject: [PATCH 4/8] Break Turnstile functionality into shared library --- .../turnstile.js => turnstile/index.ts} | 45 ++++++---- src/lib/turnstile/server.ts | 82 +++++++++++++++++++ .../[productId=uuid]/+page.server.ts | 79 ++---------------- .../user-data/[productId=uuid]/+page.svelte | 14 +--- 4 files changed, 120 insertions(+), 100 deletions(-) rename src/lib/{google-play/turnstile.js => turnstile/index.ts} (58%) create mode 100644 src/lib/turnstile/server.ts diff --git a/src/lib/google-play/turnstile.js b/src/lib/turnstile/index.ts similarity index 58% rename from src/lib/google-play/turnstile.js rename to src/lib/turnstile/index.ts index 62bc459fc6..29da43e782 100644 --- a/src/lib/google-play/turnstile.js +++ b/src/lib/turnstile/index.ts @@ -1,12 +1,12 @@ // Codes where the Google Play locale's base language tag doesn't match Turnstile's. // https://developers.cloudflare.com/turnstile/reference/supported-languages/ -const LANGUAGE_OVERRIDES = /** @type {Record} */ ({ +const LANGUAGE_OVERRIDES: Record = { 'iw-IL': 'he', // Hebrew: Google Play uses the legacy ISO 639-1 code 'no-NO': 'nb', // Norwegian: Turnstile only supports Bokmål - 'fil': 'tl-ph', // Filipino: Ternstile only supports Tagalog (better than defaulting to English) + fil: 'tl-ph', // Filipino: Ternstile only supports Tagalog (better than defaulting to English) 'zh-TW': 'zh-tw', // Chinese (Traditional, Taiwan): base 'zh' would default to Simplified 'zh-HK': 'zh-tw' // Chinese (Traditional, Hong Kong): Turnstile has no zh-hk, zh-tw is closer than Simplified -}); +}; const TURNSTILE_BASE_LANGUAGES = new Set([ 'ar', @@ -52,10 +52,8 @@ const TURNSTILE_BASE_LANGUAGES = new Set([ * Maps a Google Play locale code to a Turnstile-supported language code, * falling back to 'auto' (browser detection) when Turnstile has no match. * There are currently 34 languages that don't have a match in Turnstile. - * @param {string} locale - * @returns {string} */ -export function toTurnstileLanguage(locale) { +export function toTurnstileLanguage(locale: string) { if (LANGUAGE_OVERRIDES[locale]) { return LANGUAGE_OVERRIDES[locale]; } @@ -64,26 +62,37 @@ export function toTurnstileLanguage(locale) { return TURNSTILE_BASE_LANGUAGES.has(base) ? base : 'auto'; } -/** - * @param {string} containerId - * @param {string | undefined} sitekey - * @param {(token: string) => void} callback - * @param {string} [language] - */ -export function initTurnstile(containerId, sitekey, callback, language = 'auto') { +export function initTurnstile( + containerId: string, + sitekey: string | undefined, + callback: (token: string) => void, + language = 'auto' +) { if (sitekey) { - // @ts-ignore - const widgetId = turnstile.render(containerId, { + // @ts-expect-error turnstile is a global variable set by imported cloudflare library + const widgetId = turnstile?.render(containerId, { sitekey, callback, language }); return () => { - // @ts-ignore - turnstile.remove(widgetId); + // @ts-expect-error turnstile is a global variable set by imported cloudflare library + turnstile?.remove(widgetId); }; } else { console.error('Turnstile SiteKey not set!'); } -} \ No newline at end of file +} + +export function resolveToken(formData: FormData) { + const tokenFromForm = formData.get('cf-turnstile-response'); + const tokenFromWidget = window.turnstile?.getResponse?.(); + const token = + typeof tokenFromForm === 'string' && tokenFromForm.length > 0 ? tokenFromForm : tokenFromWidget; + if (typeof token === 'string' && token.length > 0) { + return token; + } else { + return null; + } +} diff --git a/src/lib/turnstile/server.ts b/src/lib/turnstile/server.ts new file mode 100644 index 0000000000..590ba7b4fd --- /dev/null +++ b/src/lib/turnstile/server.ts @@ -0,0 +1,82 @@ +import { SpanStatusCode, trace } from '@opentelemetry/api'; + +const tracer = trace.getTracer('TurnstileVerification'); + +const TURNSTILE_TIMEOUT_MS = 5000; + +export function resolveToken(formData: FormData) { + const turnstileToken = formData.get('turnstileToken'); + const turnstileResponse = formData.get('cf-turnstile-response'); + + if ( + (!turnstileToken || (typeof turnstileToken === 'string' && !turnstileToken.trim())) && + typeof turnstileResponse === 'string' + ) { + formData.set('turnstileToken', turnstileResponse.trim()); + } +} + +export async function verifyToken(token: string, secret: string | undefined) { + return tracer.startActiveSpan('Verify Turnstile Token', async (span) => { + try { + if (!secret) { + span.recordException('Turnstile secret key is not configured'); + span.setStatus({ + code: SpanStatusCode.ERROR, + message: 'Turnstile secret key is not configured' + }); + return 500; + } + + let verification: Response; + try { + verification = await fetch('https://challenges.cloudflare.com/turnstile/v0/siteverify', { + method: 'POST', + body: new URLSearchParams({ secret, response: token }), + signal: AbortSignal.timeout(TURNSTILE_TIMEOUT_MS) + }); + } catch (e) { + span.recordException(e as Error); + span.setStatus({ + code: SpanStatusCode.ERROR, + message: (e as Error).message + }); + console.warn('Turnstile verification request failed', { error: e }); + return 503; + } + + const result = await verification.json().catch(() => null); + if (!verification.ok || !result || typeof result.success !== 'boolean') { + span.setStatus({ + code: SpanStatusCode.ERROR, + message: `Turnstile verification returned an invalid response: ${JSON.stringify(result)}` + }); + console.warn('Turnstile verification returned an invalid response', { + status: verification.status + }); + return 502; + } + + if (!result.success) { + console.warn('Turnstile verification failed', { + errorCodes: result['error-codes'], + hostname: result.hostname, + action: result.action + }); + return 400; + } + + return 200; + } catch (e) { + span.recordException(e as Error); + span.setStatus({ + code: SpanStatusCode.ERROR, + message: (e as Error).message + }); + console.warn('Turnstile verification request failed', { error: e }); + return 500; + } finally { + span.end(); + } + }); +} diff --git a/src/routes/(unauthenticated)/(google-play)/user-data/[productId=uuid]/+page.server.ts b/src/routes/(unauthenticated)/(google-play)/user-data/[productId=uuid]/+page.server.ts index 93549d6f54..76d9742951 100644 --- a/src/routes/(unauthenticated)/(google-play)/user-data/[productId=uuid]/+page.server.ts +++ b/src/routes/(unauthenticated)/(google-play)/user-data/[productId=uuid]/+page.server.ts @@ -12,11 +12,10 @@ import type { Locale } from '$lib/google-play/paraglide/runtime'; import { saveDeleteRequestVerificationCode } from '$lib/google-play/server'; import { DatabaseWrites } from '$lib/server/database'; import { sendEmail } from '$lib/server/email-service/EmailClient'; +import { resolveToken, verifyToken } from '$lib/turnstile/server'; const tracer = trace.getTracer('UDMRequests'); -const TURNSTILE_TIMEOUT_MS = 5000; - const localizedSchema = (locale: Locale) => v.object({ email: localizedEmailSchema(locale), @@ -46,15 +45,7 @@ export const actions: Actions = { return tracer.startActiveSpan('UDM - Send Code', async (span) => { try { const formData = await request.formData(); - const turnstileToken = formData.get('turnstileToken'); - const turnstileResponse = formData.get('cf-turnstile-response'); - - if ( - (!turnstileToken || (typeof turnstileToken === 'string' && !turnstileToken.trim())) && - typeof turnstileResponse === 'string' - ) { - formData.set('turnstileToken', turnstileResponse); - } + resolveToken(formData); const locale = locals.locale as Locale; @@ -64,69 +55,17 @@ export const actions: Actions = { return fail(400, { form }); } - const token = form.data.turnstileToken.trim(); + const verifyResult = await verifyToken( + form.data.turnstileToken, + env.USER_DATA_TURNSTILE_SECRET_KEY + ); - const secret = env.USER_DATA_TURNSTILE_SECRET_KEY; - if (!secret) { - span.recordException('Turnstile secret key is not configured'); - span.setStatus({ - code: SpanStatusCode.ERROR, - message: 'Turnstile secret key is not configured' - }); - return message( - form, - { error: m.alert_verification_failed({}, { locale }) }, - { status: 500 } - ); - } - - let verification: Response; - try { - verification = await fetch('https://challenges.cloudflare.com/turnstile/v0/siteverify', { - method: 'POST', - body: new URLSearchParams({ secret, response: token }), - signal: AbortSignal.timeout(TURNSTILE_TIMEOUT_MS) - }); - } catch (e) { - span.recordException(e as Error); - span.setStatus({ - code: SpanStatusCode.ERROR, - message: (e as Error).message - }); - console.warn('Turnstile verification request failed', { error: e }); - return message( - form, - { error: m.alert_verification_failed({}, { locale }) }, - { status: 503 } - ); - } - - const result = await verification.json().catch(() => null); - if (!verification.ok || !result || typeof result.success !== 'boolean') { - span.setStatus({ - code: SpanStatusCode.ERROR, - message: `Turnstile verification returned an invalid response: ${JSON.stringify(result)}` - }); - console.warn('Turnstile verification returned an invalid response', { - status: verification.status - }); - return message( - form, - { error: m.alert_verification_failed({}, { locale }) }, - { status: 502 } - ); - } - - if (!result.success) { - console.warn('Turnstile verification failed', { - errorCodes: result['error-codes'], - hostname: result.hostname, - action: result.action - }); + if (verifyResult !== 200) { + // logging handled in verifyToken return message( form, { error: m.alert_verification_failed({}, { locale }) }, - { status: 400 } + { status: verifyResult } ); } diff --git a/src/routes/(unauthenticated)/(google-play)/user-data/[productId=uuid]/+page.svelte b/src/routes/(unauthenticated)/(google-play)/user-data/[productId=uuid]/+page.svelte index ddd568f372..186140ee7e 100644 --- a/src/routes/(unauthenticated)/(google-play)/user-data/[productId=uuid]/+page.svelte +++ b/src/routes/(unauthenticated)/(google-play)/user-data/[productId=uuid]/+page.svelte @@ -9,8 +9,8 @@ import LocaleSelector from '$lib/google-play/components/LocaleSelector.svelte'; import { m } from '$lib/google-play/paraglide/messages'; import { type Locale, localizeHref } from '$lib/google-play/paraglide/runtime'; - import { initTurnstile, toTurnstileLanguage } from '$lib/google-play/turnstile'; import { getBasicVariant } from '$lib/ldml'; + import { initTurnstile, resolveToken, toTurnstileLanguage } from '$lib/turnstile'; interface Props { data: PageData; @@ -28,17 +28,7 @@ onSubmit: ({ cancel, formData }) => { deleteSubmitAttempted = true; - if (!turnstileToken) { - const tokenFromForm = formData.get('cf-turnstile-response'); - const tokenFromWidget = window.turnstile?.getResponse?.(); - const token = - typeof tokenFromForm === 'string' && tokenFromForm.length > 0 - ? tokenFromForm - : tokenFromWidget; - if (typeof token === 'string' && token.length > 0) { - turnstileToken = token; - } - } + turnstileToken ||= resolveToken(formData); if (!turnstileToken) { $message = { error: m.alert_verify_human() }; From a58cf6ca6a272e0dea3ff511f24f87d9d81d1147 Mon Sep 17 00:00:00 2001 From: Aidan Jones Date: Fri, 28 Aug 2026 10:40:02 -0500 Subject: [PATCH 5/8] Add error logging utility functions --- src/auth.ts | 3 ++- src/hooks.server.ts | 8 +++++--- src/lib/otel/index.ts | 6 +++--- src/lib/projects/index.ts | 3 ++- src/lib/server/build-engine-api/requests.ts | 3 ++- src/lib/server/bullmq/BullWorker.ts | 3 ++- src/lib/server/bullmq/queues.ts | 18 +++++++++++------- src/lib/server/database/Products.ts | 5 ++--- src/lib/server/database/prisma.ts | 3 ++- src/lib/server/email-service/EmailClient.ts | 5 ++--- src/lib/server/job-executors/email.ts | 3 ++- src/lib/server/job-executors/product.ts | 3 ++- src/lib/server/workflow/dbProcedures.ts | 5 +++-- src/lib/turnstile/server.ts | 10 ++++++---- src/lib/utils/index.ts | 4 ++++ src/lib/utils/server.ts | 3 +++ src/routes/(authenticated)/+layout.server.ts | 5 ++++- .../projects/[id=number]/sse/+server.ts | 1 - .../[[orgId=number]]/sse/products/+server.ts | 6 +++++- .../[[orgId=number]]/sse/updates/+server.ts | 6 +++++- .../user-data/[productId=uuid]/+page.server.ts | 7 ++++--- .../docs/[filename]/+server.ts | 6 +++++- 22 files changed, 76 insertions(+), 40 deletions(-) create mode 100644 src/lib/utils/server.ts diff --git a/src/auth.ts b/src/auth.ts index ea56b745dc..00b963040e 100644 --- a/src/auth.ts +++ b/src/auth.ts @@ -13,6 +13,7 @@ import { checkInviteErrors } from '$lib/organizationInvites'; import { localizeHref } from '$lib/paraglide/runtime'; import { RoleId } from '$lib/prisma'; import { DatabaseReads, DatabaseWrites } from '$lib/server/database'; +import { stringifyError } from '$lib/utils'; declare module '@auth/sveltekit' { interface Session { @@ -332,7 +333,7 @@ export const populateSecurityInfo: Handle = async ({ event, resolve }) => { // Suppress auth failures but log for debugging trace.getActiveSpan()?.addEvent('API auth failed', { 'auth.hasToken': !!authToken, - 'auth.validationError': e instanceof Error ? e.message : String(e) + 'auth.validationError': stringifyError(e) }); } } diff --git a/src/hooks.server.ts b/src/hooks.server.ts index 0bc7d752b5..86bf825eb4 100644 --- a/src/hooks.server.ts +++ b/src/hooks.server.ts @@ -25,6 +25,8 @@ import { QueueConnected, getQueues } from '$lib/server/bullmq'; import { bullboardHandle } from '$lib/server/bullmq/BullBoard'; import { allWorkers } from '$lib/server/bullmq/BullMQ'; import { DatabaseConnected, DatabaseReads, DatabaseWrites } from '$lib/server/database'; +import { stringifyError } from '$lib/utils'; +import { logLocalDev } from '$lib/utils/server'; if (!building) { // Start OTEL collector @@ -190,7 +192,7 @@ export const handle: Handle = async ({ event, resolve }) => { export const handleError: HandleServerError = ({ error, event, status }) => { // Log the error with OTEL OTEL.instance.logger.error('Error in handleError', { - error: error instanceof Error ? error.message : String(error), + error: stringifyError(error), route: event.route.id, method: event.request.method, url: event.url.href @@ -198,7 +200,7 @@ export const handleError: HandleServerError = ({ error, event, status }) => { trace.getActiveSpan()?.recordException(error as Error); trace.getActiveSpan()?.setStatus({ code: SpanStatusCode.ERROR, // Error - message: error instanceof Error ? error.message : String(error) + message: stringifyError(error) }); if (status === 404) { @@ -209,7 +211,7 @@ export const handleError: HandleServerError = ({ error, event, status }) => { }; } - console.error('Error occurred:', error); + logLocalDev?.('Error occurred:', error); return { message: 'An unexpected error occurred. Please try again later.', diff --git a/src/lib/otel/index.ts b/src/lib/otel/index.ts index 9abae2b56c..e6984ae8f1 100644 --- a/src/lib/otel/index.ts +++ b/src/lib/otel/index.ts @@ -19,6 +19,7 @@ import { LoggerProvider, SimpleLogRecordProcessor } from '@opentelemetry/sdk-log import { PeriodicExportingMetricReader } from '@opentelemetry/sdk-metrics'; import { NodeSDK } from '@opentelemetry/sdk-node'; import { ATTR_SERVICE_NAME, ATTR_SERVICE_VERSION } from '@opentelemetry/semantic-conventions'; +import { inLocalDevelopment } from '$lib/utils/server'; class Logger { constructor( @@ -60,8 +61,7 @@ export default class OTEL { private _logger: Logger; private constructor() { - const isDev = process.env.NODE_ENV === 'development'; - const endpoint = `http://${isDev ? 'localhost' : 'otel'}:6317`; + const endpoint = `http://${inLocalDevelopment ? 'localhost' : 'otel'}:6317`; const resource = resourceFromAttributes({ [ATTR_SERVICE_NAME]: 'scriptoria', @@ -79,7 +79,7 @@ export default class OTEL { resource, processors: [logProcessor] }).getLogger('scriptoria-logger'), - isDev + inLocalDevelopment ); this.sdk = new NodeSDK({ diff --git a/src/lib/projects/index.ts b/src/lib/projects/index.ts index 7602803271..1da79c9532 100644 --- a/src/lib/projects/index.ts +++ b/src/lib/projects/index.ts @@ -2,6 +2,7 @@ import type { Session } from '@auth/sveltekit'; import type { Prisma } from '@prisma/client'; import * as v from 'valibot'; import { RoleId } from '$lib/prisma'; +import { stringifyError } from '$lib/utils'; import { isAdminForOrg } from '$lib/utils/roles'; import { idSchema, langtagRegex, paginateSchema, requiredString } from '$lib/valibot'; @@ -165,7 +166,7 @@ export const importJSONSchema = v.pipe( return JSON.parse(dataset.value || '{}'); } catch (e) { addIssue({ - message: e instanceof Error ? e.message : String(e), + message: stringifyError(e), path: [ { type: 'unknown', diff --git a/src/lib/server/build-engine-api/requests.ts b/src/lib/server/build-engine-api/requests.ts index 3dfea30145..28de8b79fd 100644 --- a/src/lib/server/build-engine-api/requests.ts +++ b/src/lib/server/build-engine-api/requests.ts @@ -4,6 +4,7 @@ import { DatabaseReads } from '../database/prisma'; import * as Types from './types'; import { env } from '$env/dynamic/private'; import { activeSystems } from '$lib/organizations/server'; +import { stringifyError } from '$lib/utils'; const tracer = trace.getTracer('build-engine-api'); @@ -97,7 +98,7 @@ export async function request(resource: string, auth: Types.Auth, opts?: Types.R name: '', status: 500, code: 500, - message: typeof e === 'string' ? e.toUpperCase() : e instanceof Error ? e.message : e, + message: stringifyError(e), type: '' } as Types.ErrorResponse }; diff --git a/src/lib/server/bullmq/BullWorker.ts b/src/lib/server/bullmq/BullWorker.ts index 96714e2f8e..6a50a76bd3 100644 --- a/src/lib/server/bullmq/BullWorker.ts +++ b/src/lib/server/bullmq/BullWorker.ts @@ -6,6 +6,7 @@ import { getQueues, getWorkerConfig } from './queues'; import * as BullMQ from './types'; import { building } from '$app/environment'; import { SSEPageUpdates } from '$lib/projects/listener'; +import { logLocalDev } from '$lib/utils/server'; const tracer = trace.getTracer('BullWorker'); @@ -41,7 +42,7 @@ export abstract class BullWorker { code: SpanStatusCode.ERROR, // Error message: (error as Error).message }); - console.error(error); + logLocalDev?.(error); throw error; } finally { span.end(); diff --git a/src/lib/server/bullmq/queues.ts b/src/lib/server/bullmq/queues.ts index ca520cf2be..2dd715e41c 100644 --- a/src/lib/server/bullmq/queues.ts +++ b/src/lib/server/bullmq/queues.ts @@ -19,13 +19,15 @@ import type { } from './types'; import { QueueName } from './types'; import OTEL from '$lib/otel'; +import { stringifyError } from '$lib/utils'; +import { inLocalDevelopment, logLocalDev } from '$lib/utils/server'; class Connection { private conn: Redis; private connected: boolean; constructor(isQueueConnection = false, keyPrefix?: string) { this.conn = new Redis({ - host: process.env.NODE_ENV === 'development' ? 'localhost' : process.env.VALKEY_HOST, + host: inLocalDevelopment ? 'localhost' : process.env.VALKEY_HOST, maxRetriesPerRequest: isQueueConnection ? undefined : null, keyPrefix }); @@ -49,10 +51,10 @@ class Connection { }); this.connected = false; if (err.message.includes('ENOTFOUND')) { - console.error('Fatal Valkey connection', err); + logLocalDev?.('Fatal Valkey connection', err); process.exit(1); } else if (!err.message.includes('ECONNREFUSED')) { - console.error('Valkey connection error', err); + logLocalDev?.('Valkey connection error', err); } }); setInterval(() => { @@ -64,8 +66,8 @@ class Connection { }) .catch((err) => { if (this.connected) { - console.error(err); - console.log('Valkey disconnected'); + logLocalDev?.(err); + logLocalDev?.('Valkey disconnected'); this.connected = false; OTEL.instance.logger.error('Valkey disconnected', { error: err.message, @@ -149,7 +151,8 @@ async function createJobRecord(job: Job) { } if (!found) { job.log('Error recovering transition. No job record created.'); - console.error(`Error recovering transition ${job.data.transition}`); + logLocalDev?.(`Error recovering transition ${job.data.transition}`); + OTEL.instance.logger.error(`Error recovering transition ${job.data.transition}`); return; } else { job.log(`Transition ${job.data.transition} not found. Replacing with ${found}`); @@ -171,7 +174,8 @@ async function createJobRecord(job: Job) { } } catch (e) { job.log(`Error creating job records: ${e}`); - console.error(e); + logLocalDev?.(e); + OTEL.instance.logger.error(`Error creating job records`, { error: stringifyError(e) }); } } diff --git a/src/lib/server/database/Products.ts b/src/lib/server/database/Products.ts index d7118ed8c7..97603d9105 100644 --- a/src/lib/server/database/Products.ts +++ b/src/lib/server/database/Products.ts @@ -4,6 +4,7 @@ import { BullMQ, getQueues } from '../bullmq/index'; import { delete as deleteInstance } from './WorkflowInstances'; import prisma from './prisma'; import type { RequirePrimitive } from './utility'; +import { logLocalDev } from '$lib/utils/server'; import { WorkflowState } from '$lib/workflowTypes'; export async function create( @@ -290,9 +291,7 @@ async function validateProductBase( 'product.product-definition-allowed': productInOrg, 'product.project-type-allowed': projectTypeAllowed }; - if (process.env.NODE_ENV === 'development') { - console.log(log); - } + logLocalDev?.(log); span.addEvent(msg, log); span.recordException(new Error(msg)); diff --git a/src/lib/server/database/prisma.ts b/src/lib/server/database/prisma.ts index 5e6b9d9bc5..9154a79671 100644 --- a/src/lib/server/database/prisma.ts +++ b/src/lib/server/database/prisma.ts @@ -1,6 +1,7 @@ import { Prisma, PrismaClient } from '@prisma/client'; import { ReadonlyClient } from './ReadonlyPrisma'; import OTEL from '$lib/otel'; +import { logLocalDev } from '$lib/utils/server'; // This is the home of all database operations through prisma // It is used from both the node-server package (which runs tasks) and from the sveltekit @@ -48,7 +49,7 @@ class ConnectionChecker { error: e.message }); this.connected = false; - console.log('Error checking database connection:', e); + logLocalDev?.('Error checking database connection:', e); } } else { throw e; diff --git a/src/lib/server/email-service/EmailClient.ts b/src/lib/server/email-service/EmailClient.ts index daa0f546ba..2e739af0c7 100644 --- a/src/lib/server/email-service/EmailClient.ts +++ b/src/lib/server/email-service/EmailClient.ts @@ -10,10 +10,9 @@ import { } from './EmailTemplates'; import { building } from '$app/environment'; import { RoleId } from '$lib/prisma'; +import { inLocalDevelopment } from '$lib/utils/server'; -const EMAIL_NAME = - process.env.ADMIN_NAME ?? - 'Scriptoria' + (process.env.NODE_ENV === 'development' ? ' Staging' : ''); +const EMAIL_NAME = process.env.ADMIN_NAME ?? 'Scriptoria' + (inLocalDevelopment ? ' Staging' : ''); const ADMIN_EMAIL = process.env.ADMIN_EMAIL || ''; let transporter: Transporter | null = null; if (!building) { diff --git a/src/lib/server/job-executors/email.ts b/src/lib/server/job-executors/email.ts index 2ae7a53b86..1a3e6cdcca 100644 --- a/src/lib/server/job-executors/email.ts +++ b/src/lib/server/job-executors/email.ts @@ -16,6 +16,7 @@ import { activeSystems } from '$lib/organizations/server'; import { RoleId } from '$lib/prisma'; import type { ProjectImportJSON } from '$lib/projects'; import { NotificationType } from '$lib/users'; +import { inLocalDevelopment } from '$lib/utils/server'; export async function inviteUser(job: Job): Promise { const inviteInformation = await DatabaseReads.organizationMembershipInvites.findFirstOrThrow({ @@ -238,7 +239,7 @@ export async function notifySuperAdminsOfOfflineSystems( await getQueues().Emails.removeJobScheduler(BullMQ.JobSchedulerId.SystemStatusEmail); return; } - if (process.env.NODE_ENV === 'development') { + if (inLocalDevelopment) { console.log( 'Not notifying super admins of offline systems - ', statuses.map((s) => s.BuildEngineUrl).join(', ') diff --git a/src/lib/server/job-executors/product.ts b/src/lib/server/job-executors/product.ts index 0c2f85924c..88691d2068 100644 --- a/src/lib/server/job-executors/product.ts +++ b/src/lib/server/job-executors/product.ts @@ -4,6 +4,7 @@ import { BullMQ, getQueues } from '../bullmq'; import { DatabaseReads, DatabaseWrites } from '../database'; import { Workflow } from '../workflow'; import { NotificationType } from '$lib/users'; +import { stringifyError } from '$lib/utils'; import type { WorkflowInstanceContext } from '$lib/workflowTypes'; import { ENVKeys, WorkflowAction } from '$lib/workflowTypes'; @@ -268,7 +269,7 @@ export async function createLocal(job: Job): Promise return productId; } catch (err) { - await job.log(err instanceof Error ? err.message : String(err)); + await job.log(stringifyError(err)); return false; } } diff --git a/src/lib/server/workflow/dbProcedures.ts b/src/lib/server/workflow/dbProcedures.ts index 320918581f..2f05bdea2d 100644 --- a/src/lib/server/workflow/dbProcedures.ts +++ b/src/lib/server/workflow/dbProcedures.ts @@ -3,6 +3,7 @@ import { ProductTransitionType } from '../../prisma'; import { BullMQ, getQueues } from '../bullmq'; import { DatabaseWrites } from '../database'; import { DatabaseReads } from '../database/prisma'; +import { stringifyError } from '$lib/utils'; import type { WorkflowState } from '$lib/workflowTypes'; export async function deleteWorkflow(productId: string, status: WorkflowState) { @@ -64,7 +65,7 @@ export async function markResolved(productId: string) { }); } } catch (err) { - const exception = err instanceof Error ? err : new Error(String(err)); + const exception = stringifyError(err); const span = trace.getActiveSpan(); span?.recordException(exception); span?.setStatus({ @@ -103,7 +104,7 @@ export async function notifyAutoPublishOwner(productId: string) { } }); } catch (err) { - const exception = err instanceof Error ? err : new Error(String(err)); + const exception = stringifyError(err); const span = trace.getActiveSpan(); span?.recordException(exception); span?.setStatus({ diff --git a/src/lib/turnstile/server.ts b/src/lib/turnstile/server.ts index 590ba7b4fd..c65f7f4598 100644 --- a/src/lib/turnstile/server.ts +++ b/src/lib/turnstile/server.ts @@ -1,4 +1,5 @@ import { SpanStatusCode, trace } from '@opentelemetry/api'; +import { logLocalDev } from '$lib/utils/server'; const tracer = trace.getTracer('TurnstileVerification'); @@ -25,6 +26,7 @@ export async function verifyToken(token: string, secret: string | undefined) { code: SpanStatusCode.ERROR, message: 'Turnstile secret key is not configured' }); + logLocalDev?.('Turnstile secret key is not configured'); return 500; } @@ -41,7 +43,7 @@ export async function verifyToken(token: string, secret: string | undefined) { code: SpanStatusCode.ERROR, message: (e as Error).message }); - console.warn('Turnstile verification request failed', { error: e }); + logLocalDev?.('Turnstile verification request failed', { error: e }); return 503; } @@ -51,14 +53,14 @@ export async function verifyToken(token: string, secret: string | undefined) { code: SpanStatusCode.ERROR, message: `Turnstile verification returned an invalid response: ${JSON.stringify(result)}` }); - console.warn('Turnstile verification returned an invalid response', { + logLocalDev?.('Turnstile verification returned an invalid response', { status: verification.status }); return 502; } if (!result.success) { - console.warn('Turnstile verification failed', { + logLocalDev?.('Turnstile verification failed', { errorCodes: result['error-codes'], hostname: result.hostname, action: result.action @@ -73,7 +75,7 @@ export async function verifyToken(token: string, secret: string | undefined) { code: SpanStatusCode.ERROR, message: (e as Error).message }); - console.warn('Turnstile verification request failed', { error: e }); + logLocalDev?.('Turnstile verification request failed', { error: e }); return 500; } finally { span.end(); diff --git a/src/lib/utils/index.ts b/src/lib/utils/index.ts index 2682f9a7fd..865bef046a 100644 --- a/src/lib/utils/index.ts +++ b/src/lib/utils/index.ts @@ -98,3 +98,7 @@ export type ValueKey = { params?: Parameters<(typeof m)[T]>[0]; class?: ClassValue; }; + +export function stringifyError(err: unknown) { + return err instanceof Error ? err.message : String(err); +} diff --git a/src/lib/utils/server.ts b/src/lib/utils/server.ts new file mode 100644 index 0000000000..8791677fc9 --- /dev/null +++ b/src/lib/utils/server.ts @@ -0,0 +1,3 @@ +export const inLocalDevelopment = process.env.NODE_ENV === 'development'; + +export const logLocalDev = inLocalDevelopment ? console.log : undefined; diff --git a/src/routes/(authenticated)/+layout.server.ts b/src/routes/(authenticated)/+layout.server.ts index 14c425f9b8..448be34ee1 100644 --- a/src/routes/(authenticated)/+layout.server.ts +++ b/src/routes/(authenticated)/+layout.server.ts @@ -5,11 +5,13 @@ import { array, safeParse } from 'valibot'; import type { LayoutServerLoad } from './$types'; import { langtagSchema } from '$lib/ldml'; import { readLDML } from '$lib/ldml/server'; +import OTEL from '$lib/otel'; import { locales } from '$lib/paraglide/runtime'; import { getUserTasks } from '$lib/projects/sse'; import { QueueConnected } from '$lib/server/bullmq/queues'; import { DatabaseReads } from '$lib/server/database'; import { getSiteParam } from '$lib/site-params/server'; +import { logLocalDev } from '$lib/utils/server'; export const load: LayoutServerLoad = async (event) => { event.locals.security.requireAuthenticated(); @@ -42,7 +44,8 @@ export const load: LayoutServerLoad = async (event) => { return res.success ? res.output : []; }) .catch((r) => { - console.error(r); + OTEL.instance.logger.error(r); + logLocalDev?.(r); return []; }), l10nMap: await readLDML(localDir, locales), diff --git a/src/routes/(authenticated)/projects/[id=number]/sse/+server.ts b/src/routes/(authenticated)/projects/[id=number]/sse/+server.ts index a788e062dd..ec14289f77 100644 --- a/src/routes/(authenticated)/projects/[id=number]/sse/+server.ts +++ b/src/routes/(authenticated)/projects/[id=number]/sse/+server.ts @@ -31,7 +31,6 @@ export async function POST(request) { // This is a little wasteful because it will calculate much of the same data // multiple times if multiple users are connected to the same project page. if (updateId.includes(id)) { - // console.log(`Project page SSE update for project ${id}`); const projectData = await getProjectDetails(id, request.locals.security.sessionForm); const { error } = emit('projectData', stringify(projectData)); if (error) { diff --git a/src/routes/(authenticated)/software-update/[[orgId=number]]/sse/products/+server.ts b/src/routes/(authenticated)/software-update/[[orgId=number]]/sse/products/+server.ts index 24b4a6fc75..6efd5d2d2c 100644 --- a/src/routes/(authenticated)/software-update/[[orgId=number]]/sse/products/+server.ts +++ b/src/routes/(authenticated)/software-update/[[orgId=number]]/sse/products/+server.ts @@ -1,7 +1,10 @@ import { stringify } from 'devalue'; import { produce } from 'sveltekit-sse'; +import OTEL from '$lib/otel/index.js'; import { SSEPageUpdates } from '$lib/projects/listener'; import { getProducts } from '$lib/software-updates/server'; +import { stringifyError } from '$lib/utils/index.js'; +import { logLocalDev } from '$lib/utils/server.js'; // Handle POST requests to establish an SSE connection for products data export async function POST({ locals, params }) { @@ -32,7 +35,8 @@ export async function POST({ locals, params }) { } } } catch (err) { - console.error('Error in software-update SSE updateCb:', err); + OTEL.instance.logger.error(stringifyError(err)); + logLocalDev?.('Error in software-update SSE updateCb:', err); SSEPageUpdates.off('updatableProducts', updateCb); clearInterval(pingInterval); emit('error', stringify({ message: 'Failed to fetch updatable products' })); diff --git a/src/routes/(authenticated)/software-update/[[orgId=number]]/sse/updates/+server.ts b/src/routes/(authenticated)/software-update/[[orgId=number]]/sse/updates/+server.ts index 251235a80c..ba176c4c5f 100644 --- a/src/routes/(authenticated)/software-update/[[orgId=number]]/sse/updates/+server.ts +++ b/src/routes/(authenticated)/software-update/[[orgId=number]]/sse/updates/+server.ts @@ -1,7 +1,10 @@ import { stringify } from 'devalue'; import { produce } from 'sveltekit-sse'; +import OTEL from '$lib/otel/index.js'; import { SSEPageUpdates } from '$lib/projects/listener'; import { getUpdates } from '$lib/software-updates/server'; +import { stringifyError } from '$lib/utils/index.js'; +import { logLocalDev } from '$lib/utils/server.js'; // Handle POST requests to establish an SSE connection for update data export async function POST({ locals, params }) { @@ -32,7 +35,8 @@ export async function POST({ locals, params }) { } } } catch (err) { - console.error('Error in software-update SSE updateCb:', err); + OTEL.instance.logger.error(stringifyError(err)); + logLocalDev?.('Error in software-update SSE updateCb:', err); SSEPageUpdates.off('softwareUpdates', updateCb); clearInterval(pingInterval); emit('error', stringify({ message: 'Failed to fetch software updates' })); diff --git a/src/routes/(unauthenticated)/(google-play)/user-data/[productId=uuid]/+page.server.ts b/src/routes/(unauthenticated)/(google-play)/user-data/[productId=uuid]/+page.server.ts index 76d9742951..6b380ac2d3 100644 --- a/src/routes/(unauthenticated)/(google-play)/user-data/[productId=uuid]/+page.server.ts +++ b/src/routes/(unauthenticated)/(google-play)/user-data/[productId=uuid]/+page.server.ts @@ -13,6 +13,7 @@ import { saveDeleteRequestVerificationCode } from '$lib/google-play/server'; import { DatabaseWrites } from '$lib/server/database'; import { sendEmail } from '$lib/server/email-service/EmailClient'; import { resolveToken, verifyToken } from '$lib/turnstile/server'; +import { logLocalDev } from '$lib/utils/server'; const tracer = trace.getTracer('UDMRequests'); @@ -88,7 +89,7 @@ export const actions: Actions = { code: SpanStatusCode.ERROR, message: (e as Error).message }); - console.error(e); + logLocalDev?.(e); return message( form, { error: m.alert_verification_failed({}, { locale }) }, @@ -117,7 +118,7 @@ export const actions: Actions = { DateExpires: new Date() } }); - console.error(e); + logLocalDev?.(e); return message( form, { error: m.alert_verification_failed({}, { locale }) }, @@ -132,7 +133,7 @@ export const actions: Actions = { code: SpanStatusCode.ERROR, message: (e as Error).message }); - console.error(e); + logLocalDev?.(e); return error(500); } finally { span.end(); diff --git a/src/routes/(unauthenticated)/docs/[filename]/+server.ts b/src/routes/(unauthenticated)/docs/[filename]/+server.ts index 25aaa74db7..8953b234be 100644 --- a/src/routes/(unauthenticated)/docs/[filename]/+server.ts +++ b/src/routes/(unauthenticated)/docs/[filename]/+server.ts @@ -2,6 +2,9 @@ import { error } from '@sveltejs/kit'; import { readFile } from 'fs/promises'; import { join } from 'path'; import type { RequestEvent } from './$types'; +import OTEL from '$lib/otel'; +import { stringifyError } from '$lib/utils'; +import { logLocalDev } from '$lib/utils/server'; export async function GET({ params, locals }: RequestEvent) { locals.security.requireNothing(); @@ -32,7 +35,8 @@ export async function GET({ params, locals }: RequestEvent) { } }); } catch (err) { - console.error('Error reading PDF:', err); + OTEL.instance.logger.error(stringifyError(err)); + logLocalDev?.('Error reading PDF:', err); throw error(404, 'PDF not found'); } } From 6947c02d0faf44684474fe12174fe2f7d2eb193a Mon Sep 17 00:00:00 2001 From: Aidan Jones Date: Fri, 28 Aug 2026 13:46:31 -0500 Subject: [PATCH 6/8] Revamp org invite request - Use turnstile to block bots - Verify website exists - Use verification code to verify email address --- README.md | 3 + .../components/settings/SubmitButton.svelte | 21 ++- src/lib/locales/en-US.json | 2 + src/lib/locales/es-419.json | 3 +- src/lib/locales/fr-FR.json | 2 + .../+page.server.ts | 100 +++++++--- .../+page.svelte | 175 +++++++++++++++--- .../verify/[requestId=uuid]/+page.server.ts | 97 ++++++++++ .../verify/[requestId=uuid]/+page.svelte | 96 ++++++++++ 9 files changed, 442 insertions(+), 57 deletions(-) create mode 100644 src/routes/(unauthenticated)/request-access-for-organization/verify/[requestId=uuid]/+page.server.ts create mode 100644 src/routes/(unauthenticated)/request-access-for-organization/verify/[requestId=uuid]/+page.svelte diff --git a/README.md b/README.md index d877dce751..87ca1f5762 100644 --- a/README.md +++ b/README.md @@ -46,6 +46,9 @@ ADMIN_EMAIL= USER_DATA_TURNSTILE_SECRET_KEY= PUBLIC_USER_DATA_TURNSTILE_SITEKEY= + +ORG_REQUEST_TURNSTILE_SECRET_KEY= +PUBLIC_ORG_REQUEST_TURNSTILE_SITEKEY= ``` > **Note:** Contact [@sillsdev/scriptoria-developers](https://github.com/orgs/sillsdev/teams/scriptoria-developers/members) for help obtaining the secret values. diff --git a/src/lib/components/settings/SubmitButton.svelte b/src/lib/components/settings/SubmitButton.svelte index c415e1845e..1f5fa4df4d 100644 --- a/src/lib/components/settings/SubmitButton.svelte +++ b/src/lib/components/settings/SubmitButton.svelte @@ -2,14 +2,31 @@ import IconButton, { type IconButtonProps } from './IconButton.svelte'; import { Icons } from '$lib/icons'; + interface Props extends IconButtonProps { + waiting?: boolean; + } + let { class: classes, icon = Icons.Save, disabled = false, key = 'common_save', + waiting = false, // eslint-disable-next-line svelte/valid-compile ...rest - }: IconButtonProps = $props(); + }: Props = $props(); - + + +{#snippet loading()} + +{/snippet} diff --git a/src/lib/locales/en-US.json b/src/lib/locales/en-US.json index af74832ae2..cd010ac170 100644 --- a/src/lib/locales/en-US.json +++ b/src/lib/locales/en-US.json @@ -112,6 +112,7 @@ "common_or": "or", "common_passThrough": "{value}", "common_type": "Type", + "common_expires": "Expires", "models_add": "Add {name}", "models_edit": "Edit {name}", "models_save": "Save {name}", @@ -166,6 +167,7 @@ "invitations_orgAdminEmail": "Organization Admin Email", "invitations_ourUsers": "Our Users", "invitations_verifyUser": "Before submitting a request, please verify that your organization is not already a user of Scriptoria.", + "invitations_verifyWebsite": "Scriptoria ran into an issue verifying that your website exists.", "newOrganization_title": "Add organization", "org_title": "Organizations", "org_add": "Add Organization", diff --git a/src/lib/locales/es-419.json b/src/lib/locales/es-419.json index 29e6fd6305..ab6a0183f7 100644 --- a/src/lib/locales/es-419.json +++ b/src/lib/locales/es-419.json @@ -112,6 +112,7 @@ "common_or": "o", "common_passThrough": "{value}", "common_type": "Tipo", + "common_expires": "Vence", "models_add": "Añadir {name}", "models_edit": "Editar {name}", "models_save": "Guardar {name}", @@ -166,7 +167,7 @@ "invitations_orgAdminEmail": "Correo electrónico del administrador", "invitations_ourUsers": "Nuestras Usuarias", "invitations_verifyUser": "Antes de enviar una solicitud, por favor verifique que su organización no sea ya usuaria de Scriptoria.", - "newOrganization_title": "Agregar organización", + "invitations_verifyWebsite": "Scriptoria tuvo problemas para verificar que tu sitio web existe.", "org_title": "Organizaciones", "org_add": "Añadir organización", "org_addSuccess": "Organización añadida", diff --git a/src/lib/locales/fr-FR.json b/src/lib/locales/fr-FR.json index ecd36c8779..3146ead771 100644 --- a/src/lib/locales/fr-FR.json +++ b/src/lib/locales/fr-FR.json @@ -112,6 +112,7 @@ "common_or": "o", "common_passThrough": "{value}", "common_type": "Type", + "common_expires": "Expires", "models_add": "Add {name}", "models_edit": "Edit {name}", "models_save": "Save {name}", @@ -166,6 +167,7 @@ "invitations_orgAdminEmail": "Organization Admin Email", "invitations_ourUsers": "Our Users", "invitations_verifyUser": "Before submitting a request, please verify that your organization is not already a user of Scriptoria.", + "invitations_verifyWebsite": "Scriptoria ran into an issue verifying that your website exists.", "newOrganization_title": "Add organization", "org_title": "Organizations", "org_add": "Add Organization", diff --git a/src/routes/(unauthenticated)/request-access-for-organization/+page.server.ts b/src/routes/(unauthenticated)/request-access-for-organization/+page.server.ts index 1a69178fa2..703f16e79b 100644 --- a/src/routes/(unauthenticated)/request-access-for-organization/+page.server.ts +++ b/src/routes/(unauthenticated)/request-access-for-organization/+page.server.ts @@ -1,40 +1,92 @@ +import { trace } from '@opentelemetry/api'; +import { error, redirect } from '@sveltejs/kit'; +import { randomInt, randomUUID } from 'node:crypto'; import { fail, superValidate } from 'sveltekit-superforms'; import { valibot } from 'sveltekit-superforms/adapters'; import * as v from 'valibot'; import type { Actions, PageServerLoad } from './$types'; -import { BullMQ, getQueues } from '$lib/server/bullmq'; +import { env } from '$env/dynamic/private'; +import { m as gp } from '$lib/google-play/paraglide/messages'; +import { type Locale, localizeHref } from '$lib/paraglide/runtime'; +import { getAuthConnection } from '$lib/server/bullmq/queues'; import { DatabaseReads } from '$lib/server/database'; +import { sendEmail } from '$lib/server/email-service/EmailClient'; +import { resolveToken, verifyToken } from '$lib/turnstile/server'; -const requestSchema = v.object({ - organizationName: v.pipe(v.string(), v.nonEmpty()), - email: v.pipe(v.string(), v.nonEmpty(), v.email()), - url: v.pipe(v.string(), v.nonEmpty()) +const tracer = trace.getTracer('OrgInviteRequest'); + +const requestSchema = v.objectAsync({ + organizationName: v.pipe(v.string(), v.trim(), v.nonEmpty()), + email: v.pipe(v.string(), v.trim(), v.nonEmpty(), v.email()), + url: v.pipeAsync(v.string(), v.trim(), v.nonEmpty(), v.url()), + turnstileToken: v.pipe(v.string(), v.trim(), v.nonEmpty()) }); export const load = (async ({ locals }) => { locals.security.requireNothing(); - - return { - publicOrgExists: !!(await DatabaseReads.organizations.findFirst({ - where: { VisibleToPublic: true }, - select: { - Id: true - } - })) - }; + return tracer.startActiveSpan('Org Invite - Load Request Page', async (span) => { + try { + return { + publicOrgExists: !!(await DatabaseReads.organizations.findFirst({ + where: { VisibleToPublic: true }, + select: { + Id: true + } + })), + form: await superValidate(valibot(requestSchema)) + }; + } finally { + span.end(); + } + }); }) satisfies PageServerLoad; export const actions = { - async request(event) { - event.locals.security.requireNothing(); - const form = await superValidate(event.request, valibot(requestSchema)); - if (!form.valid) return fail(400, { form, ok: false }); - await getQueues().Emails.add('Email SuperAdmins about new org ' + form.data.organizationName, { - type: BullMQ.JobType.Email_NotifySuperAdminsOfNewOrganizationRequest, - email: form.data.email, - organizationName: form.data.organizationName, - url: form.data.url + async request({ locals, request }) { + locals.security.requireNothing(); + return tracer.startActiveSpan('Org Invite - Process Request', async (span) => { + try { + const formData = await request.formData(); + resolveToken(formData); + const form = await superValidate(formData, valibot(requestSchema)); + console.log(form); + if (!form.valid) return fail(400, { form, ok: false }); + + const verifyResult = await verifyToken( + form.data.turnstileToken, + env.ORG_REQUEST_TURNSTILE_SECRET_KEY + ); + + if (verifyResult !== 200) { + // logging handled in verifyToken + form.data.turnstileToken = ''; + return fail(verifyResult, { form, ok: false }); + } + + // code to use for exchange + const requestId = randomUUID(); + const code = randomInt(100_000, 1_000_000).toString(); + + try { + await getAuthConnection().set( + `org-invite:${requestId}`, + JSON.stringify({ ...form.data, code }), + 'EX', + 600 + ); // 10 minute (600 s) TTL + + await sendEmail( + [{ email: form.data.email, name: form.data.organizationName }], + gp.email_subject({}, { locale: locals.locale as Locale }), + gp.email_body({ code }, { locale: locals.locale as Locale }) + ); + } catch { + error(500, 'Failed to generate request email'); + } + redirect(303, localizeHref(`/request-access-for-organization/verify/${requestId}`)); + } finally { + span.end(); + } }); - return { form, ok: true }; } } satisfies Actions; diff --git a/src/routes/(unauthenticated)/request-access-for-organization/+page.svelte b/src/routes/(unauthenticated)/request-access-for-organization/+page.svelte index 43a00244aa..f2ee1ee190 100644 --- a/src/routes/(unauthenticated)/request-access-for-organization/+page.svelte +++ b/src/routes/(unauthenticated)/request-access-for-organization/+page.svelte @@ -1,13 +1,16 @@ - { - return async ({ result }) => { - if (result.type === 'success') { - const data = result.data; - if (data?.ok) { - goto(localizeHref('/request-access-for-organization/success')); - } - } else { + let submitAttempted = $state(false); + + let websiteVerified: 'empty' | 'pending' | 'verified' | 'unreachable' = $state('empty'); + + const { form, enhance, delayed } = superForm(data.form, { + invalidateAll: false, + resetForm: false, + onSubmit: ({ cancel, formData }) => { + submitAttempted = true; + + $form.turnstileToken ||= resolveToken(formData) || ''; + + if (!$form.turnstileToken) { + cancel(); + return; + } + + formData.set('turnstileToken', $form.turnstileToken); + }, + onUpdate: ({ result }) => { + const resultData = result.data as ActionData; + console.log(resultData); + if (!resultData?.ok || !resultData?.form.data.turnstileToken) { + window.turnstile?.reset?.(); + $form.turnstileToken = ''; + toast('error', m.errors_generic({ errorMessage: '' })); } - }; - }} -> + } + }); + + onMount(() => + initTurnstile( + '#turnstile-container', + env.PUBLIC_ORG_REQUEST_TURNSTILE_SITEKEY, + (token: string) => { + $form.turnstileToken = token; + } + ) + ); + + $inspect($form); + + + + + + +
-

{m.invitations_requestOrgInvite()}

+

{m.invitations_requestOrgInvite()}

{#if data.publicOrgExists}

{m.invitations_verifyUser()} @@ -44,19 +83,95 @@

{/if} - - - -
- - - -
+ + - + + + { + websiteVerified = 'empty'; + e.currentTarget.setCustomValidity(''); + }} + onchange={(e) => { + const input = e.currentTarget; + let url = ''; + try { + url = new URL($form.url).toString(); + } catch { + // empty + websiteVerified = 'empty'; + } + + if (url) { + websiteVerified = 'pending'; + fetch(url, { mode: 'no-cors' }) + .then(() => { + websiteVerified = 'verified'; + input.setCustomValidity(''); + }) + .catch(() => { + websiteVerified = 'unreachable'; + input.setCustomValidity(m.invitations_verifyWebsite()); + }); + } + }} + /> + {#if websiteVerified === 'pending'} + + {:else if websiteVerified === 'verified'} + + {:else if websiteVerified === 'unreachable'} + + {/if} + + + {$form.url + ? m.invitations_verifyWebsite() + : m.errors_requiredField({ field: m.invitations_orgUrl() })} +
+
+
+
+ {#if submitAttempted && !$form.turnstileToken} + + {gp.alert_verify_human({}, { locale: getLocale() })} + + {/if}
- +
diff --git a/src/routes/(unauthenticated)/request-access-for-organization/verify/[requestId=uuid]/+page.server.ts b/src/routes/(unauthenticated)/request-access-for-organization/verify/[requestId=uuid]/+page.server.ts new file mode 100644 index 0000000000..65611eda52 --- /dev/null +++ b/src/routes/(unauthenticated)/request-access-for-organization/verify/[requestId=uuid]/+page.server.ts @@ -0,0 +1,97 @@ +import { SpanStatusCode, trace } from '@opentelemetry/api'; +import { redirect } from '@sveltejs/kit'; +import { fail, superValidate } from 'sveltekit-superforms'; +import { valibot } from 'sveltekit-superforms/adapters'; +import * as v from 'valibot'; +import type { Actions, PageServerLoad } from './$types'; +import { localizeHref } from '$lib/paraglide/runtime'; +import { BullMQ, getQueues } from '$lib/server/bullmq'; +import { getAuthConnection } from '$lib/server/bullmq/queues'; + +const tracer = trace.getTracer('OrgInviteRequest'); + +const codeSchema = v.object({ code: v.pipe(v.string(), v.trim(), v.digits(), v.length(6)) }); + +const requestSchema = v.nullable( + v.pipe( + v.string(), + v.parseJson(), + v.object({ + organizationName: v.pipe(v.string(), v.trim(), v.nonEmpty()), + email: v.pipe(v.string(), v.trim(), v.nonEmpty(), v.email()), + url: v.pipe(v.string(), v.trim(), v.nonEmpty(), v.url()), + code: codeSchema.entries.code + }) + ) +); + +export const load = (async ({ locals, params }) => { + locals.security.requireNothing(); + return tracer.startActiveSpan('Org Invite - Load Verification page', async (span) => { + span.setAttribute('org-invite.request-id', params.requestId); + try { + const key = `org-invite:${params.requestId}`; + const request = v.safeParse(requestSchema, await getAuthConnection().get(key)); + + if (request.success && request.output) { + return { + email: request.output.email, + ttl: await getAuthConnection().ttl(key), + form: await superValidate(valibot(codeSchema)) + }; + } else { + redirect(308, localizeHref(`/request-access-for-organization`)); + } + } finally { + span.end(); + } + }); +}) satisfies PageServerLoad; + +export const actions = { + async verifyCode({ locals, request: eventRequest, params }) { + locals.security.requireNothing(); + return tracer.startActiveSpan('Org Invite - Verify Code', async (span) => { + span.setAttribute('org-invite.request-id', params.requestId); + try { + const key = `org-invite:${params.requestId}`; + + const request = v.safeParse(requestSchema, await getAuthConnection().get(key)); + if (!request.success || !request.output) return fail(404, { ok: false }); + + const form = await superValidate(eventRequest, valibot(codeSchema)); + + if (!form.valid || form.data.code !== request.output.code) { + return fail(400, { form, ok: false, codeMatch: false }); + } + + try { + //immediately invalidate + await getAuthConnection().del(key); + } catch { + /* empty */ + } + + await getQueues().Emails.add( + 'Email SuperAdmins about new org ' + request.output.organizationName, + { + type: BullMQ.JobType.Email_NotifySuperAdminsOfNewOrganizationRequest, + email: request.output.email, + organizationName: request.output.organizationName, + url: request.output.url + } + ); + return { form, ok: true }; + } catch (e) { + span.recordException(e as Error); + span.setStatus({ + code: SpanStatusCode.ERROR, + message: (e as Error).message + }); + return fail(500, { ok: false }); + } finally { + span.end(); + } + }); + } +} satisfies Actions; diff --git a/src/routes/(unauthenticated)/request-access-for-organization/verify/[requestId=uuid]/+page.svelte b/src/routes/(unauthenticated)/request-access-for-organization/verify/[requestId=uuid]/+page.svelte new file mode 100644 index 0000000000..a706171950 --- /dev/null +++ b/src/routes/(unauthenticated)/request-access-for-organization/verify/[requestId=uuid]/+page.svelte @@ -0,0 +1,96 @@ + + +

+ {gp.check_email_description({ email: data.email }, { locale: getLocale() })} +

+ +

{m.common_expires()}: {$expireTime}

+ +
+
+ + + + {gp.error_invalid_code({}, { locale: getLocale() })} + +
+ + + From a03a8414eb858717192a48ba4306a123a88cceff Mon Sep 17 00:00:00 2001 From: Aidan Jones Date: Fri, 28 Aug 2026 13:50:34 -0500 Subject: [PATCH 7/8] Remove unneeded console log --- .../request-access-for-organization/+page.server.ts | 1 - .../request-access-for-organization/+page.svelte | 2 -- 2 files changed, 3 deletions(-) diff --git a/src/routes/(unauthenticated)/request-access-for-organization/+page.server.ts b/src/routes/(unauthenticated)/request-access-for-organization/+page.server.ts index 703f16e79b..219a2c9f99 100644 --- a/src/routes/(unauthenticated)/request-access-for-organization/+page.server.ts +++ b/src/routes/(unauthenticated)/request-access-for-organization/+page.server.ts @@ -49,7 +49,6 @@ export const actions = { const formData = await request.formData(); resolveToken(formData); const form = await superValidate(formData, valibot(requestSchema)); - console.log(form); if (!form.valid) return fail(400, { form, ok: false }); const verifyResult = await verifyToken( diff --git a/src/routes/(unauthenticated)/request-access-for-organization/+page.svelte b/src/routes/(unauthenticated)/request-access-for-organization/+page.svelte index f2ee1ee190..fb75ecc4d8 100644 --- a/src/routes/(unauthenticated)/request-access-for-organization/+page.svelte +++ b/src/routes/(unauthenticated)/request-access-for-organization/+page.svelte @@ -59,8 +59,6 @@ } ) ); - - $inspect($form); From dd4f0882e708f5bd2bb840fdb967a3b98a190598 Mon Sep 17 00:00:00 2001 From: Aidan Jones Date: Fri, 28 Aug 2026 14:33:48 -0500 Subject: [PATCH 8/8] Address CR feedback --- src/lib/components/settings/SubmitButton.svelte | 3 ++- src/lib/locales/es-419.json | 1 + src/lib/server/workflow/dbProcedures.ts | 5 ++--- src/routes/(authenticated)/+layout.server.ts | 3 ++- .../admin/settings/organizations/edit/+page.server.ts | 6 ++++-- .../admin/settings/organizations/edit/+page.svelte | 11 +++++++++++ .../admin/settings/organizations/new/+page.server.ts | 3 ++- .../admin/settings/organizations/new/+page.svelte | 11 +++++++++++ .../request-access-for-organization/+page.svelte | 1 - .../verify/[requestId=uuid]/+page.svelte | 11 +++++------ 10 files changed, 40 insertions(+), 15 deletions(-) diff --git a/src/lib/components/settings/SubmitButton.svelte b/src/lib/components/settings/SubmitButton.svelte index 1f5fa4df4d..a9247b6102 100644 --- a/src/lib/components/settings/SubmitButton.svelte +++ b/src/lib/components/settings/SubmitButton.svelte @@ -12,6 +12,7 @@ disabled = false, key = 'common_save', waiting = false, + children, // eslint-disable-next-line svelte/valid-compile ...rest }: Props = $props(); @@ -24,7 +25,7 @@ {key} disabled={disabled || waiting} {...rest} - children={waiting ? loading : undefined} + children={waiting ? loading : children} /> {#snippet loading()} diff --git a/src/lib/locales/es-419.json b/src/lib/locales/es-419.json index ab6a0183f7..769bb454fd 100644 --- a/src/lib/locales/es-419.json +++ b/src/lib/locales/es-419.json @@ -168,6 +168,7 @@ "invitations_ourUsers": "Nuestras Usuarias", "invitations_verifyUser": "Antes de enviar una solicitud, por favor verifique que su organización no sea ya usuaria de Scriptoria.", "invitations_verifyWebsite": "Scriptoria tuvo problemas para verificar que tu sitio web existe.", + "newOrganization_title": "Agregar organización", "org_title": "Organizaciones", "org_add": "Añadir organización", "org_addSuccess": "Organización añadida", diff --git a/src/lib/server/workflow/dbProcedures.ts b/src/lib/server/workflow/dbProcedures.ts index 2f05bdea2d..320918581f 100644 --- a/src/lib/server/workflow/dbProcedures.ts +++ b/src/lib/server/workflow/dbProcedures.ts @@ -3,7 +3,6 @@ import { ProductTransitionType } from '../../prisma'; import { BullMQ, getQueues } from '../bullmq'; import { DatabaseWrites } from '../database'; import { DatabaseReads } from '../database/prisma'; -import { stringifyError } from '$lib/utils'; import type { WorkflowState } from '$lib/workflowTypes'; export async function deleteWorkflow(productId: string, status: WorkflowState) { @@ -65,7 +64,7 @@ export async function markResolved(productId: string) { }); } } catch (err) { - const exception = stringifyError(err); + const exception = err instanceof Error ? err : new Error(String(err)); const span = trace.getActiveSpan(); span?.recordException(exception); span?.setStatus({ @@ -104,7 +103,7 @@ export async function notifyAutoPublishOwner(productId: string) { } }); } catch (err) { - const exception = stringifyError(err); + const exception = err instanceof Error ? err : new Error(String(err)); const span = trace.getActiveSpan(); span?.recordException(exception); span?.setStatus({ diff --git a/src/routes/(authenticated)/+layout.server.ts b/src/routes/(authenticated)/+layout.server.ts index 448be34ee1..11e3babc46 100644 --- a/src/routes/(authenticated)/+layout.server.ts +++ b/src/routes/(authenticated)/+layout.server.ts @@ -11,6 +11,7 @@ import { getUserTasks } from '$lib/projects/sse'; import { QueueConnected } from '$lib/server/bullmq/queues'; import { DatabaseReads } from '$lib/server/database'; import { getSiteParam } from '$lib/site-params/server'; +import { stringifyError } from '$lib/utils'; import { logLocalDev } from '$lib/utils/server'; export const load: LayoutServerLoad = async (event) => { @@ -44,7 +45,7 @@ export const load: LayoutServerLoad = async (event) => { return res.success ? res.output : []; }) .catch((r) => { - OTEL.instance.logger.error(r); + OTEL.instance.logger.error(stringifyError(r)); logLocalDev?.(r); return []; }), diff --git a/src/routes/(authenticated)/admin/settings/organizations/edit/+page.server.ts b/src/routes/(authenticated)/admin/settings/organizations/edit/+page.server.ts index 92cccb5f6d..5df53f2810 100644 --- a/src/routes/(authenticated)/admin/settings/organizations/edit/+page.server.ts +++ b/src/routes/(authenticated)/admin/settings/organizations/edit/+page.server.ts @@ -44,7 +44,8 @@ export const load = (async ({ url, locals }) => { buildEngineApiAccessToken: data.BuildEngineApiAccessToken, logoUrl: data.LogoUrl, useDefaultBuildEngine: data.UseDefaultBuildEngine, - publicByDefault: data.PublicByDefault ?? false + publicByDefault: data.PublicByDefault ?? false, + visibleToPublic: data.VisibleToPublic ?? false }, valibot(editSchema) ), @@ -81,7 +82,8 @@ export const actions = { ContactEmail: form.data.contact, PublicByDefault: form.data.publicByDefault, UseDefaultBuildEngine: form.data.useDefaultBuildEngine, - WebsiteUrl: form.data.websiteUrl + WebsiteUrl: form.data.websiteUrl, + VisibleToPublic: form.data.visibleToPublic }); return { ok: true, form }; diff --git a/src/routes/(authenticated)/admin/settings/organizations/edit/+page.svelte b/src/routes/(authenticated)/admin/settings/organizations/edit/+page.svelte index 355e2f5974..8c593b82f2 100644 --- a/src/routes/(authenticated)/admin/settings/organizations/edit/+page.svelte +++ b/src/routes/(authenticated)/admin/settings/organizations/edit/+page.svelte @@ -113,6 +113,17 @@ onIcon={Icons.Visible} offIcon={Icons.Invisible} /> +
diff --git a/src/routes/(authenticated)/admin/settings/organizations/new/+page.server.ts b/src/routes/(authenticated)/admin/settings/organizations/new/+page.server.ts index fd6ff21825..b1784539d9 100644 --- a/src/routes/(authenticated)/admin/settings/organizations/new/+page.server.ts +++ b/src/routes/(authenticated)/admin/settings/organizations/new/+page.server.ts @@ -27,7 +27,8 @@ export const actions = { ContactEmail: form.data.contact, PublicByDefault: form.data.publicByDefault, UseDefaultBuildEngine: form.data.useDefaultBuildEngine, - WebsiteUrl: form.data.websiteUrl + WebsiteUrl: form.data.websiteUrl, + VisibleToPublic: form.data.visibleToPublic }); return { ok: true, form }; } diff --git a/src/routes/(authenticated)/admin/settings/organizations/new/+page.svelte b/src/routes/(authenticated)/admin/settings/organizations/new/+page.svelte index fc35f1ad64..5c7519dbd2 100644 --- a/src/routes/(authenticated)/admin/settings/organizations/new/+page.svelte +++ b/src/routes/(authenticated)/admin/settings/organizations/new/+page.svelte @@ -106,6 +106,17 @@ onIcon={Icons.Visible} offIcon={Icons.Invisible} /> +
diff --git a/src/routes/(unauthenticated)/request-access-for-organization/+page.svelte b/src/routes/(unauthenticated)/request-access-for-organization/+page.svelte index fb75ecc4d8..81270775db 100644 --- a/src/routes/(unauthenticated)/request-access-for-organization/+page.svelte +++ b/src/routes/(unauthenticated)/request-access-for-organization/+page.svelte @@ -40,7 +40,6 @@ }, onUpdate: ({ result }) => { const resultData = result.data as ActionData; - console.log(resultData); if (!resultData?.ok || !resultData?.form.data.turnstileToken) { window.turnstile?.reset?.(); $form.turnstileToken = ''; diff --git a/src/routes/(unauthenticated)/request-access-for-organization/verify/[requestId=uuid]/+page.svelte b/src/routes/(unauthenticated)/request-access-for-organization/verify/[requestId=uuid]/+page.svelte index a706171950..f85b10b7d5 100644 --- a/src/routes/(unauthenticated)/request-access-for-organization/verify/[requestId=uuid]/+page.svelte +++ b/src/routes/(unauthenticated)/request-access-for-organization/verify/[requestId=uuid]/+page.svelte @@ -22,7 +22,7 @@ if (val) { return val - 1; } else { - goto(localizeHref(`request-access-for-organization`), { replaceState: true }); + goto(localizeHref(`/request-access-for-organization`), { replaceState: true }); return val; } }); @@ -30,11 +30,9 @@ return () => clearInterval(interval); }); - const expireTime = derived(expiresIn, ($elapsed) => { - const date = new Date(0); - date.setTime($elapsed * 1000); - const minutes = String(date.getMinutes()); - const seconds = String(date.getSeconds()); + const expireTime = derived(expiresIn, (elapsed) => { + const minutes = String(Math.floor(elapsed / 60)); + const seconds = String(elapsed % 60); return `${minutes}:${seconds.length > 1 ? '' : '0'}${seconds}`; }); @@ -79,6 +77,7 @@ autocomplete="one-time-code" required class="input h-16 w-36 validator text-center text-[2rem] mx-auto" + oninput={(e) => e.currentTarget.setCustomValidity('')} /> {gp.error_invalid_code({}, { locale: getLocale() })}