feat: update payment recap links and add new receipt page

- Changed payment recap link in OrdiniModal to point to the new receipt page.
- Added new PaymentRecapPage for displaying receipt details.
- Updated PaymentsTable to set paid_at date based on payment status.
- Introduced manual payment type in PaymentType and updated related strings.
- Refactored getOrdineRicevutaData to use overridableProcedure for access control.
- Enhanced mail sending functionality to include PDF attachments for receipts and conditions.
- Updated PDF generation service to handle new receipt PDF generation.
- Cleaned up context checking utilities by removing unnecessary session checks.
This commit is contained in:
Marco Pedone 2026-03-04 19:12:01 +01:00
parent 6c9524e54b
commit 280e0ab5c2
21 changed files with 384 additions and 279 deletions

View file

@ -64,8 +64,8 @@ const Base = ({
</Text> </Text>
)} )}
</Section> </Section>
<Section className="mt-3"> <Section className="mt-3 text-center">
<Text className="text-left text-sm"> <Text className="text-sm">
Arcenia S.r.l. Via Beata Giovanna 1, Bassano del Grappa (VI) - Arcenia S.r.l. Via Beata Giovanna 1, Bassano del Grappa (VI) -
tel: 0424529869 tel: 0424529869
</Text> </Text>

View file

@ -1,15 +1,10 @@
import { Button, Heading, Row, Section, Text } from "@react-email/components"; import { Heading, Row, Section, Text } from "@react-email/components";
import Base from "./base"; import Base from "./base";
export type PagamentoConferma_NewMail = { export type PagamentoConferma_NewMail = {
mailType: "pagamentoConferma"; mailType: "pagamentoConferma";
props: PagamentoConfermaProps;
}; };
type PagamentoConfermaProps = { const PagamentoConferma = () => {
receiptHref: string;
};
const PagamentoConferma = ({ receiptHref }: PagamentoConfermaProps) => {
return ( return (
<Base preview="Conferma Pagamento"> <Base preview="Conferma Pagamento">
<> <>
@ -19,20 +14,18 @@ const PagamentoConferma = ({ receiptHref }: PagamentoConfermaProps) => {
<Section className="px-5 text-left text-base md:px-12"> <Section className="px-5 text-left text-base md:px-12">
<Row> <Row>
<Text> <Text>
{/**TODO correggere */}
Gentile Cliente, la informiamo che il pagamento è stato confermato Gentile Cliente, la informiamo che il pagamento è stato confermato
con successo. Procederemo ad attivare i servizi acquistati se non con successo.
lo fossero già. Può controllare lo stato del suo account nella </Text>
sezione dedicata. <Text>
Puoi controllare lo stato del tuo account nella sezione dedicata.
</Text>
<Text>
In allegato troverai una ricevuta di cortesia, la fattura
eletnica arriverà separatamente.
</Text> </Text>
</Row> </Row>
<Section className="mb-5">
<Button
className="mx-auto box-border inline-flex h-10 items-center justify-center whitespace-nowrap rounded-md bg-neutral-100 px-4 py-2 text-center align-middle font-semibold text-neutral-900"
href={receiptHref}
>
Vai al tuo account
</Button>
</Section>
</Section> </Section>
</> </>
</Base> </Base>

View file

@ -0,0 +1,34 @@
import { Heading, Row, Section, Text } from "@react-email/components";
import Base from "./base";
export type ServizioAttivato_NewMail = {
mailType: "servizioAttivato";
};
const ServizioAttivato = () => {
return (
<Base noreply preview="Conferma Servizio Attivato">
<>
<Heading className="text-center text-3xl leading-8">
Conferma Attivazione Servizio
</Heading>
<Section className="px-5 text-left text-base md:px-12">
<Row>
<Text>
Gentile Cliente, ti informiamo che il pagamento è stato confermato
con successo e il servizio è stato attivato.
</Text>
<Text>
Puoi controllare lo stato del tuo account nella sezione dedicata.
</Text>
<Text>
In allegato troverai una ricevuta di cortesia e le condizioni
contrattuali accettate relative al servizio attivato.
</Text>
</Row>
</Section>
</>
</Base>
);
};
export default ServizioAttivato;

View file

@ -10,12 +10,21 @@ import { Card } from "~/components/ui/card";
import { PaymentMethodToString, type PaymentType } from "~/i18n/stripe"; import { PaymentMethodToString, type PaymentType } from "~/i18n/stripe";
import { formatCurrency } from "~/lib/utils"; import { formatCurrency } from "~/lib/utils";
export function ReceiptGenerator({ data }: { data: PurchaseData }) { export function ReceiptGenerator({
data,
raw,
}: {
data: PurchaseData;
raw: boolean;
}) {
const contentRef = useRef<HTMLDivElement>(null); const contentRef = useRef<HTMLDivElement>(null);
const reactToPrintFn = useReactToPrint({ const reactToPrintFn = useReactToPrint({
contentRef, contentRef,
//print: async (target) => {console.log("Printing...", target.contentDocument);}, //print: async (target) => {console.log("Printing...", target.contentDocument);},
}); });
if (raw) {
return <Receipt data={data} />;
}
return ( return (
<div className="mx-auto max-w-4xl"> <div className="mx-auto max-w-4xl">
<div className="space-y-6"> <div className="space-y-6">
@ -27,7 +36,9 @@ export function ReceiptGenerator({ data }: { data: PurchaseData }) {
</Button> </Button>
</div> </div>
<div ref={contentRef}> <div ref={contentRef}>
<Receipt data={data} /> <Card className="p-4 print:border-none print:shadow-none">
<Receipt data={data} />
</Card>
</div> </div>
</div> </div>
</div> </div>
@ -65,123 +76,119 @@ function Receipt({ data }: ReceiptProps) {
0, 0,
); );
const note = const note =
"La presente è non costituisce ricevuta o fattura fiscale.\nRiceverai la fattura elettronica direttamente al tuo indirizzo di posta."; "La presente è non costituisce ricevuta o fattura fiscale ai sensi dell'Art.21, DPR 633/72.\nSeguirà fattura elettronica direttamente al tuo indirizzo di posta.";
const footer = const footer =
"Arcenia Srl. | Tel. +39 0424529869 | Email: arca@infoalloggi.it"; "Arcenia Srl. | Tel. +39 0424529869 | Email: arca@infoalloggi.it";
return ( return (
<Card className="p-4 print:border-none print:shadow-none"> <div className="space-y-6">
<div className="space-y-6"> <div className="flex items-start justify-between">
<div className="flex items-start justify-between"> <div>
<div> <h1 className="font-bold text-2xl">{companyName}</h1>
<h1 className="font-bold text-2xl">{companyName}</h1> <p className="whitespace-pre-line text-sm">{companyAddress}</p>
<p className="whitespace-pre-line text-sm">{companyAddress}</p> </div>
</div>
<div className="relative size-24"> <div className="relative size-24">
<Image <Image
alt={`${companyName} logo`} alt={`${companyName} logo`}
className="object-contain" className="object-contain"
fill fill
src={"/favicon/favicon.png"} src={"/favicon/favicon.png"}
/> />
</div>
</div> </div>
<div className="border-b pb-4"> </div>
<h2 className="text-center font-semibold text-xl"> <div className="border-b pb-4">
Ricevuta di cortesia <h2 className="text-center font-semibold text-xl">
</h2> Ricevuta di cortesia
<div className="mt-2 flex justify-between"> </h2>
<div> <div className="mt-2 flex justify-between">
<p className="text-muted-foreground text-sm">ID Pagamento</p>
<p className="text-sm">{data.paymentId}</p>
</div>
<div className="text-right">
<p className="text-muted-foreground text-sm">Data</p>
<p className="font-medium">
{format(data.date, "PPP", {
locale: it,
})}
</p>
<p className="text-sm">
{format(data.date, "p", {
locale: it,
})}
</p>
</div>
</div>
</div>
<div className="grid grid-cols-2 gap-6">
<div> <div>
<h3 className="mb-1 font-semibold">Intestatario:</h3> <p className="text-muted-foreground text-sm">ID Pagamento</p>
<p className="font-medium">{data.clienteNome}</p> <p className="text-sm">{data.paymentId}</p>
<p className="whitespace-pre-line text-sm"> </div>
{data.clienteIndirizzo} <div className="text-right">
<p className="text-muted-foreground text-sm">Data</p>
<p className="font-medium">
{format(data.date, "PPP", {
locale: it,
})}
</p>
<p className="text-sm">
{format(data.date, "p", {
locale: it,
})}
</p> </p>
</div> </div>
</div> </div>
</div>
<div className="grid grid-cols-2 gap-6">
<div> <div>
<table className="w-full"> <h3 className="mb-1 font-semibold">Intestatario:</h3>
<thead> <p className="font-medium">{data.clienteNome}</p>
<tr className="border-b"> <p className="whitespace-pre-line text-sm">{data.clienteIndirizzo}</p>
<th className="py-2 text-left">Descrizione</th>
<th className="py-2 text-right">Prezzo</th>
<th className="py-2 text-right">Sconto</th>
<th className="py-2 text-right">Importo</th>
</tr>
</thead>
<tbody>
{data.items.map((item) => (
<tr className="border-b" key={item.description}>
<td className="py-2">{item.description}</td>
<td className="py-2 text-right">
{formatCurrency(item.price)}
</td>
<td className="py-2 text-right">
{item.discount > 0
? `-${(item.discount * 100).toFixed(0)}%`
: ""}
</td>
<td className="py-2 text-right">
{formatCurrency(item.price * (1 - item.discount))}
</td>
</tr>
))}
</tbody>
</table>
</div>
<div className="flex justify-end">
<div className="w-64">
<div className="mt-1 flex justify-between pt-2 font-bold">
<span>Totale:</span>
<span>{formatCurrency(total)}</span>
</div>
<div className="flex justify-between py-1">
<span>di cui IVA (22%):</span>
<span>{formatCurrency(total * 0.22)}</span>
</div>
</div>
</div>
<div className="border-t pt-4">
<div className="flex justify-between">
<div>
<h3 className="mb-1 font-semibold">Metodo di pagamento</h3>
<p>{PaymentMethodToString(data.paymentMethod as PaymentType)}</p>
</div>
<div className="text-right">
<h3 className="mb-1 font-semibold">Status pagamento</h3>
<p className="font-medium text-green-600">Pagato</p>
</div>
</div>
</div>
<div className="border-t pt-4">
<h3 className="mb-1 font-semibold">Note</h3>
<p className="whitespace-pre-line text-sm">{note}</p>
</div>
<div className="pt-6 text-center text-muted-foreground text-sm">
<p>{footer}</p>
</div> </div>
</div> </div>
</Card> <div>
<table className="w-full">
<thead>
<tr className="border-b">
<th className="py-2 text-left">Descrizione</th>
<th className="py-2 text-right">Prezzo</th>
<th className="py-2 text-right">Sconto</th>
<th className="py-2 text-right">Importo</th>
</tr>
</thead>
<tbody>
{data.items.map((item) => (
<tr className="border-b" key={item.description}>
<td className="py-2">{item.description}</td>
<td className="py-2 text-right">
{formatCurrency(item.price)}
</td>
<td className="py-2 text-right">
{item.discount > 0
? `-${(item.discount * 100).toFixed(0)}%`
: ""}
</td>
<td className="py-2 text-right">
{formatCurrency(item.price * (1 - item.discount))}
</td>
</tr>
))}
</tbody>
</table>
</div>
<div className="flex justify-end">
<div className="w-64">
<div className="mt-1 flex justify-between pt-2 font-bold">
<span>Totale:</span>
<span>{formatCurrency(total)}</span>
</div>
<div className="flex justify-between py-1">
<span>di cui IVA (22%):</span>
<span>{formatCurrency(total * 0.22)}</span>
</div>
</div>
</div>
<div className="border-t pt-4">
<div className="flex justify-between">
<div>
<h3 className="mb-1 font-semibold">Metodo di pagamento</h3>
<p>{PaymentMethodToString(data.paymentMethod as PaymentType)}</p>
</div>
<div className="text-right">
<h3 className="mb-1 font-semibold">Status pagamento</h3>
<p className="font-medium text-green-600">Pagato</p>
</div>
</div>
</div>
<div className="border-t pt-4">
<h3 className="mb-1 font-semibold">Note</h3>
<p className="whitespace-pre-line text-sm">{note}</p>
</div>
<div className="pt-6 text-center text-muted-foreground text-sm">
<p>{footer}</p>
</div>
</div>
); );
} }

View file

@ -145,7 +145,7 @@ export const OrdiniModal = ({
) && ( ) && (
<div> <div>
<Link <Link
href={`/area-riservata/payment-recap/${data.ordine_id}`} href={`/servizio/ricevuta-cortesia/${data.ordine_id}`}
target="_blank" target="_blank"
> >
<Button variant="outline"> <Button variant="outline">

View file

@ -176,7 +176,10 @@ export const PaymentsTable = ({
<DropdownMenuRadioGroup <DropdownMenuRadioGroup
onValueChange={(v) => { onValueChange={(v) => {
update({ update({
data: { paymentstatus: EnumFromString(v) }, data: {
paymentstatus: EnumFromString(v),
paid_at: v === "Successo" ? new Date() : null,
},
pagamentoId: data.id, pagamentoId: data.id,
}); });
}} }}

View file

@ -1,4 +1,5 @@
export type PaymentType = export type PaymentType =
| "manual"
| "acss_debit" | "acss_debit"
| "affirm" | "affirm"
| "afterpay_clearpay" | "afterpay_clearpay"
@ -50,6 +51,8 @@ export type PaymentType =
| "zip"; | "zip";
export const PaymentMethodToString = (type: PaymentType) => { export const PaymentMethodToString = (type: PaymentType) => {
switch (type) { switch (type) {
case "manual":
return "Pagamento in sede";
case "card": case "card":
return "Carta di credito"; return "Carta di credito";
case "sepa_debit": case "sepa_debit":

View file

@ -4,7 +4,7 @@ import { LoadingPage } from "~/components/loading";
import { Status500 } from "~/components/status-page"; import { Status500 } from "~/components/status-page";
import type { NextPageWithLayout } from "~/pages/_app"; import type { NextPageWithLayout } from "~/pages/_app";
import type { TestiEStringheStingaId } from "~/schemas/public/TestiEStringhe"; import type { TestiEStringheStingaId } from "~/schemas/public/TestiEStringhe";
import { TrpcAuthedFetchingIstance } from "~/server/utils/ssgHelper"; import { generateSSGHelper } from "~/server/utils/ssgHelper";
import { zTestiEStringheStingaId } from "~/server/utils/zod_types"; import { zTestiEStringheStingaId } from "~/server/utils/zod_types";
import { api } from "~/utils/api"; import { api } from "~/utils/api";
@ -38,6 +38,9 @@ const CondizioniPage: NextPageWithLayout<CondizioniProps> = ({
}; };
CondizioniPage.getLayout = function getLayout(page) { CondizioniPage.getLayout = function getLayout(page) {
if (page.props.raw) {
return <>{page}</>;
}
return <AreaRiservataLayout noSidebar>{page}</AreaRiservataLayout>; return <AreaRiservataLayout noSidebar>{page}</AreaRiservataLayout>;
}; };
@ -45,7 +48,7 @@ export default CondizioniPage;
export const getServerSideProps = (async (context) => { export const getServerSideProps = (async (context) => {
const id = context.params?.stringaId; const id = context.params?.stringaId;
const raw = context.query?.raw;
if (typeof id !== "string") { if (typeof id !== "string") {
console.error("Error: stringaId is not a string"); console.error("Error: stringaId is not a string");
return { return {
@ -67,18 +70,17 @@ export const getServerSideProps = (async (context) => {
}; };
} }
const access_token = context.req.cookies.access_token; const helper = generateSSGHelper();
const helper = await TrpcAuthedFetchingIstance({ access_token });
if (helper) { if (helper) {
await helper.trpc.strings.getStringa.prefetch({ await helper.strings.getStringa.prefetch({
stringaId: parsed.data, stringaId: parsed.data,
}); });
return { return {
props: { props: {
raw: raw === "true",
stringaId: parsed.data, stringaId: parsed.data,
trpcState: helper.trpc.dehydrate(), trpcState: helper.dehydrate(),
}, },
}; };
} }

View file

@ -5,15 +5,17 @@ import { LoadingPage } from "~/components/loading";
import { Status500 } from "~/components/status-page"; import { Status500 } from "~/components/status-page";
import type { NextPageWithLayout } from "~/pages/_app"; import type { NextPageWithLayout } from "~/pages/_app";
import type { OrdiniOrdineId } from "~/schemas/public/Ordini"; import type { OrdiniOrdineId } from "~/schemas/public/Ordini";
import { TrpcAuthedFetchingIstance } from "~/server/utils/ssgHelper"; import { generateSSGHelper } from "~/server/utils/ssgHelper";
import { zOrdineId } from "~/server/utils/zod_types"; import { zOrdineId } from "~/server/utils/zod_types";
import { api } from "~/utils/api"; import { api } from "~/utils/api";
type PagamentoRecapProps = { type PagamentoRecapProps = {
raw: boolean;
ordineId: OrdiniOrdineId; ordineId: OrdiniOrdineId;
}; };
const PaymentRecapPage: NextPageWithLayout<PagamentoRecapProps> = ({ const PaymentRecapPage: NextPageWithLayout<PagamentoRecapProps> = ({
raw,
ordineId, ordineId,
}: PagamentoRecapProps) => { }: PagamentoRecapProps) => {
const { data, isLoading } = api.servizio.getOrdineRicevutaData.useQuery({ const { data, isLoading } = api.servizio.getOrdineRicevutaData.useQuery({
@ -28,12 +30,15 @@ const PaymentRecapPage: NextPageWithLayout<PagamentoRecapProps> = ({
} }
return ( return (
<div className="mx-auto w-full p-4"> <div className="mx-auto w-full p-4">
<ReceiptGenerator data={data} /> <ReceiptGenerator data={data} raw={raw} />
</div> </div>
); );
}; };
PaymentRecapPage.getLayout = function getLayout(page) { PaymentRecapPage.getLayout = function getLayout(page) {
if (page.props.raw) {
return <>{page}</>;
}
return <AreaRiservataLayout noSidebar>{page}</AreaRiservataLayout>; return <AreaRiservataLayout noSidebar>{page}</AreaRiservataLayout>;
}; };
@ -41,7 +46,7 @@ export default PaymentRecapPage;
export const getServerSideProps = (async (context) => { export const getServerSideProps = (async (context) => {
const id = context.params?.ordineId; const id = context.params?.ordineId;
const raw = context.query?.raw;
if (typeof id !== "string") { if (typeof id !== "string") {
console.error("Error: ordineId is not a string"); console.error("Error: ordineId is not a string");
return { return {
@ -63,18 +68,17 @@ export const getServerSideProps = (async (context) => {
}; };
} }
const access_token = context.req.cookies.access_token; const helper = generateSSGHelper();
const helper = await TrpcAuthedFetchingIstance({ access_token });
if (helper) { if (helper) {
await helper.trpc.servizio.getOrdineRicevutaData.prefetch({ await helper.servizio.getOrdineRicevutaData.prefetch({
ordineId: parsed.data, ordineId: parsed.data,
}); });
return { return {
props: { props: {
raw: raw === "true",
ordineId: parsed.data, ordineId: parsed.data,
trpcState: helper.trpc.dehydrate(), trpcState: helper.dehydrate(),
}, },
}; };
} }

View file

@ -9,8 +9,12 @@ import { signToken, verifyToken } from "~/server/auth/jwt";
export const authProxy: ProxyFn = async (req: NextRequest) => { export const authProxy: ProxyFn = async (req: NextRequest) => {
const path = req.nextUrl.pathname; const path = req.nextUrl.pathname;
const isOverridableRoute =
path.startsWith("/area-riservata/admin/scheda-annuncio-stampa/") ||
path.startsWith("/servizio/condizioni/") ||
path.startsWith("/servizio/ricevuta-cortesia/");
// permette a puppeteer di accedere alla pagina di generazione PDF bypassando l'autenticazione normale // permette a puppeteer di accedere alla pagina di generazione PDF bypassando l'autenticazione normale
if (path.startsWith("/area-riservata/admin/scheda-annuncio-stampa/")) { if (isOverridableRoute) {
const overrideToken = req.cookies.get( const overrideToken = req.cookies.get(
TOKEN_CONFIG.OVERRIDE_TOKEN_COOKIE_NAME, TOKEN_CONFIG.OVERRIDE_TOKEN_COOKIE_NAME,
)?.value; )?.value;

View file

@ -8,6 +8,7 @@ import type { UsersAnagraficaUpdate } from "~/schemas/public/UsersAnagrafica";
import { import {
adminProcedure, adminProcedure,
createTRPCRouter, createTRPCRouter,
overridableProcedure,
protectedProcedure, protectedProcedure,
publicProcedure, publicProcedure,
} from "~/server/api/trpc"; } from "~/server/api/trpc";
@ -169,14 +170,14 @@ export const servizioRouter = createTRPCRouter({
.query(async ({ input }) => { .query(async ({ input }) => {
return await getOrdineById(input.ordineId); return await getOrdineById(input.ordineId);
}), }),
getOrdineRicevutaData: protectedProcedure getOrdineRicevutaData: overridableProcedure
.input( .input(
z.object({ z.object({
ordineId: zOrdineId, ordineId: zOrdineId,
}), }),
) )
.query(async ({ input, ctx }) => { .query(async ({ input }) => {
return await getOrdineRicevutaData({ ctx, ordineId: input.ordineId }); return await getOrdineRicevutaData({ ordineId: input.ordineId });
}), }),
getServizioOrdini: protectedProcedure getServizioOrdini: protectedProcedure
.input( .input(

View file

@ -14,11 +14,10 @@ export const testRouter = createTRPCRouter({
console.error("test console.error"); console.error("test console.error");
throw new Error("This is a test error"); throw new Error("This is a test error");
}), }),
});
/* /*
PROTECTED API CALL EXAMPLE PROTECTED API CALL EXAMPLE
curl --location 'localhost:3000/api/trpc/test.testApi' \ curl --location 'localhost:3000/api/trpc/test.testApi' \
--header 'Authorization: Basic aW5mb2FsbG9nZ2ktYWRtaW46Y3lZazI0dTlmUm41RHNxakZXcFRWOEdLQk53aFF2RXRiTW1IeEplVTczWlNhWHJkQ0xBUGc2TnREa0M0M1pRYTlkVktIYnhNdjdmV3B5NjVnRm1Bc0VSZVh3dVBZVEw4MmpKblNVaHJ6Y3FHa3F2c0hQeXhtRUFGellWU250R2VRVHIyNWRKYjNVTkNNdVJwZ3dhRFc2OVpoYzdLajhMZkI0WkNIUjlrWWZXVnNHcng3dHlncWR2YzgyU0ZYajU0TmhucG02UEVCTGJLVXVhTVRRSmVBM3dEZ1BTQ1h3RFZtaG5lZDZKOFpRM3F6TnNyNWJCdUx0SHh2RlRBS0d5NzRXMmZZOXBja2pNUlVFSllSa1ZQWndidkJ0VEt5MkY4VUxBc2V1bnpXajdHNWQ2UTNFeGZtTkNYcWc0TURhaEg5Y1NydVh5V3hMTkFzR1FUUFNGaEQ5SFlWekIzTWVnMnJrNUN2YWNmcThtZFo2dFVFNEtScHdibjdqSkFldjdXUmJWZ1BkbmNNTEt4d204eVQ5cFFaNEROVWthWWo2enRYZmhzQkhxRjVFckN1MzJHY2piM0g2S3h2V3N1OXA1ZmtQcmg3WkFSWE1MRGRTODJlbkZVNENxd3p0VGF5RW1HTlZnWUpC' --header 'Authorization: Basic aW5mb2FsbG9nZ2ktYWRtaW46Y3lZazI0dTlmUm41RHNxakZXcFRWOEdLQk53aFF2RXRiTW1IeEplVTczWlNhWHJkQ0xBUGc2TnREa0M0M1pRYTlkVktIYnhNdjdmV3B5NjVnRm1Bc0VSZVh3dVBZVEw4MmpKblNVaHJ6Y3FHa3F2c0hQeXhtRUFGellWU250R2VRVHIyNWRKYjNVTkNNdVJwZ3dhRFc2OVpoYzdLajhMZkI0WkNIUjlrWWZXVnNHcng3dHlncWR2YzgyU0ZYajU0TmhucG02UEVCTGJLVXVhTVRRSmVBM3dEZ1BTQ1h3RFZtaG5lZDZKOFpRM3F6TnNyNWJCdUx0SHh2RlRBS0d5NzRXMmZZOXBja2pNUlVFSllSa1ZQWndidkJ0VEt5MkY4VUxBc2V1bnpXajdHNWQ2UTNFeGZtTkNYcWc0TURhaEg5Y1NydVh5V3hMTkFzR1FUUFNGaEQ5SFlWekIzTWVnMnJrNUN2YWNmcThtZFo2dFVFNEtScHdibjdqSkFldjdXUmJWZ1BkbmNNTEt4d204eVQ5cFFaNEROVWthWWo2enRYZmhzQkhxRjVFckN1MzJHY2piM0g2S3h2V3N1OXA1ZmtQcmg3WkFSWE1MRGRTODJlbkZVNENxd3p0VGF5RW1HTlZnWUpC'
*/ */
});

View file

@ -17,7 +17,6 @@ import {
} from "~/server/controllers/user.controller"; } from "~/server/controllers/user.controller";
import { db } from "~/server/db"; import { db } from "~/server/db";
import { getClientProfilo } from "~/server/services/user.service"; import { getClientProfilo } from "~/server/services/user.service";
import { checkCtxSession } from "~/server/utils/ctxChecker";
import { zUserId } from "~/server/utils/zod_types"; import { zUserId } from "~/server/utils/zod_types";
export const usersRouter = createTRPCRouter({ export const usersRouter = createTRPCRouter({
@ -27,8 +26,8 @@ export const usersRouter = createTRPCRouter({
id: zUserId, id: zUserId,
}), }),
) )
.mutation(async ({ ctx, input }) => { .mutation(async ({ input }) => {
return await blockUserHandler({ ctx, id: input.id }); return await blockUserHandler(input.id);
}), }),
deleteUser: adminProcedure deleteUser: adminProcedure
.input( .input(
@ -36,8 +35,8 @@ export const usersRouter = createTRPCRouter({
id: zUserId, id: zUserId,
}), }),
) )
.mutation(async ({ ctx, input }) => { .mutation(async ({ input }) => {
return await deleteUserHandler({ ctx, id: input.id }); return await deleteUserHandler(input.id);
}), }),
editUser: protectedProcedure editUser: protectedProcedure
.input( .input(
@ -46,8 +45,7 @@ export const usersRouter = createTRPCRouter({
data: z.custom<UsersUpdate>(), data: z.custom<UsersUpdate>(),
}), }),
) )
.mutation(async ({ ctx, input }) => { .mutation(async ({ input }) => {
checkCtxSession({ ctx });
return await editAccountHandler({ ...input }); return await editAccountHandler({ ...input });
}), }),
editUserAnagrafica: protectedProcedure editUserAnagrafica: protectedProcedure

View file

@ -141,6 +141,33 @@ const enforceUserIsAuthed = t.middleware(({ ctx, next, path }) => {
message: `User is not logged, error in path: ${path}`, message: `User is not logged, error in path: ${path}`,
}); });
} }
return next({
ctx: {
// infers the `session` as non-nullable
session: { ...ctx.session },
},
});
});
const enforceUserIsAuthedOrOverride = t.middleware(({ ctx, next, path }) => {
const overrideToken =
ctx.req?.cookies[TOKEN_CONFIG.OVERRIDE_TOKEN_COOKIE_NAME];
if (overrideToken && overrideToken === env.EXP_API_PASS) {
return next({
ctx: {
// infers the `session` as non-nullable
session: { ...ctx.session },
},
});
}
if (!ctx.session) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: `User is not logged, error in path: ${path}`,
});
}
return next({ return next({
ctx: { ctx: {
// infers the `session` as non-nullable // infers the `session` as non-nullable
@ -158,6 +185,7 @@ const enforceUserIsAuthed = t.middleware(({ ctx, next, path }) => {
* @see https://trpc.io/docs/procedures * @see https://trpc.io/docs/procedures
*/ */
export const protectedProcedure = t.procedure.use(enforceUserIsAuthed); export const protectedProcedure = t.procedure.use(enforceUserIsAuthed);
export const overridableProcedure = t.procedure.use(enforceUserIsAuthedOrOverride);
/** Reusable middleware that enforces users are logged in before running the procedure. */ /** Reusable middleware that enforces users are logged in before running the procedure. */
const enforceUserIsAdmin = t.middleware(({ ctx, next, path }) => { const enforceUserIsAdmin = t.middleware(({ ctx, next, path }) => {

View file

@ -5,6 +5,7 @@ import type {
OrdiniOrdineId, OrdiniOrdineId,
OrdiniUpdate, OrdiniUpdate,
} from "~/schemas/public/Ordini"; } from "~/schemas/public/Ordini";
import PaymentStatusEnum from "~/schemas/public/PaymentStatusEnum";
import type { UsersId } from "~/schemas/public/Users"; import type { UsersId } from "~/schemas/public/Users";
import { db, type Querier } from "~/server/db"; import { db, type Querier } from "~/server/db";
@ -67,11 +68,36 @@ export const getOrdineById = async (ordineId: OrdiniOrdineId) => {
export const createOrdine = async (data: NewOrdini) => { export const createOrdine = async (data: NewOrdini) => {
try { try {
return await db return await db.transaction().execute(async (tx) => {
.insertInto("ordini") const prezzo = await tx
.values(data) .selectFrom("prezziario")
.returningAll() .select(["prezzo_cent", "nome_it"])
.executeTakeFirstOrThrow(); .where("prezziario.idprezziario", "=", data.packid)
.executeTakeFirstOrThrow();
const order = await tx
.insertInto("ordini")
.values(data)
.returningAll()
.executeTakeFirstOrThrow();
await tx
.insertInto("payments")
.values({
ordine_id: order.ordine_id,
servizio_id: order.servizio_id,
userid: order.userid,
amount_cent: prezzo.prezzo_cent,
paymentname: prezzo.nome_it,
paymentmethod: "manual",
paymentstatus: data.isActive
? PaymentStatusEnum.success
: PaymentStatusEnum.processing,
paid_at: data.isActive ? new Date() : null,
})
.execute();
return order;
});
} catch (e) { } catch (e) {
throw new TRPCError({ throw new TRPCError({
code: "INTERNAL_SERVER_ERROR", code: "INTERNAL_SERVER_ERROR",

View file

@ -32,7 +32,6 @@ import TipologiaPosizioneEnum from "~/schemas/public/TipologiaPosizioneEnum";
import type { Users, UsersId } from "~/schemas/public/Users"; import type { Users, UsersId } from "~/schemas/public/Users";
import type { UsersAnagraficaUpdate } from "~/schemas/public/UsersAnagrafica"; import type { UsersAnagraficaUpdate } from "~/schemas/public/UsersAnagrafica";
import type { UsersStorageUserStorageId } from "~/schemas/public/UsersStorage"; import type { UsersStorageUserStorageId } from "~/schemas/public/UsersStorage";
import type { Context } from "~/server/api/trpc";
import { import {
addEventToQueue, addEventToQueue,
canSendNotification, canSendNotification,
@ -882,8 +881,8 @@ export const getAllServizioAnnunci = async (
withVideos(), withVideos(),
]) ])
//ignora annunci che non sono disponibili //ignora annunci che non sono disponibili
.where("annunci.web", "=", true) //.where("annunci.web", "=", true)
.where("annunci.stato", "!=", "Sospeso") //.where("annunci.stato", "!=", "Sospeso")
.orderBy("servizio_annunci.user_confirmed_at", (ob) => .orderBy("servizio_annunci.user_confirmed_at", (ob) =>
ob.asc().nullsLast(), ob.asc().nullsLast(),
) )
@ -1035,8 +1034,8 @@ export const getServizioDataById = async (
withVideos(), withVideos(),
]) ])
//ignora annunci che non sono disponibili //ignora annunci che non sono disponibili
.where("annunci.web", "=", true) //.where("annunci.web", "=", true)
.where("annunci.stato", "!=", "Sospeso") //.where("annunci.stato", "!=", "Sospeso")
.orderBy("servizio_annunci.user_confirmed_at", (ob) => .orderBy("servizio_annunci.user_confirmed_at", (ob) =>
ob.asc().nullsLast(), ob.asc().nullsLast(),
) )
@ -2025,10 +2024,8 @@ export const getCompatibileAnnunci = async ({
export const getOrdineRicevutaData = async ({ export const getOrdineRicevutaData = async ({
ordineId, ordineId,
ctx,
}: { }: {
ordineId: OrdiniOrdineId; ordineId: OrdiniOrdineId;
ctx: Context;
}) => { }) => {
try { try {
const data = await db const data = await db
@ -2070,19 +2067,6 @@ export const getOrdineRicevutaData = async ({
.executeTakeFirstOrThrow(); .executeTakeFirstOrThrow();
if (!ctx.session) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "User not authenticated",
});
}
if (!ctx.session.isAdmin && data.userid !== ctx.session.id) {
throw new TRPCError({
code: "FORBIDDEN",
message: "You do not have permission to access this ordine",
});
}
if (!data || !data.user || !data.anagrafica) { if (!data || !data.user || !data.anagrafica) {
throw new TRPCError({ throw new TRPCError({
code: "NOT_FOUND", code: "NOT_FOUND",

View file

@ -4,10 +4,15 @@ import stripe from "~/lib/stripe";
import OrderTypeEnum from "~/schemas/public/OrderTypeEnum"; import OrderTypeEnum from "~/schemas/public/OrderTypeEnum";
import PaymentStatusEnum from "~/schemas/public/PaymentStatusEnum"; import PaymentStatusEnum from "~/schemas/public/PaymentStatusEnum";
import type { Payments, PaymentsId } from "~/schemas/public/Payments"; import type { Payments, PaymentsId } from "~/schemas/public/Payments";
import type { TestiEStringheStingaId } from "~/schemas/public/TestiEStringhe";
import TipologiaPosizioneEnum from "~/schemas/public/TipologiaPosizioneEnum"; import TipologiaPosizioneEnum from "~/schemas/public/TipologiaPosizioneEnum";
import { db } from "~/server/db"; import { db } from "~/server/db";
import { NewMail } from "~/server/services/mailer"; import { NewMail } from "~/server/services/mailer";
import { getUser } from "~/server/services/user.service"; import { getUser } from "~/server/services/user.service";
import {
genPdfCondizioniBase64,
genPdfRicevutaCortesiaBase64,
} from "../services/puppeteer.service";
export const stripeHealthCheck = async () => { export const stripeHealthCheck = async () => {
try { try {
@ -162,23 +167,31 @@ export const whIntentSucceededHandler = async ({
}) })
.execute(); .execute();
//SERVIZIO ATTIVATO
await NewMail({ await NewMail({
template: { template: {
mailType: "generic", mailType: "servizioAttivato",
props: {
link: {
href: `${env.BASE_URL}/area-riservata/payment-recap/${ordine.ordine_id}`,
label: "Vai alla ricevuta",
},
noreply: true,
testo: `Il pagamento dell'acconto per il servizio ${ordine.packid} è stato confermato.`,
title: "Pagamento Confermato ",
},
}, },
mail: { mail: {
subject: "Pagamento Confermato - Acconto Infoalloggi.it", subject: "Pagamento Confermato - Acconto Infoalloggi.it",
to: utente.email, to: utente.email,
attachments: [
{
filename: `condizioni_contrattuali_${ordine.created_at.toISOString()}.pdf`,
//ATTENTION: hardcoded condizioni key
content: await genPdfCondizioniBase64(
"CONDIZIONI_CERCHI" as TestiEStringheStingaId,
),
encoding: "base64",
contentType: "application/pdf",
},
{
filename: `ricevuta_cortesia_${ordine.ordine_id}.pdf`,
content: await genPdfRicevutaCortesiaBase64(ordine.ordine_id),
encoding: "base64",
contentType: "application/pdf",
},
],
}, },
userId: utente.id, userId: utente.id,
}); });
@ -196,20 +209,19 @@ export const whIntentSucceededHandler = async ({
.execute(); .execute();
await NewMail({ await NewMail({
template: { template: {
mailType: "generic", mailType: "pagamentoConferma",
props: {
link: {
href: `${env.BASE_URL}/area-riservata/payment-recap/${ordine.ordine_id}`,
label: "Vai alla ricevuta",
},
noreply: true,
testo: `Il pagamento del saldo per il servizio ${ordine.packid} è stato confermato.`,
title: "Pagamento Confermato ",
},
}, },
mail: { mail: {
subject: "Pagamento Confermato - Saldo Infoalloggi.it", subject: "Pagamento Confermato - Saldo Infoalloggi.it",
to: utente.email, to: utente.email,
attachments: [
{
filename: `ricevuta_cortesia_${ordine.ordine_id}.pdf`,
content: await genPdfRicevutaCortesiaBase64(ordine.ordine_id),
encoding: "base64",
contentType: "application/pdf",
},
],
}, },
userId: utente.id, userId: utente.id,
}); });
@ -224,20 +236,19 @@ export const whIntentSucceededHandler = async ({
.execute(); .execute();
await NewMail({ await NewMail({
template: { template: {
mailType: "generic", mailType: "pagamentoConferma",
props: {
link: {
href: `${env.BASE_URL}/area-riservata/payment-recap/${ordine.ordine_id}`,
label: "Vai alla ricevuta",
},
noreply: true,
testo: `Il pagamento per la consulenza è stato confermato.`,
title: "Pagamento Confermato ",
},
}, },
mail: { mail: {
subject: "Pagamento Confermato - Consulenza Infoalloggi.it", subject: "Pagamento Confermato - Consulenza Infoalloggi.it",
to: utente.email, to: utente.email,
attachments: [
{
filename: `ricevuta_cortesia_${ordine.ordine_id}.pdf`,
content: await genPdfRicevutaCortesiaBase64(ordine.ordine_id),
encoding: "base64",
contentType: "application/pdf",
},
],
}, },
userId: utente.id, userId: utente.id,
}); });
@ -247,13 +258,18 @@ export const whIntentSucceededHandler = async ({
await NewMail({ await NewMail({
template: { template: {
mailType: "pagamentoConferma", mailType: "pagamentoConferma",
props: {
receiptHref: `${env.BASE_URL}/area-riservata/payment-recap/${ordine.ordine_id}`,
},
}, },
mail: { mail: {
subject: "Pagamento Confermato", subject: "Pagamento Confermato - Infoalloggi.it",
to: utente.email, to: utente.email,
attachments: [
{
filename: `ricevuta_cortesia_${ordine.ordine_id}.pdf`,
content: await genPdfRicevutaCortesiaBase64(ordine.ordine_id),
encoding: "base64",
contentType: "application/pdf",
},
],
}, },
userId: utente.id, userId: utente.id,
}); });
@ -294,7 +310,7 @@ export const whIntentFailedHandler = async ({
mailType: "pagamentoErrore", mailType: "pagamentoErrore",
}, },
mail: { mail: {
subject: "Pagamento Fallito", subject: "Pagamento Fallito - Infoalloggi.it",
to: utente.email, to: utente.email,
}, },
userId: utente.id, userId: utente.id,

View file

@ -1,9 +1,7 @@
import { TRPCError } from "@trpc/server"; import { TRPCError } from "@trpc/server";
import type { UsersId, UsersUpdate } from "~/schemas/public/Users"; import type { UsersId, UsersUpdate } from "~/schemas/public/Users";
import type { UsersAnagraficaUpdate } from "~/schemas/public/UsersAnagrafica"; import type { UsersAnagraficaUpdate } from "~/schemas/public/UsersAnagrafica";
import type { Context } from "~/server/api/trpc";
import { db, type Querier } from "~/server/db"; import { db, type Querier } from "~/server/db";
import { checkCtxAdmin } from "~/server/utils/ctxChecker";
export const editAccountHandler = async ({ export const editAccountHandler = async ({
id, id,
@ -92,15 +90,7 @@ export const editAnagraficaHandler = async ({
} }
}; };
export const deleteUserHandler = async ({ export const deleteUserHandler = async (id: UsersId) => {
ctx,
id,
}: {
ctx: Context;
id: UsersId;
}) => {
checkCtxAdmin({ ctx });
try { try {
await db.deleteFrom("users").where("id", "=", id).execute(); await db.deleteFrom("users").where("id", "=", id).execute();
return { success: true }; return { success: true };
@ -112,14 +102,7 @@ export const deleteUserHandler = async ({
} }
}; };
export const blockUserHandler = async ({ export const blockUserHandler = async (id: UsersId) => {
ctx,
id,
}: {
ctx: Context;
id: UsersId;
}) => {
checkCtxAdmin({ ctx });
return await db.transaction().execute(async (trx) => { return await db.transaction().execute(async (trx) => {
return await trx return await trx
.updateTable("users") .updateTable("users")

View file

@ -29,6 +29,8 @@ import EmailSchedaContatto, {
} from "emails/scheda-annuncio"; } from "emails/scheda-annuncio";
import type { ContattiConScheda_NewMail } from "emails/scheda-contatti"; import type { ContattiConScheda_NewMail } from "emails/scheda-contatti";
import EmailContattiConScheda from "emails/scheda-contatti"; import EmailContattiConScheda from "emails/scheda-contatti";
import type { ServizioAttivato_NewMail } from "emails/servizio-attivato";
import ServizioAttivato from "emails/servizio-attivato";
import ShareAnnunci, { type ShareAnnunci_NewMail } from "emails/share-annunci"; import ShareAnnunci, { type ShareAnnunci_NewMail } from "emails/share-annunci";
import VerificaEmail, { import VerificaEmail, {
type VerificaEmail_NewMail, type VerificaEmail_NewMail,
@ -93,7 +95,8 @@ export type MailsTemplates =
| Generic_NewMail | Generic_NewMail
| SchedaContatto_NewMail | SchedaContatto_NewMail
| Invito_NewMail | Invito_NewMail
| ContattiConScheda_NewMail; | ContattiConScheda_NewMail
| ServizioAttivato_NewMail;
export const genMail = async ({ ...mail }: MailsTemplates) => { export const genMail = async ({ ...mail }: MailsTemplates) => {
let mailComponent: JSX.Element | null = null; let mailComponent: JSX.Element | null = null;
@ -121,9 +124,7 @@ export const genMail = async ({ ...mail }: MailsTemplates) => {
break; break;
case "pagamentoConferma": case "pagamentoConferma":
mailComponent = PagamentoConferma({ mailComponent = PagamentoConferma();
...mail.props,
});
title = "Conferma pagamento"; title = "Conferma pagamento";
break; break;
@ -191,6 +192,10 @@ export const genMail = async ({ ...mail }: MailsTemplates) => {
}); });
title = "Contatti Con Scheda"; title = "Contatti Con Scheda";
break; break;
case "servizioAttivato":
mailComponent = ServizioAttivato();
title = "Servizio Attivato";
break;
default: default:
throw new TRPCError({ throw new TRPCError({
code: "INTERNAL_SERVER_ERROR", code: "INTERNAL_SERVER_ERROR",

View file

@ -1,8 +1,10 @@
import { TRPCError } from "@trpc/server"; import { TRPCError } from "@trpc/server";
import type Redis from "ioredis"; import type Redis from "ioredis";
import puppeteer from "puppeteer-core"; import puppeteer, { type PDFOptions } from "puppeteer-core";
import { env } from "~/env"; import { env } from "~/env";
import type { AnnunciId } from "~/schemas/public/Annunci"; import type { AnnunciId } from "~/schemas/public/Annunci";
import type { OrdiniOrdineId } from "~/schemas/public/Ordini";
import type { TestiEStringheStingaId } from "~/schemas/public/TestiEStringhe";
import { getKeydbClient } from "~/utils/keydb"; import { getKeydbClient } from "~/utils/keydb";
import { TOKEN_CONFIG } from "../auth/configs"; import { TOKEN_CONFIG } from "../auth/configs";
@ -82,13 +84,17 @@ export const genPdfSchedaAnnuncioBase64 = async (annuncioId: AnnunciId) => {
export const genPdfSchedaAnnuncio = async (annuncioId: AnnunciId) => { export const genPdfSchedaAnnuncio = async (annuncioId: AnnunciId) => {
const keybd = getKeydbClient(); const keybd = getKeydbClient();
const url = `/area-riservata/admin/scheda-annuncio-stampa/${annuncioId}?raw=true`; const url = `/area-riservata/admin/scheda-annuncio-stampa/${annuncioId}?raw=true`;
const options: PDFOptions = { printBackground: true };
if (keybd) { if (keybd) {
const cacheKey = `${SCHEDA_ANNUNCIO_CACHE_PREFIX}${annuncioId}`; const cacheKey = `${SCHEDA_ANNUNCIO_CACHE_PREFIX}${annuncioId}`;
const cached = await keybd.get(cacheKey); const cached = await keybd.get(cacheKey);
if (cached) { if (cached) {
return new Uint8Array(Buffer.from(cached, "base64")); return new Uint8Array(Buffer.from(cached, "base64"));
} }
const scheda = await puppeteerGenPdf(url); const scheda = await puppeteerGenPdf({
url,
options,
});
await keybd.set( await keybd.set(
cacheKey, cacheKey,
Buffer.from(scheda).toString("base64"), Buffer.from(scheda).toString("base64"),
@ -97,10 +103,26 @@ export const genPdfSchedaAnnuncio = async (annuncioId: AnnunciId) => {
); );
return scheda; return scheda;
} }
return await puppeteerGenPdf(url); return await puppeteerGenPdf({ url, options });
}; };
export const genPdfCondizioni = async (condizioniId: AnnunciId) => { export const genPdfCondizioniBase64 = async (
condizioniId: TestiEStringheStingaId,
) => {
const pdf = await genPdfCondizioni(condizioniId);
const pdfBase64 = Buffer.from(pdf).toString("base64");
const base64Content = pdfBase64.includes("base64,")
? pdfBase64.split("base64,")[1]
: pdfBase64;
if (!base64Content) {
throw new Error("Failed to convert PDF to base64");
}
return base64Content;
};
export const genPdfCondizioni = async (
condizioniId: TestiEStringheStingaId,
) => {
const keybd = getKeydbClient(); const keybd = getKeydbClient();
const url = `/servizio/condizioni/${condizioniId}?raw=true`; const url = `/servizio/condizioni/${condizioniId}?raw=true`;
if (keybd) { if (keybd) {
@ -109,7 +131,7 @@ export const genPdfCondizioni = async (condizioniId: AnnunciId) => {
if (cached) { if (cached) {
return new Uint8Array(Buffer.from(cached, "base64")); return new Uint8Array(Buffer.from(cached, "base64"));
} }
const scheda = await puppeteerGenPdf(url); const scheda = await puppeteerGenPdf({ url });
await keybd.set( await keybd.set(
cacheKey, cacheKey,
Buffer.from(scheda).toString("base64"), Buffer.from(scheda).toString("base64"),
@ -118,10 +140,31 @@ export const genPdfCondizioni = async (condizioniId: AnnunciId) => {
); );
return scheda; return scheda;
} }
return await puppeteerGenPdf(url); return await puppeteerGenPdf({ url });
}; };
const puppeteerGenPdf = async (url: string) => { export const genPdfRicevutaCortesiaBase64 = async (
ordineId: OrdiniOrdineId,
) => {
const url = `/servizio/ricevuta-cortesia/${ordineId}?raw=true`;
const pdf = await puppeteerGenPdf({ url });
const pdfBase64 = Buffer.from(pdf).toString("base64");
const base64Content = pdfBase64.includes("base64,")
? pdfBase64.split("base64,")[1]
: pdfBase64;
if (!base64Content) {
throw new Error("Failed to convert PDF to base64");
}
return base64Content;
};
const puppeteerGenPdf = async ({
url,
options,
}: {
url: string;
options?: PDFOptions;
}) => {
let browser = null; let browser = null;
try { try {
@ -155,7 +198,7 @@ const puppeteerGenPdf = async (url: string) => {
const pdf = await page.pdf({ const pdf = await page.pdf({
format: "A4", format: "A4",
printBackground: true, ...options,
}); });
return pdf; return pdf;
} catch (error) { } catch (error) {

View file

@ -1,7 +1,5 @@
import type { Prettify } from "TypeHelpers.type";
import { TRPCError } from "@trpc/server"; import { TRPCError } from "@trpc/server";
import type { NextApiRequest, NextApiResponse } from "next"; import type { NextApiRequest, NextApiResponse } from "next";
import type { Context, Session } from "~/server/api/trpc";
export const checkCtx = (props: { export const checkCtx = (props: {
res: NextApiResponse | undefined; res: NextApiResponse | undefined;
@ -11,29 +9,3 @@ export const checkCtx = (props: {
if (!req || !res) if (!req || !res)
throw new TRPCError({ code: "BAD_REQUEST", message: "res o req mancanti" }); throw new TRPCError({ code: "BAD_REQUEST", message: "res o req mancanti" });
}; };
type CtxWSession = Prettify<Context & { session: Session }>;
export const checkCtxSession = ({ ctx }: { ctx: Context }) => {
if (!ctx.session) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "Utente non autorizzato",
});
}
return ctx as CtxWSession;
};
export const checkCtxAdmin = ({ ctx }: { ctx: Context }) => {
const ckd_ctx = checkCtxSession({ ctx });
if (!ckd_ctx.session.isAdmin) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "Utente non autorizzato",
});
}
return ckd_ctx as Context & {
session: Session & {
isAdmin: true;
};
};
};