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