feat(core-realtime): authorize function (4 scope kinds)
This commit is contained in:
60
packages/core-realtime/src/authorize.test.ts
Normal file
60
packages/core-realtime/src/authorize.test.ts
Normal file
@@ -0,0 +1,60 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { z } from "zod";
|
||||
import { authorize } from "@/authorize";
|
||||
import { defineRealtimeChannel } from "@/realtime-channel";
|
||||
|
||||
const schema = z.object({}).strict();
|
||||
|
||||
describe("authorize", () => {
|
||||
describe("public", () => {
|
||||
const ch = defineRealtimeChannel("a", schema, { scope: "public" });
|
||||
it("allows anonymous", async () => {
|
||||
expect(await authorize(ch, {}, null)).toBe(true);
|
||||
});
|
||||
it("allows authenticated", async () => {
|
||||
expect(await authorize(ch, {}, { userId: "u1", roles: [] })).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("authenticated", () => {
|
||||
const ch = defineRealtimeChannel("a", schema, { scope: "authenticated" });
|
||||
it("rejects anonymous", async () => {
|
||||
expect(await authorize(ch, {}, null)).toBe(false);
|
||||
});
|
||||
it("allows any user", async () => {
|
||||
expect(await authorize(ch, {}, { userId: "u1", roles: [] })).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("{ role }", () => {
|
||||
const ch = defineRealtimeChannel("a", schema, { scope: { role: "admin" } });
|
||||
it("rejects anonymous", async () => {
|
||||
expect(await authorize(ch, {}, null)).toBe(false);
|
||||
});
|
||||
it("rejects user without role", async () => {
|
||||
expect(await authorize(ch, {}, { userId: "u1", roles: ["editor"] })).toBe(false);
|
||||
});
|
||||
it("allows user with role", async () => {
|
||||
expect(await authorize(ch, {}, { userId: "u1", roles: ["admin", "editor"] })).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("{ userScoped }", () => {
|
||||
const ch = defineRealtimeChannel("a", schema, {
|
||||
scope: { userScoped: true, template: "notifications.user.{userId}" },
|
||||
});
|
||||
it("rejects anonymous", async () => {
|
||||
expect(await authorize(ch, { userId: "u1" }, null)).toBe(false);
|
||||
});
|
||||
it("rejects user requesting someone else's channel", async () => {
|
||||
expect(
|
||||
await authorize(ch, { userId: "u_other" }, { userId: "u1", roles: [] }),
|
||||
).toBe(false);
|
||||
});
|
||||
it("allows user requesting own channel", async () => {
|
||||
expect(
|
||||
await authorize(ch, { userId: "u1" }, { userId: "u1", roles: [] }),
|
||||
).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
22
packages/core-realtime/src/authorize.ts
Normal file
22
packages/core-realtime/src/authorize.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import type { z } from "zod";
|
||||
import type { RealtimeChannelDescriptor } from "./realtime-channel";
|
||||
|
||||
export async function authorize(
|
||||
descriptor: RealtimeChannelDescriptor<string, z.ZodType>,
|
||||
params: Record<string, string>,
|
||||
user: { userId: string; roles: string[] } | null,
|
||||
): Promise<boolean> {
|
||||
const scope = descriptor.scope;
|
||||
|
||||
if (scope === "public") return true;
|
||||
if (scope === "authenticated") return user !== null;
|
||||
|
||||
if (typeof scope === "object" && "role" in scope) {
|
||||
return user !== null && user.roles.includes(scope.role);
|
||||
}
|
||||
if (typeof scope === "object" && "userScoped" in scope) {
|
||||
return user !== null && params.userId === user.userId;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
Reference in New Issue
Block a user