feat(auth) : sync Sudloh profiles and validate sessions

This commit is contained in:
2026-10-06 00:48:29 +07:00 Unverified
parent d6153be50d
commit c856904c12
16 changed files with 520 additions and 32 deletions
+12 -1
View File
@@ -7,9 +7,13 @@ const returning = vi.fn();
const where = vi.fn(() => ({ returning }));
const set = vi.fn(() => ({ where }));
const write = vi.fn();
const linkedAccounts = vi.fn();
vi.mock("@/lib/commission/server", () => ({ requireCommissionUser }));
vi.mock("@/db", () => ({ getDb: () => ({ update: () => ({ set }) }) }));
vi.mock("@/db", () => ({ getDb: () => ({
update: () => ({ set }),
select: () => ({ from: () => ({ where: () => ({ limit: linkedAccounts }) }) }),
}) }));
vi.mock("@/lib/media/storage", () => ({ getMediaStorage: async () => ({ write }), publicMediaUrl: (key: string) => `https://cdn.test/${key}` }));
vi.mock("@/lib/security/rate-limit", () => ({ limitRequest: async () => undefined }));
@@ -27,6 +31,7 @@ describe("profile update", () => {
process.env.BETTER_AUTH_URL = "https://guide.sudloh.com";
vi.clearAllMocks();
requireCommissionUser.mockResolvedValue({ id: "user-1", image: null });
linkedAccounts.mockResolvedValue([]);
returning.mockResolvedValue([{ name: "New Name", image: null }]);
});
@@ -60,4 +65,10 @@ describe("profile update", () => {
expect((await POST(profileRequest("New Name"))).status).toBe(401);
expect(set).not.toHaveBeenCalled();
});
it("sends Sudloh-linked users to Sudloh for profile changes", async () => {
linkedAccounts.mockResolvedValueOnce([{ id: "sudloh-account" }]);
expect((await POST(profileRequest("New Name"))).status).toBe(403);
expect(set).not.toHaveBeenCalled();
});
});
+5 -2
View File
@@ -1,7 +1,7 @@
import { eq } from "drizzle-orm";
import { and, eq } from "drizzle-orm";
import sharp from "sharp";
import { getDb } from "@/db";
import { users } from "@/db/schema";
import { accounts, users } from "@/db/schema";
import { requireCommissionUser } from "@/lib/commission/server";
import { inspectImage } from "@/lib/media/inspect";
import { getMediaStorage, publicMediaUrl } from "@/lib/media/storage";
@@ -15,6 +15,9 @@ export async function POST(request: Request) {
try {
requireSameOrigin(request);
const user = await requireCommissionUser();
const [sudloh] = await getDb().select({ id: accounts.id }).from(accounts)
.where(and(eq(accounts.userId, user.id), eq(accounts.providerId, "sudloh"))).limit(1);
if (sudloh) throw new HttpError(403, "manage-profile-at-sudloh");
await limitRequest("profile-update", user.id, 20);
if (!request.headers.get("content-type")?.startsWith("multipart/form-data;"))
throw new HttpError(415, "expected-multipart");
+16
View File
@@ -0,0 +1,16 @@
import { getCustomerSession, isSudlohOidcEnabled } from "@/lib/auth/server";
import { refreshLinkedSudlohProfile, validateSudlohSession } from "@/lib/auth/sudloh";
import { errorResponse, HttpError, requireSameOrigin } from "@/lib/security/http";
export async function POST(request: Request) {
try {
requireSameOrigin(request);
const session = await getCustomerSession();
if (!session) throw new HttpError(401, "unauthorized");
if (isSudlohOidcEnabled() && process.env.SUDLOH_OIDC_ONLY === "true") {
if (!await validateSudlohSession(session.user.id, session.session.id, true))
throw new HttpError(401, "sudloh-session-expired");
} else await refreshLinkedSudlohProfile(session.user.id);
return Response.json({ ok: true }, { headers: { "Cache-Control": "no-store" } });
} catch (cause) { return errorResponse(cause); }
}