feat: migrate from NextAuth to pure EncryptID auth
Replace the NextAuth Credentials provider wrapper with direct EncryptID SDK integration. The auth() function now reads the encryptid_token cookie directly, keeping the same interface so all API routes and server components work unchanged. - Replace SessionProvider with EncryptIDProvider - Add /api/auth/session endpoint for cookie management - Update signin/signup pages to use SDK login/register + cookie sync - Update Navbar to use useEncryptID() hook - Remove next-auth, @auth/prisma-adapter, bcryptjs dependencies - Drop Account, Session, VerificationToken Prisma models + DB tables - Remove legacy email/password register route Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
8ed22c9caa
commit
a9866f4a32
|
|
@ -12,8 +12,8 @@ services:
|
|||
- "traefik.docker.network=traefik-public"
|
||||
environment:
|
||||
- DATABASE_URL=postgresql://rvote:${DB_PASSWORD}@postgres:5432/rvote
|
||||
- NEXTAUTH_SECRET=${NEXTAUTH_SECRET}
|
||||
- NEXTAUTH_URL=https://rvote.online
|
||||
- ENCRYPTID_SERVER_URL=https://encryptid.jeffemmett.com
|
||||
- NEXT_PUBLIC_ENCRYPTID_SERVER_URL=https://encryptid.jeffemmett.com
|
||||
- SMTP_HOST=${SMTP_HOST:-mx.jeffemmett.com}
|
||||
- SMTP_PORT=${SMTP_PORT:-587}
|
||||
- SMTP_USER=${SMTP_USER:-noreply@jeffemmett.com}
|
||||
|
|
|
|||
|
|
@ -9,16 +9,13 @@
|
|||
"lint": "eslint"
|
||||
},
|
||||
"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",
|
||||
"clsx": "^2.1.1",
|
||||
"date-fns": "^4.1.0",
|
||||
"lucide-react": "^0.563.0",
|
||||
"next": "16.1.6",
|
||||
"next-auth": "^5.0.0-beta.30",
|
||||
"next-themes": "^0.4.6",
|
||||
"prisma": "^6.19.2",
|
||||
"radix-ui": "^1.4.3",
|
||||
|
|
@ -29,7 +26,6 @@
|
|||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/postcss": "^4",
|
||||
"@types/bcryptjs": "^2.4.6",
|
||||
"@types/node": "^20",
|
||||
"@types/react": "^19",
|
||||
"@types/react-dom": "^19",
|
||||
|
|
|
|||
|
|
@ -22,49 +22,10 @@ model User {
|
|||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
// NextAuth fields
|
||||
accounts Account[]
|
||||
sessions Session[]
|
||||
|
||||
// Space memberships
|
||||
spaceMemberships SpaceMember[]
|
||||
}
|
||||
|
||||
model Account {
|
||||
id String @id @default(cuid())
|
||||
userId String
|
||||
type String
|
||||
provider String
|
||||
providerAccountId String
|
||||
refresh_token String? @db.Text
|
||||
access_token String? @db.Text
|
||||
expires_at Int?
|
||||
token_type String?
|
||||
scope String?
|
||||
id_token String? @db.Text
|
||||
session_state String?
|
||||
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@unique([provider, providerAccountId])
|
||||
}
|
||||
|
||||
model Session {
|
||||
id String @id @default(cuid())
|
||||
sessionToken String @unique
|
||||
userId String
|
||||
expires DateTime
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
}
|
||||
|
||||
model VerificationToken {
|
||||
identifier String
|
||||
token String @unique
|
||||
expires DateTime
|
||||
|
||||
@@unique([identifier, token])
|
||||
}
|
||||
|
||||
// ─── Spaces (Multi-Tenant) ───────────────────────────────────────────
|
||||
|
||||
model Space {
|
||||
|
|
|
|||
|
|
@ -1,3 +0,0 @@
|
|||
import { handlers } from "@/lib/auth";
|
||||
|
||||
export const { GET, POST } = handlers;
|
||||
|
|
@ -1,73 +0,0 @@
|
|||
import { prisma } from "@/lib/prisma";
|
||||
import bcrypt from "bcryptjs";
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
|
||||
export async function POST(req: NextRequest) {
|
||||
try {
|
||||
const { email, password, name } = await req.json();
|
||||
|
||||
if (!email || !password) {
|
||||
return NextResponse.json(
|
||||
{ error: "Email and password are required" },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
// Validate email format
|
||||
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||
if (!emailRegex.test(email)) {
|
||||
return NextResponse.json(
|
||||
{ error: "Invalid email format" },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
// Validate password strength
|
||||
if (password.length < 8) {
|
||||
return NextResponse.json(
|
||||
{ error: "Password must be at least 8 characters" },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
// Check if user already exists
|
||||
const existingUser = await prisma.user.findUnique({
|
||||
where: { email: email.toLowerCase() },
|
||||
});
|
||||
|
||||
if (existingUser) {
|
||||
return NextResponse.json(
|
||||
{ error: "An account with this email already exists" },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
// Hash password
|
||||
const passwordHash = await bcrypt.hash(password, 12);
|
||||
|
||||
// Create user with initial credits
|
||||
const user = await prisma.user.create({
|
||||
data: {
|
||||
email: email.toLowerCase(),
|
||||
passwordHash,
|
||||
name: name || null,
|
||||
credits: 50, // Starting credits
|
||||
emailVerified: new Date(), // Auto-verify for now
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
email: true,
|
||||
name: true,
|
||||
credits: true,
|
||||
},
|
||||
});
|
||||
|
||||
return NextResponse.json({ user }, { status: 201 });
|
||||
} catch (error) {
|
||||
console.error("Registration error:", error);
|
||||
return NextResponse.json(
|
||||
{ error: "Failed to create account" },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,74 @@
|
|||
import { verifyEncryptIDToken } from '@encryptid/sdk/server';
|
||||
import { prisma } from '@/lib/prisma';
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
|
||||
const SERVER_URL =
|
||||
process.env.ENCRYPTID_SERVER_URL || 'https://encryptid.jeffemmett.com';
|
||||
|
||||
/**
|
||||
* POST /api/auth/session — Verify EncryptID token and set session cookie.
|
||||
* Called by signin/signup pages after successful WebAuthn ceremony.
|
||||
*/
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const { token } = await request.json();
|
||||
if (!token || typeof token !== 'string') {
|
||||
return NextResponse.json({ error: 'Token required' }, { status: 400 });
|
||||
}
|
||||
|
||||
const claims = await verifyEncryptIDToken(token, { serverUrl: SERVER_URL });
|
||||
const did: string | undefined = claims.did || claims.sub;
|
||||
if (!did) {
|
||||
return NextResponse.json({ error: 'Invalid token' }, { status: 401 });
|
||||
}
|
||||
|
||||
// Upsert user in DB
|
||||
const user = await prisma.user.upsert({
|
||||
where: { did },
|
||||
update: { name: claims.username || undefined },
|
||||
create: {
|
||||
did,
|
||||
email: `${did}@encryptid.local`,
|
||||
name: claims.username || null,
|
||||
credits: 50,
|
||||
emailVerified: new Date(),
|
||||
},
|
||||
});
|
||||
|
||||
const response = NextResponse.json({
|
||||
user: {
|
||||
id: user.id,
|
||||
email: user.email,
|
||||
name: user.name,
|
||||
did: user.did,
|
||||
},
|
||||
});
|
||||
|
||||
response.cookies.set('encryptid_token', token, {
|
||||
httpOnly: true,
|
||||
secure: process.env.NODE_ENV === 'production',
|
||||
sameSite: 'lax',
|
||||
path: '/',
|
||||
maxAge: 60 * 60 * 24 * 7, // 7 days
|
||||
});
|
||||
|
||||
return response;
|
||||
} catch {
|
||||
return NextResponse.json({ error: 'Invalid token' }, { status: 401 });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* DELETE /api/auth/session — Clear session cookie (sign out).
|
||||
*/
|
||||
export async function DELETE() {
|
||||
const response = NextResponse.json({ ok: true });
|
||||
response.cookies.set('encryptid_token', '', {
|
||||
httpOnly: true,
|
||||
secure: process.env.NODE_ENV === 'production',
|
||||
sameSite: 'lax',
|
||||
path: '/',
|
||||
maxAge: 0,
|
||||
});
|
||||
return response;
|
||||
}
|
||||
|
|
@ -1,22 +1,19 @@
|
|||
"use client";
|
||||
|
||||
import { useState, Suspense } from "react";
|
||||
import { signIn } from "next-auth/react";
|
||||
import { useRouter, useSearchParams } from "next/navigation";
|
||||
import Link from "next/link";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Loader2, Fingerprint } from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
import { EncryptIDClient } from "@encryptid/sdk/client";
|
||||
|
||||
const ENCRYPTID_SERVER = process.env.NEXT_PUBLIC_ENCRYPTID_SERVER_URL || "https://encryptid.jeffemmett.com";
|
||||
const encryptid = new EncryptIDClient(ENCRYPTID_SERVER);
|
||||
import { useEncryptID } from "@encryptid/sdk/ui/react";
|
||||
|
||||
function SignInForm() {
|
||||
const router = useRouter();
|
||||
const searchParams = useSearchParams();
|
||||
const callbackUrl = searchParams.get("callbackUrl") || "/";
|
||||
const { login } = useEncryptID();
|
||||
|
||||
const [isPasskeyLoading, setIsPasskeyLoading] = useState(false);
|
||||
|
||||
|
|
@ -24,22 +21,22 @@ function SignInForm() {
|
|||
setIsPasskeyLoading(true);
|
||||
|
||||
try {
|
||||
// SDK handles the full WebAuthn ceremony
|
||||
const authResult = await encryptid.authenticate();
|
||||
// SDK handles the full WebAuthn ceremony + stores token in localStorage
|
||||
await login();
|
||||
|
||||
// Exchange EncryptID token for NextAuth session
|
||||
const result = await signIn("encryptid", {
|
||||
token: authResult.token,
|
||||
redirect: false,
|
||||
});
|
||||
|
||||
if (result?.error) {
|
||||
toast.error("Passkey verified but session creation failed");
|
||||
} else {
|
||||
toast.success("Signed in with passkey!");
|
||||
router.push(callbackUrl);
|
||||
router.refresh();
|
||||
// Get the token from localStorage and set it as a cookie for SSR
|
||||
const token = localStorage.getItem("encryptid_token");
|
||||
if (token) {
|
||||
await fetch("/api/auth/session", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ token }),
|
||||
});
|
||||
}
|
||||
|
||||
toast.success("Signed in with passkey!");
|
||||
router.push(callbackUrl);
|
||||
router.refresh();
|
||||
} catch (error: any) {
|
||||
if (error.name === "NotAllowedError") {
|
||||
toast.error("No passkey found. Create an account first.");
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { signIn } from "next-auth/react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import Link from "next/link";
|
||||
import { Button } from "@/components/ui/button";
|
||||
|
|
@ -10,13 +9,11 @@ import { Label } from "@/components/ui/label";
|
|||
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Loader2, Fingerprint } from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
import { EncryptIDClient } from "@encryptid/sdk/client";
|
||||
|
||||
const ENCRYPTID_SERVER = process.env.NEXT_PUBLIC_ENCRYPTID_SERVER_URL || "https://encryptid.jeffemmett.com";
|
||||
const encryptid = new EncryptIDClient(ENCRYPTID_SERVER);
|
||||
import { useEncryptID } from "@encryptid/sdk/ui/react";
|
||||
|
||||
export default function SignUpPage() {
|
||||
const router = useRouter();
|
||||
const { register } = useEncryptID();
|
||||
|
||||
const [name, setName] = useState("");
|
||||
const [isPasskeyLoading, setIsPasskeyLoading] = useState(false);
|
||||
|
|
@ -27,23 +24,22 @@ export default function SignUpPage() {
|
|||
try {
|
||||
const username = name.trim() || `user-${Date.now().toString(36)}`;
|
||||
|
||||
// SDK handles the full WebAuthn registration ceremony
|
||||
const regResult = await encryptid.register(username, name || username);
|
||||
// SDK handles the full WebAuthn registration ceremony + stores token
|
||||
await register(username, name || username);
|
||||
|
||||
// Exchange EncryptID token for NextAuth session
|
||||
const result = await signIn("encryptid", {
|
||||
token: regResult.token,
|
||||
redirect: false,
|
||||
});
|
||||
|
||||
if (result?.error) {
|
||||
toast.error("Passkey registered but session creation failed");
|
||||
router.push("/auth/signin");
|
||||
} else {
|
||||
toast.success("Account created with passkey! Welcome to rVote.");
|
||||
router.push("/");
|
||||
router.refresh();
|
||||
// Get the token from localStorage and set it as a cookie for SSR
|
||||
const token = localStorage.getItem("encryptid_token");
|
||||
if (token) {
|
||||
await fetch("/api/auth/session", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ token }),
|
||||
});
|
||||
}
|
||||
|
||||
toast.success("Account created with passkey! Welcome to rVote.");
|
||||
router.push("/");
|
||||
router.refresh();
|
||||
} catch (error: any) {
|
||||
if (error.name === "NotAllowedError") {
|
||||
toast.error("Passkey registration was cancelled");
|
||||
|
|
|
|||
|
|
@ -0,0 +1,13 @@
|
|||
'use client';
|
||||
|
||||
import { EncryptIDProvider } from '@encryptid/sdk/ui/react';
|
||||
|
||||
export function AuthProvider({ children }: { children: React.ReactNode }) {
|
||||
// Cast to any to bypass React 18/19 type mismatch between SDK and app
|
||||
const Provider = EncryptIDProvider as any;
|
||||
return (
|
||||
<Provider serverUrl={process.env.NEXT_PUBLIC_ENCRYPTID_SERVER_URL}>
|
||||
{children}
|
||||
</Provider>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
"use client";
|
||||
|
||||
import Link from "next/link";
|
||||
import { useSession, signOut } from "next-auth/react";
|
||||
import { useEncryptID } from "@encryptid/sdk/ui/react";
|
||||
import { usePathname } from "next/navigation";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { CreditDisplay } from "./CreditDisplay";
|
||||
|
|
@ -15,7 +15,7 @@ import {
|
|||
import { Avatar, AvatarFallback } from "@/components/ui/avatar";
|
||||
|
||||
export function Navbar() {
|
||||
const { data: session, status } = useSession();
|
||||
const { isAuthenticated, username, did, loading, logout } = useEncryptID();
|
||||
const pathname = usePathname();
|
||||
|
||||
// Hide the main navbar on space pages — SpaceNav handles navigation there
|
||||
|
|
@ -23,6 +23,14 @@ export function Navbar() {
|
|||
return null;
|
||||
}
|
||||
|
||||
async function handleSignOut() {
|
||||
// Clear the server-side cookie
|
||||
await fetch("/api/auth/session", { method: "DELETE" });
|
||||
// Clear client-side state
|
||||
logout();
|
||||
window.location.href = "/";
|
||||
}
|
||||
|
||||
return (
|
||||
<nav className="border-b bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60">
|
||||
<div className="container mx-auto px-4">
|
||||
|
|
@ -48,9 +56,9 @@ export function Navbar() {
|
|||
</div>
|
||||
|
||||
<div className="flex items-center gap-4">
|
||||
{status === "loading" ? (
|
||||
{loading ? (
|
||||
<div className="h-8 w-20 animate-pulse bg-muted rounded" />
|
||||
) : session?.user ? (
|
||||
) : isAuthenticated ? (
|
||||
<>
|
||||
<CreditDisplay />
|
||||
<DropdownMenu>
|
||||
|
|
@ -61,8 +69,8 @@ export function Navbar() {
|
|||
>
|
||||
<Avatar className="h-8 w-8">
|
||||
<AvatarFallback>
|
||||
{session.user.name?.[0]?.toUpperCase() ||
|
||||
session.user.email?.[0]?.toUpperCase() ||
|
||||
{username?.[0]?.toUpperCase() ||
|
||||
did?.[0]?.toUpperCase() ||
|
||||
"U"}
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
|
|
@ -71,12 +79,12 @@ export function Navbar() {
|
|||
<DropdownMenuContent className="w-56" align="end" forceMount>
|
||||
<div className="flex items-center justify-start gap-2 p-2">
|
||||
<div className="flex flex-col space-y-1 leading-none">
|
||||
{session.user.name && (
|
||||
<p className="font-medium">{session.user.name}</p>
|
||||
{username && (
|
||||
<p className="font-medium">{username}</p>
|
||||
)}
|
||||
{session.user.email && (
|
||||
{did && (
|
||||
<p className="w-[200px] truncate text-sm text-muted-foreground">
|
||||
{session.user.email}
|
||||
{did}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
|
@ -91,7 +99,7 @@ export function Navbar() {
|
|||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem
|
||||
className="cursor-pointer"
|
||||
onClick={() => signOut()}
|
||||
onClick={handleSignOut}
|
||||
>
|
||||
Sign out
|
||||
</DropdownMenuItem>
|
||||
|
|
|
|||
|
|
@ -1,13 +1,13 @@
|
|||
"use client";
|
||||
|
||||
import { SessionProvider } from "next-auth/react";
|
||||
import { AuthProvider } from "./AuthProvider";
|
||||
import { Toaster } from "sonner";
|
||||
|
||||
export function Providers({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<SessionProvider>
|
||||
<AuthProvider>
|
||||
{children}
|
||||
<Toaster position="bottom-right" />
|
||||
</SessionProvider>
|
||||
</AuthProvider>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
127
src/lib/auth.ts
127
src/lib/auth.ts
|
|
@ -1,78 +1,57 @@
|
|||
import NextAuth from "next-auth";
|
||||
import Credentials from "next-auth/providers/credentials";
|
||||
import { PrismaAdapter } from "@auth/prisma-adapter";
|
||||
import { prisma } from "./prisma";
|
||||
import { verifyEncryptIDToken } from "./encryptid";
|
||||
import { verifyEncryptIDToken } from '@encryptid/sdk/server';
|
||||
import { cookies } from 'next/headers';
|
||||
import { prisma } from './prisma';
|
||||
|
||||
export const { handlers, signIn, signOut, auth } = NextAuth({
|
||||
adapter: PrismaAdapter(prisma),
|
||||
trustHost: true,
|
||||
session: {
|
||||
strategy: "jwt",
|
||||
},
|
||||
pages: {
|
||||
signIn: "/auth/signin",
|
||||
error: "/auth/error",
|
||||
},
|
||||
providers: [
|
||||
// EncryptID passkey login — sole auth provider
|
||||
Credentials({
|
||||
id: "encryptid",
|
||||
name: "EncryptID Passkey",
|
||||
credentials: {
|
||||
token: { label: "Token", type: "text" },
|
||||
const SERVER_URL =
|
||||
process.env.ENCRYPTID_SERVER_URL || 'https://encryptid.jeffemmett.com';
|
||||
|
||||
interface AuthSession {
|
||||
user: {
|
||||
id: string;
|
||||
email: string;
|
||||
name: string | null;
|
||||
did: string | null;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current user session.
|
||||
* Works in both server components and API route handlers.
|
||||
* Reads the encryptid_token cookie, verifies it, and upserts the user.
|
||||
*
|
||||
* Drop-in replacement for the old NextAuth `auth()` function.
|
||||
*/
|
||||
export async function auth(): Promise<AuthSession | null> {
|
||||
const cookieStore = await cookies();
|
||||
const token = cookieStore.get('encryptid_token')?.value;
|
||||
if (!token) return null;
|
||||
|
||||
try {
|
||||
const claims = await verifyEncryptIDToken(token, { serverUrl: SERVER_URL });
|
||||
const did: string | undefined = claims.did || claims.sub;
|
||||
if (!did) return null;
|
||||
|
||||
const user = await prisma.user.upsert({
|
||||
where: { did },
|
||||
update: { name: claims.username || undefined },
|
||||
create: {
|
||||
did,
|
||||
email: `${did}@encryptid.local`,
|
||||
name: claims.username || null,
|
||||
credits: 50,
|
||||
emailVerified: new Date(),
|
||||
},
|
||||
async authorize(credentials) {
|
||||
if (!credentials?.token) {
|
||||
return null;
|
||||
}
|
||||
});
|
||||
|
||||
// Verify the EncryptID JWT
|
||||
const claims = await verifyEncryptIDToken(credentials.token as string);
|
||||
if (!claims) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const did = claims.did || claims.sub;
|
||||
|
||||
// Find existing user by DID or create a new one
|
||||
let user = await prisma.user.findFirst({
|
||||
where: { did },
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
// Create new passkey-only user
|
||||
user = await prisma.user.create({
|
||||
data: {
|
||||
email: `${did}@encryptid.local`, // Placeholder email for DID-only users
|
||||
did,
|
||||
name: claims.username || null,
|
||||
credits: 50, // Starting credits
|
||||
emailVerified: new Date(),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
id: user.id,
|
||||
email: user.email,
|
||||
name: user.name,
|
||||
};
|
||||
return {
|
||||
user: {
|
||||
id: user.id,
|
||||
email: user.email,
|
||||
name: user.name,
|
||||
did: user.did,
|
||||
},
|
||||
}),
|
||||
],
|
||||
callbacks: {
|
||||
async jwt({ token, user }) {
|
||||
if (user) {
|
||||
token.id = user.id;
|
||||
}
|
||||
return token;
|
||||
},
|
||||
async session({ session, token }) {
|
||||
if (session.user) {
|
||||
session.user.id = token.id as string;
|
||||
}
|
||||
return session;
|
||||
},
|
||||
},
|
||||
});
|
||||
};
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,35 +0,0 @@
|
|||
/**
|
||||
* 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.
|
||||
* Returns claims if valid, null if invalid.
|
||||
*/
|
||||
export async function verifyEncryptIDToken(token: string): Promise<{
|
||||
sub: string;
|
||||
username?: string;
|
||||
did?: string;
|
||||
exp?: number;
|
||||
} | null> {
|
||||
try {
|
||||
const claims = await sdkVerify(token, {
|
||||
serverUrl: ENCRYPTID_SERVER_URL,
|
||||
});
|
||||
return {
|
||||
sub: claims.sub,
|
||||
username: claims.username,
|
||||
did: claims.did,
|
||||
exp: claims.exp,
|
||||
};
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,12 +1,11 @@
|
|||
/**
|
||||
* Space Role bridge for rVote
|
||||
*
|
||||
* Bridges NextAuth session + EncryptID SDK SpaceRole system.
|
||||
* Bridges EncryptID auth + SDK SpaceRole system.
|
||||
* Resolves the user's effective SpaceRole in the current space
|
||||
* by querying the EncryptID membership server.
|
||||
*/
|
||||
|
||||
import { auth } from './auth';
|
||||
import { prisma } from './prisma';
|
||||
import {
|
||||
SpaceRole,
|
||||
|
|
@ -14,7 +13,6 @@ import {
|
|||
type ResolvedRole,
|
||||
} from '@encryptid/sdk/types';
|
||||
import { RVOTE_PERMISSIONS } from '@encryptid/sdk/types/modules';
|
||||
import type { Session } from 'next-auth';
|
||||
|
||||
const ENCRYPTID_SERVER = process.env.ENCRYPTID_SERVER_URL || 'https://encryptid.jeffemmett.com';
|
||||
|
||||
|
|
@ -105,7 +103,7 @@ export async function resolveUserSpaceRole(
|
|||
* Check if the current session user has a specific rVote capability.
|
||||
*/
|
||||
export async function checkVoteCapability(
|
||||
session: Session | null,
|
||||
session: { user: { id: string } } | null,
|
||||
spaceSlug: string,
|
||||
capability: keyof typeof RVOTE_PERMISSIONS.capabilities,
|
||||
): Promise<boolean> {
|
||||
|
|
|
|||
|
|
@ -1,10 +0,0 @@
|
|||
import { DefaultSession } from "next-auth";
|
||||
|
||||
declare module "next-auth" {
|
||||
interface Session {
|
||||
user: {
|
||||
id: string;
|
||||
did?: string | null;
|
||||
} & DefaultSession["user"];
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue