feat(deploy): dockerize web app for portainer git-stack deploy behind external traefik
Add root Dockerfile (standalone Next output, multi-stage pnpm build), drop in-stack caddy in favor of publishing web's port for an external traefik LXC (file-provider dynamic config included), and document the portainer deploy flow. Also fixes issues that blocked any production build: a bad auth-client type cast, the ai SDK's mimeType->mediaType rename, an implicit-any callback param, and push.ts eagerly calling webpush.setVapidDetails at module import time (which crashed page-data collection whenever VAPID env vars weren't present at build) — now lazily configured on first send. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -135,7 +135,7 @@ function SocialButton({ provider, label, icon }: { provider: "github" | "discord
|
||||
function AuthentikButton({ label }: { label: string }) {
|
||||
if (!process.env["NEXT_PUBLIC_AUTHENTIK_ENABLED"]) return null;
|
||||
return (
|
||||
<Button variant="outline" className="w-full gap-2" type="button" onClick={() => (authClient as { signIn: { genericOAuth: (opts: { providerId: string; callbackURL: string }) => Promise<void> } }).signIn.genericOAuth({ providerId: "authentik", callbackURL: "/recipes" })}>
|
||||
<Button variant="outline" className="w-full gap-2" type="button" onClick={() => (authClient as unknown as { signIn: { genericOAuth: (opts: { providerId: string; callbackURL: string }) => Promise<void> } }).signIn.genericOAuth({ providerId: "authentik", callbackURL: "/recipes" })}>
|
||||
<AuthentikIcon />
|
||||
{label}
|
||||
</Button>
|
||||
|
||||
@@ -49,7 +49,7 @@ export async function importFromPhoto(
|
||||
{
|
||||
role: "user",
|
||||
content: [
|
||||
{ type: "image", image: imageBase64, mimeType },
|
||||
{ type: "image", image: imageBase64, mediaType: mimeType },
|
||||
{ type: "text", text: "Extract the complete recipe from this image." },
|
||||
],
|
||||
},
|
||||
|
||||
@@ -128,7 +128,7 @@ export const auth = betterAuth({
|
||||
},
|
||||
changeEmail: {
|
||||
enabled: true,
|
||||
sendChangeEmailVerification: async ({ newEmail, url }) => {
|
||||
sendChangeEmailVerification: async ({ newEmail, url }: { newEmail: string; url: string }) => {
|
||||
await sendEmail({
|
||||
to: newEmail,
|
||||
subject: "Verify your new Epicure email",
|
||||
|
||||
+12
-5
@@ -1,16 +1,23 @@
|
||||
import webpush from "web-push";
|
||||
import { db, pushSubscriptions, eq } from "@epicure/db";
|
||||
|
||||
webpush.setVapidDetails(
|
||||
"mailto:contact@epicure.app",
|
||||
process.env.NEXT_PUBLIC_VAPID_PUBLIC_KEY!,
|
||||
process.env.VAPID_PRIVATE_KEY!
|
||||
);
|
||||
let vapidConfigured = false;
|
||||
|
||||
function ensureVapidConfigured(): void {
|
||||
if (vapidConfigured) return;
|
||||
webpush.setVapidDetails(
|
||||
"mailto:contact@epicure.app",
|
||||
process.env.NEXT_PUBLIC_VAPID_PUBLIC_KEY!,
|
||||
process.env.VAPID_PRIVATE_KEY!
|
||||
);
|
||||
vapidConfigured = true;
|
||||
}
|
||||
|
||||
export async function sendPushNotification(
|
||||
userId: string,
|
||||
notification: { title: string; body: string; url?: string }
|
||||
): Promise<void> {
|
||||
ensureVapidConfigured();
|
||||
const subs = await db
|
||||
.select()
|
||||
.from(pushSubscriptions)
|
||||
|
||||
@@ -42,6 +42,7 @@ const securityHeaders = [
|
||||
];
|
||||
|
||||
const nextConfig: NextConfig = {
|
||||
output: "standalone",
|
||||
async headers() {
|
||||
return [
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user