feat: pagina inbox e valida tokens do companion

This commit is contained in:
Felipe Coutinho
2026-03-20 18:40:13 +00:00
parent 3c31ee5d90
commit 29551ee02f
12 changed files with 451 additions and 185 deletions

View File

@@ -1,29 +1,55 @@
import { InboxPage } from "@/features/inbox/components/inbox-page";
import {
type ResolvedInboxSearchParams,
resolveInboxPagination,
resolveInboxStatus,
} from "@/features/inbox/page-helpers";
import {
fetchAppLogoMap,
fetchInboxDialogData,
fetchInboxItems,
fetchInboxItemsPage,
fetchInboxStatusCounts,
} from "@/features/inbox/queries";
import { getUserId } from "@/shared/lib/auth/server";
export default async function Page() {
const userId = await getUserId();
type PageSearchParams = Promise<ResolvedInboxSearchParams>;
const [pendingItems, processedItems, discardedItems, dialogData, appLogoMap] =
await Promise.all([
fetchInboxItems(userId, "pending"),
fetchInboxItems(userId, "processed"),
fetchInboxItems(userId, "discarded"),
fetchInboxDialogData(userId),
fetchAppLogoMap(userId),
]);
type PageProps = {
searchParams?: PageSearchParams;
};
const EMPTY_DIALOG_DATA = {
payerOptions: [],
splitPayerOptions: [],
defaultPayerId: null,
accountOptions: [],
cardOptions: [],
categoryOptions: [],
estabelecimentos: [],
};
export default async function Page({ searchParams }: PageProps) {
const userId = await getUserId();
const resolvedSearchParams = searchParams ? await searchParams : undefined;
const activeStatus = resolveInboxStatus(resolvedSearchParams);
const paginationInput = resolveInboxPagination(resolvedSearchParams);
const [itemsPage, counts, dialogData, appLogoMap] = await Promise.all([
fetchInboxItemsPage(userId, activeStatus, paginationInput),
fetchInboxStatusCounts(userId),
activeStatus === "pending"
? fetchInboxDialogData(userId)
: Promise.resolve(EMPTY_DIALOG_DATA),
fetchAppLogoMap(userId),
]);
return (
<main className="flex flex-col items-start gap-6">
<InboxPage
pendingItems={pendingItems}
processedItems={processedItems}
discardedItems={discardedItems}
activeStatus={activeStatus}
items={itemsPage.items}
counts={counts}
pagination={itemsPage.pagination}
payerOptions={dialogData.payerOptions}
splitPayerOptions={dialogData.splitPayerOptions}
defaultPayerId={dialogData.defaultPayerId}

View File

@@ -1,4 +1,4 @@
import { and, eq, isNull } from "drizzle-orm";
import { and, eq, gt, isNull } from "drizzle-orm";
import { NextResponse } from "next/server";
import { apiTokens } from "@/db/schema";
import {
@@ -38,6 +38,7 @@ export async function POST(request: Request) {
eq(apiTokens.id, payload.tokenId),
eq(apiTokens.userId, payload.sub),
isNull(apiTokens.revokedAt),
gt(apiTokens.expiresAt, new Date()),
),
});
@@ -65,8 +66,9 @@ export async function POST(request: Request) {
tokenHash: hashToken(result.accessToken),
lastUsedAt: new Date(),
lastUsedIp:
request.headers.get("x-forwarded-for") ||
request.headers.get("x-real-ip"),
request.headers.get("x-forwarded-for")?.split(",")[0]?.trim() ||
request.headers.get("x-real-ip") ||
null,
expiresAt: result.expiresAt,
})
.where(eq(apiTokens.id, payload.tokenId));

View File

@@ -39,7 +39,9 @@ export async function DELETE(_request: Request, { params }: RouteParams) {
await db
.update(apiTokens)
.set({ revokedAt: new Date() })
.where(eq(apiTokens.id, tokenId));
.where(
and(eq(apiTokens.id, tokenId), eq(apiTokens.userId, session.user.id)),
);
return NextResponse.json({
message: "Token revogado com sucesso",

View File

@@ -1,4 +1,4 @@
import { and, eq, isNull } from "drizzle-orm";
import { and, eq, gt, isNull } from "drizzle-orm";
import { NextResponse } from "next/server";
import { apiTokens } from "@/db/schema";
import { extractBearerToken, hashToken } from "@/shared/lib/auth/api-token";
@@ -33,6 +33,7 @@ export async function POST(request: Request) {
where: and(
eq(apiTokens.tokenHash, tokenHash),
isNull(apiTokens.revokedAt),
gt(apiTokens.expiresAt, new Date()),
),
});

View File

@@ -1,4 +1,4 @@
import { and, eq, isNull } from "drizzle-orm";
import { and, eq, gt, isNull, or } from "drizzle-orm";
import { NextResponse } from "next/server";
import { z } from "zod";
import { apiTokens, inboxItems } from "@/db/schema";
@@ -63,6 +63,7 @@ export async function POST(request: Request) {
where: and(
eq(apiTokens.tokenHash, tokenHash),
isNull(apiTokens.revokedAt),
or(isNull(apiTokens.expiresAt), gt(apiTokens.expiresAt, new Date())),
),
});
@@ -111,10 +112,11 @@ export async function POST(request: Request) {
success: true,
});
} catch (error) {
console.error("[API] Error processing batch item:", error);
results.push({
clientId: item.clientId,
success: false,
error: error instanceof Error ? error.message : "Erro desconhecido",
error: "Erro ao processar notificação",
});
}
}

View File

@@ -1,4 +1,4 @@
import { and, eq, isNull } from "drizzle-orm";
import { and, eq, gt, isNull, or } from "drizzle-orm";
import { NextResponse } from "next/server";
import { z } from "zod";
import { apiTokens, inboxItems } from "@/db/schema";
@@ -56,6 +56,7 @@ export async function POST(request: Request) {
where: and(
eq(apiTokens.tokenHash, tokenHash),
isNull(apiTokens.revokedAt),
or(isNull(apiTokens.expiresAt), gt(apiTokens.expiresAt, new Date())),
),
});