diff --git a/package.json b/package.json
index f251a24..64f7eb8 100644
--- a/package.json
+++ b/package.json
@@ -10,6 +10,7 @@
},
"dependencies": {
"@auth/prisma-adapter": "^2.11.1",
+ "@encryptid/sdk": "file:../encryptid-sdk",
"@prisma/client": "^6.19.2",
"bcryptjs": "^3.0.3",
"class-variance-authority": "^0.7.1",
diff --git a/prisma/schema.prisma b/prisma/schema.prisma
index 947d322..eaede54 100644
--- a/prisma/schema.prisma
+++ b/prisma/schema.prisma
@@ -72,7 +72,8 @@ model Space {
name String
slug String @unique
description String? @db.Text
- isPublic Boolean @default(false)
+ visibility String @default("public_read") // public, public_read, authenticated, members_only
+ ownerDid String?
// Configurable per-space voting parameters
promotionThreshold Int @default(100)
diff --git a/src/app/api/spaces/[slug]/route.ts b/src/app/api/spaces/[slug]/route.ts
index c2a7a37..744c17d 100644
--- a/src/app/api/spaces/[slug]/route.ts
+++ b/src/app/api/spaces/[slug]/route.ts
@@ -1,9 +1,10 @@
import { auth } from "@/lib/auth";
import { prisma } from "@/lib/prisma";
import { requireSpaceAdmin, requireSpaceMembership } from "@/lib/spaces";
+import { checkSpaceAccess } from "@encryptid/sdk/server/nextjs";
import { NextRequest, NextResponse } from "next/server";
-// GET /api/spaces/[slug] — Get space details
+// GET /api/spaces/[slug] — Get space details (respects visibility)
export async function GET(
req: NextRequest,
{ params }: { params: Promise<{ slug: string }> }
@@ -21,7 +22,24 @@ export async function GET(
return NextResponse.json({ error: "Space not found" }, { status: 404 });
}
- return NextResponse.json(space);
+ // Check space visibility
+ const access = await checkSpaceAccess(req, slug, {
+ getSpaceConfig: async () => ({
+ spaceSlug: slug,
+ visibility: (space.visibility as any) || "public_read",
+ ownerDID: space.ownerDid || undefined,
+ app: "rvote",
+ }),
+ });
+
+ if (!access.allowed) {
+ return NextResponse.json(
+ { error: access.reason },
+ { status: access.claims ? 403 : 401 }
+ );
+ }
+
+ return NextResponse.json({ ...space, readOnly: access.readOnly });
}
// PATCH /api/spaces/[slug] — Update space (admin only)
@@ -44,7 +62,7 @@ export async function PATCH(
const body = await req.json();
const allowedFields = [
- "name", "description", "isPublic",
+ "name", "description", "visibility",
"promotionThreshold", "votingPeriodDays",
"creditsPerDay", "maxCredits", "startingCredits",
];
diff --git a/src/app/api/spaces/route.ts b/src/app/api/spaces/route.ts
index f15b8f4..96b1e78 100644
--- a/src/app/api/spaces/route.ts
+++ b/src/app/api/spaces/route.ts
@@ -40,7 +40,16 @@ export async function POST(req: NextRequest) {
}
const body = await req.json();
- const { name, description, slug: requestedSlug } = body;
+ const { name, description, slug: requestedSlug, visibility = "public_read" } = body;
+
+ // Validate visibility
+ const validVisibilities = ["public", "public_read", "authenticated", "members_only"];
+ if (!validVisibilities.includes(visibility)) {
+ return NextResponse.json(
+ { error: `Invalid visibility. Must be one of: ${validVisibilities.join(", ")}` },
+ { status: 400 }
+ );
+ }
if (!name || typeof name !== "string" || name.trim().length === 0) {
return NextResponse.json({ error: "Name is required" }, { status: 400 });
@@ -67,6 +76,8 @@ export async function POST(req: NextRequest) {
name: name.trim(),
slug,
description: description?.trim() || null,
+ visibility,
+ ownerDid: session.user.did || null,
},
});
diff --git a/src/app/s/[slug]/layout.tsx b/src/app/s/[slug]/layout.tsx
index d579fbc..c4f37e3 100644
--- a/src/app/s/[slug]/layout.tsx
+++ b/src/app/s/[slug]/layout.tsx
@@ -41,7 +41,7 @@ export default async function SpaceLayout({
name: space.name,
slug: space.slug,
description: space.description,
- isPublic: space.isPublic,
+ visibility: space.visibility,
promotionThreshold: space.promotionThreshold,
votingPeriodDays: space.votingPeriodDays,
creditsPerDay: space.creditsPerDay,
diff --git a/src/components/SpaceCard.tsx b/src/components/SpaceCard.tsx
index 739595a..4deb48c 100644
--- a/src/components/SpaceCard.tsx
+++ b/src/components/SpaceCard.tsx
@@ -8,7 +8,7 @@ interface SpaceCardProps {
name: string;
slug: string;
description: string | null;
- isPublic: boolean;
+ visibility: string;
_count: {
members: number;
proposals: number;
@@ -32,9 +32,15 @@ export function SpaceCard({ space }: SpaceCardProps) {
{space.role === "ADMIN" && (
Admin
)}
- {space.isPublic && (
+ {(space.visibility === "public" || space.visibility === "public_read") && (
Public
)}
+ {space.visibility === "authenticated" && (
+ Login Required
+ )}
+ {space.visibility === "members_only" && (
+ Members Only
+ )}
{space.description && (
diff --git a/src/components/SpaceProvider.tsx b/src/components/SpaceProvider.tsx
index a05d005..665e4ea 100644
--- a/src/components/SpaceProvider.tsx
+++ b/src/components/SpaceProvider.tsx
@@ -8,7 +8,7 @@ interface SpaceContextValue {
name: string;
slug: string;
description: string | null;
- isPublic: boolean;
+ visibility: string;
promotionThreshold: number;
votingPeriodDays: number;
creditsPerDay: number;
diff --git a/src/lib/encryptid.ts b/src/lib/encryptid.ts
index 5b494b6..f6977fc 100644
--- a/src/lib/encryptid.ts
+++ b/src/lib/encryptid.ts
@@ -1,12 +1,16 @@
/**
* EncryptID configuration for rvote-online
+ *
+ * Uses @encryptid/sdk for token verification instead of manual HTTP calls.
*/
+import { verifyEncryptIDToken as sdkVerify } from '@encryptid/sdk/server';
+
export const ENCRYPTID_SERVER_URL =
process.env.ENCRYPTID_SERVER_URL || 'https://encryptid.jeffemmett.com';
/**
- * Verify an EncryptID JWT token by calling the EncryptID server.
+ * Verify an EncryptID JWT token.
* Returns claims if valid, null if invalid.
*/
export async function verifyEncryptIDToken(token: string): Promise<{
@@ -16,22 +20,15 @@ export async function verifyEncryptIDToken(token: string): Promise<{
exp?: number;
} | null> {
try {
- const res = await fetch(`${ENCRYPTID_SERVER_URL}/api/session/verify`, {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({ token }),
+ const claims = await sdkVerify(token, {
+ serverUrl: ENCRYPTID_SERVER_URL,
});
-
- const data = await res.json();
- if (data.valid) {
- return {
- sub: data.userId,
- username: data.username,
- did: data.did,
- exp: data.exp,
- };
- }
- return null;
+ return {
+ sub: claims.sub,
+ username: claims.username,
+ did: claims.did,
+ exp: claims.exp,
+ };
} catch {
return null;
}