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/(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}
/>
+
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)/(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)/(google-play)/user-data/[productId=uuid]/+page.server.ts b/src/routes/(unauthenticated)/(google-play)/user-data/[productId=uuid]/+page.server.ts
index 93549d6f54..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
@@ -12,11 +12,11 @@ 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';
+import { logLocalDev } from '$lib/utils/server';
const tracer = trace.getTracer('UDMRequests');
-const TURNSTILE_TIMEOUT_MS = 5000;
-
const localizedSchema = (locale: Locale) =>
v.object({
email: localizedEmailSchema(locale),
@@ -46,15 +46,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 +56,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 }
);
}
@@ -149,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 }) },
@@ -178,7 +118,7 @@ export const actions: Actions = {
DateExpires: new Date()
}
});
- console.error(e);
+ logLocalDev?.(e);
return message(
form,
{ error: m.alert_verification_failed({}, { locale }) },
@@ -193,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)/(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() };
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)/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');
}
}
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()}
+
+
+
+
+ | {m.project_org()} |
+ {m.project_orgContact()} |
+
+
+
+ {#each data.organizations.toSorted((a, b) => byName(a, b, getLocale())) as org}
+
+
+ {#if org.LogoUrl}
+
+ {:else}
+
+ {/if}
+
+ {org.Name}
+
+ |
+
+ {#if org.ContactEmail}
+
+ {org.ContactEmail}
+
+ {:else}
+ {m.common_notAvailable()}
+ {/if}
+ |
+
+ {/each}
+
+
+
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..219a2c9f99 100644
--- a/src/routes/(unauthenticated)/request-access-for-organization/+page.server.ts
+++ b/src/routes/(unauthenticated)/request-access-for-organization/+page.server.ts
@@ -1,26 +1,91 @@
+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 } from './$types';
-import { BullMQ, getQueues } from '$lib/server/bullmq';
+import type { Actions, PageServerLoad } from './$types';
+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 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));
+ 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 733fa7ed30..81270775db 100644
--- a/src/routes/(unauthenticated)/request-access-for-organization/+page.svelte
+++ b/src/routes/(unauthenticated)/request-access-for-organization/+page.svelte
@@ -1,44 +1,174 @@
-
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.
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..f85b10b7d5
--- /dev/null
+++ b/src/routes/(unauthenticated)/request-access-for-organization/verify/[requestId=uuid]/+page.svelte
@@ -0,0 +1,95 @@
+
+
+
+ {gp.check_email_description({ email: data.email }, { locale: getLocale() })}
+
+
+
{m.common_expires()}: {$expireTime}
+
+