feat: Upstash Redis for per-user CF credential storage

This commit is contained in:
chunzhimoe
2026-04-12 17:16:22 +08:00
parent 18b4bb853d
commit 948fe3935e
5 changed files with 58 additions and 32 deletions
+29 -31
View File
@@ -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;
}
+6
View File
@@ -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!,
});