d7e0d7eada
New users get a Gravatar-backed avatar automatically (computed from email at signup); users can upload a custom photo instead via Settings, or revert to the Gravatar/initials fallback. avatarUrl stays the single resolved value (custom photo, OAuth photo, or precomputed Gravatar URL) so every existing avatar-rendering spot across the app needs zero changes. - users.hasCustomAvatar tracks whether avatarUrl is a real upload vs a computed Gravatar fallback, so "remove photo" knows what to revert to. - New /api/v1/upload/avatar-presign route (session-scoped, generic — the existing recipe-photo presign route required a recipeId). - CSP img-src needed www.gravatar.com added, or every browser blocks the fallback avatar outright. - Settings page now reads avatarUrl from a fresh DB query instead of Better Auth's session object — the session cookie cache (5 min TTL) was serving a stale image right after upload, showing the initials fallback until the cache happened to expire. Verified locally: signup auto-sets a Gravatar URL, upload persists across reload, remove correctly reverts to Gravatar/initials.
71 lines
1.6 KiB
TypeScript
71 lines
1.6 KiB
TypeScript
import type { NextConfig } from "next";
|
|
|
|
const storagePublicUrl = process.env["STORAGE_PUBLIC_URL"] || "http://localhost:9000";
|
|
const storageUrl = new URL(storagePublicUrl);
|
|
const storageOrigin = storageUrl.origin;
|
|
|
|
const securityHeaders = [
|
|
{
|
|
key: "X-Content-Type-Options",
|
|
value: "nosniff",
|
|
},
|
|
{
|
|
key: "X-Frame-Options",
|
|
value: "DENY",
|
|
},
|
|
{
|
|
key: "Referrer-Policy",
|
|
value: "strict-origin-when-cross-origin",
|
|
},
|
|
{
|
|
key: "Strict-Transport-Security",
|
|
value: "max-age=63072000; includeSubDomains; preload",
|
|
},
|
|
{
|
|
key: "Permissions-Policy",
|
|
value: "camera=(), microphone=(), geolocation=(), interest-cohort=()",
|
|
},
|
|
{
|
|
key: "X-DNS-Prefetch-Control",
|
|
value: "on",
|
|
},
|
|
{
|
|
key: "Content-Security-Policy",
|
|
value: [
|
|
"default-src 'self'",
|
|
"script-src 'self' 'unsafe-inline' 'unsafe-eval'", // unsafe-eval needed by Next.js dev; tighten in prod
|
|
"style-src 'self' 'unsafe-inline'",
|
|
`img-src 'self' data: blob: ${storageOrigin} https://www.gravatar.com`,
|
|
"font-src 'self'",
|
|
`connect-src 'self' ${storageOrigin}`,
|
|
"frame-ancestors 'none'",
|
|
"base-uri 'self'",
|
|
"form-action 'self'",
|
|
].join("; "),
|
|
},
|
|
];
|
|
|
|
const nextConfig: NextConfig = {
|
|
output: "standalone",
|
|
images: {
|
|
remotePatterns: [
|
|
{
|
|
protocol: storageUrl.protocol.replace(":", "") as "http" | "https",
|
|
hostname: storageUrl.hostname,
|
|
port: storageUrl.port,
|
|
pathname: "/**",
|
|
},
|
|
],
|
|
},
|
|
async headers() {
|
|
return [
|
|
{
|
|
source: "/(.*)",
|
|
headers: securityHeaders,
|
|
},
|
|
];
|
|
},
|
|
};
|
|
|
|
export default nextConfig;
|