feat: Upstash Redis for per-user CF credential storage
This commit is contained in:
+29
-31
@@ -1,58 +1,56 @@
|
||||
import { cookies } from "next/headers";
|
||||
import { jwtDecrypt, EncryptJWT } from "jose";
|
||||
import { getServerSession } from "next-auth";
|
||||
import { authOptions } from "@/lib/authOptions";
|
||||
import { redis } from "@/lib/redis";
|
||||
|
||||
export interface CFCreds {
|
||||
accountId: string;
|
||||
apiToken: string;
|
||||
}
|
||||
|
||||
function encKey(): Uint8Array {
|
||||
const s = process.env.NEXTAUTH_SECRET ?? process.env.AUTH_SECRET ?? "dev-secret-change-me";
|
||||
const buf = Buffer.from(s.padEnd(32, "0").slice(0, 32), "utf8");
|
||||
return new Uint8Array(buf);
|
||||
/** Redis key for a given user's CF credentials */
|
||||
function credsKey(userId: string) {
|
||||
return `cf_creds:${userId}`;
|
||||
}
|
||||
|
||||
/** Derive a stable user key from the next-auth session */
|
||||
async function currentUserId(): Promise<string | null> {
|
||||
const session = await getServerSession(authOptions);
|
||||
if (!session?.user) return null;
|
||||
const user = session.user as { id?: string; email?: string | null };
|
||||
return user.id ?? user.email ?? null;
|
||||
}
|
||||
|
||||
export async function saveCFCreds(creds: CFCreds): Promise<void> {
|
||||
const token = await new EncryptJWT({ ...creds })
|
||||
.setProtectedHeader({ alg: "dir", enc: "A256GCM" })
|
||||
.setExpirationTime("90d")
|
||||
.encrypt(encKey());
|
||||
// Env-var mode — nothing to save
|
||||
if (process.env.CLOUDFLARE_ACCOUNT_ID && process.env.CLOUDFLARE_API_TOKEN) return;
|
||||
|
||||
const jar = await cookies();
|
||||
jar.set("cf_creds", token, {
|
||||
httpOnly: true,
|
||||
secure: process.env.NODE_ENV === "production",
|
||||
sameSite: "lax",
|
||||
maxAge: 60 * 60 * 24 * 90,
|
||||
path: "/",
|
||||
});
|
||||
const uid = await currentUserId();
|
||||
if (!uid) throw new Error("Not authenticated");
|
||||
|
||||
await redis.set(credsKey(uid), creds);
|
||||
}
|
||||
|
||||
export async function loadCFCreds(): Promise<CFCreds | null> {
|
||||
// Env vars take priority (Vercel config / self-hosted)
|
||||
if (process.env.CLOUDFLARE_ACCOUNT_ID && process.env.CLOUDFLARE_API_TOKEN) {
|
||||
return {
|
||||
accountId: process.env.CLOUDFLARE_ACCOUNT_ID,
|
||||
apiToken: process.env.CLOUDFLARE_API_TOKEN,
|
||||
};
|
||||
}
|
||||
try {
|
||||
const jar = await cookies();
|
||||
const raw = jar.get("cf_creds")?.value;
|
||||
if (!raw) return null;
|
||||
const { payload } = await jwtDecrypt(raw, encKey());
|
||||
return {
|
||||
accountId: payload.accountId as string,
|
||||
apiToken: payload.apiToken as string,
|
||||
};
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
|
||||
const uid = await currentUserId();
|
||||
if (!uid) return null;
|
||||
|
||||
return redis.get<CFCreds>(credsKey(uid));
|
||||
}
|
||||
|
||||
export async function requireCFCreds(): Promise<CFCreds> {
|
||||
const creds = await loadCFCreds();
|
||||
if (!creds) {
|
||||
throw new Error("Cloudflare credentials not configured. Visit /settings to set them up.");
|
||||
throw new Error(
|
||||
"Cloudflare credentials not configured. Visit /settings to set them up."
|
||||
);
|
||||
}
|
||||
return creds;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
import { Redis } from "@upstash/redis";
|
||||
|
||||
export const redis = new Redis({
|
||||
url: process.env.UPSTASH_REDIS_URL!,
|
||||
token: process.env.UPSTASH_REDIS_TOKEN!,
|
||||
});
|
||||
Reference in New Issue
Block a user