38
frontend/Dockerfile
Normal file
38
frontend/Dockerfile
Normal file
@@ -0,0 +1,38 @@
|
||||
# syntax=docker/dockerfile:1
|
||||
|
||||
FROM node:20-alpine AS deps
|
||||
WORKDIR /app
|
||||
|
||||
COPY package.json package-lock.json ./
|
||||
RUN npm ci
|
||||
|
||||
FROM node:20-alpine AS builder
|
||||
WORKDIR /app
|
||||
|
||||
COPY --from=deps /app/node_modules ./node_modules
|
||||
COPY . ./
|
||||
|
||||
# Allows configuring the API URL at build time.
|
||||
ARG NEXT_PUBLIC_API_URL=http://localhost:8000
|
||||
ENV NEXT_PUBLIC_API_URL=${NEXT_PUBLIC_API_URL}
|
||||
|
||||
RUN npm run build
|
||||
|
||||
FROM node:20-alpine AS runner
|
||||
WORKDIR /app
|
||||
|
||||
ENV NODE_ENV=production
|
||||
|
||||
# If provided at runtime, Next will expose NEXT_PUBLIC_* to the browser as well
|
||||
# (but note some values may be baked at build time).
|
||||
ENV NEXT_PUBLIC_API_URL=http://localhost:8000
|
||||
|
||||
COPY --from=builder /app/.next ./.next
|
||||
COPY --from=builder /app/public ./public
|
||||
COPY --from=builder /app/package.json ./package.json
|
||||
COPY --from=builder /app/node_modules ./node_modules
|
||||
COPY --from=builder /app/next.config.ts ./next.config.ts
|
||||
|
||||
EXPOSE 3000
|
||||
|
||||
CMD ["npm", "run", "start"]
|
||||
@@ -1,9 +1,11 @@
|
||||
"use client";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
import { useMemo, useState } from "react";
|
||||
import { useParams, useRouter } from "next/navigation";
|
||||
|
||||
import { SignInButton, SignedIn, SignedOut, useAuth } from "@clerk/nextjs";
|
||||
import { SignInButton, SignedIn, SignedOut, useAuth } from "@/auth/clerk";
|
||||
|
||||
import { ApiError } from "@/api/mutator";
|
||||
import {
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
"use client";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
import { useMemo, useState } from "react";
|
||||
import Link from "next/link";
|
||||
import { useParams, useRouter } from "next/navigation";
|
||||
|
||||
import { SignInButton, SignedIn, SignedOut, useAuth } from "@clerk/nextjs";
|
||||
import { SignInButton, SignedIn, SignedOut, useAuth } from "@/auth/clerk";
|
||||
|
||||
import { ApiError } from "@/api/mutator";
|
||||
import {
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
"use client";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
import { useState } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
|
||||
import { SignInButton, SignedIn, SignedOut, useAuth } from "@clerk/nextjs";
|
||||
import { SignInButton, SignedIn, SignedOut, useAuth } from "@/auth/clerk";
|
||||
|
||||
import { ApiError } from "@/api/mutator";
|
||||
import {
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
"use client";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
import { useMemo, useState } from "react";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/navigation";
|
||||
|
||||
import { SignInButton, SignedIn, SignedOut, useAuth } from "@clerk/nextjs";
|
||||
import { SignInButton, SignedIn, SignedOut, useAuth } from "@/auth/clerk";
|
||||
import {
|
||||
type ColumnDef,
|
||||
type SortingState,
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
"use client";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
import { useParams } from "next/navigation";
|
||||
|
||||
import { SignInButton, SignedIn, SignedOut } from "@clerk/nextjs";
|
||||
import { SignInButton, SignedIn, SignedOut } from "@/auth/clerk";
|
||||
|
||||
import { BoardApprovalsPanel } from "@/components/BoardApprovalsPanel";
|
||||
import { DashboardSidebar } from "@/components/organisms/DashboardSidebar";
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
"use client";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
import { useEffect, useMemo, useRef, useState } from "react";
|
||||
import { useParams, useRouter, useSearchParams } from "next/navigation";
|
||||
|
||||
import { SignInButton, SignedIn, SignedOut, useAuth } from "@clerk/nextjs";
|
||||
import { SignInButton, SignedIn, SignedOut, useAuth } from "@/auth/clerk";
|
||||
import { X } from "lucide-react";
|
||||
|
||||
import { ApiError } from "@/api/mutator";
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
"use client";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
import { memo, useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { useParams, useRouter, useSearchParams } from "next/navigation";
|
||||
|
||||
import { SignInButton, SignedIn, SignedOut, useAuth } from "@clerk/nextjs";
|
||||
import { SignInButton, SignedIn, SignedOut, useAuth } from "@/auth/clerk";
|
||||
import {
|
||||
Activity,
|
||||
ArrowUpRight,
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
"use client";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
import { useMemo, useState } from "react";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/navigation";
|
||||
|
||||
import { SignInButton, SignedIn, SignedOut, useAuth } from "@clerk/nextjs";
|
||||
import { SignInButton, SignedIn, SignedOut, useAuth } from "@/auth/clerk";
|
||||
|
||||
import { ApiError } from "@/api/mutator";
|
||||
import { useCreateBoardApiV1BoardsPost } from "@/api/generated/boards/boards";
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
"use client";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
import { useMemo, useState } from "react";
|
||||
import Link from "next/link";
|
||||
|
||||
import { SignInButton, SignedIn, SignedOut, useAuth } from "@clerk/nextjs";
|
||||
import { SignInButton, SignedIn, SignedOut, useAuth } from "@/auth/clerk";
|
||||
import {
|
||||
type ColumnDef,
|
||||
flexRender,
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
"use client";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
import { useMemo } from "react";
|
||||
|
||||
import { SignInButton, SignedIn, SignedOut, useAuth } from "@clerk/nextjs";
|
||||
import { SignInButton, SignedIn, SignedOut, useAuth } from "@/auth/clerk";
|
||||
import {
|
||||
Area,
|
||||
AreaChart,
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
"use client";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
import { useState } from "react";
|
||||
import { useParams, useRouter } from "next/navigation";
|
||||
|
||||
import { SignInButton, SignedIn, SignedOut, useAuth } from "@clerk/nextjs";
|
||||
import { SignInButton, SignedIn, SignedOut, useAuth } from "@/auth/clerk";
|
||||
import { CheckCircle2, RefreshCcw, XCircle } from "lucide-react";
|
||||
|
||||
import { ApiError } from "@/api/mutator";
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
"use client";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
import { useMemo } from "react";
|
||||
import { useParams, useRouter } from "next/navigation";
|
||||
|
||||
import { SignInButton, SignedIn, SignedOut, useAuth } from "@clerk/nextjs";
|
||||
import { SignInButton, SignedIn, SignedOut, useAuth } from "@/auth/clerk";
|
||||
|
||||
import { ApiError } from "@/api/mutator";
|
||||
import {
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
"use client";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
import { useState } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
|
||||
import { SignInButton, SignedIn, SignedOut, useAuth } from "@clerk/nextjs";
|
||||
import { SignInButton, SignedIn, SignedOut, useAuth } from "@/auth/clerk";
|
||||
import { CheckCircle2, RefreshCcw, XCircle } from "lucide-react";
|
||||
|
||||
import { ApiError } from "@/api/mutator";
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
"use client";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
import { useMemo, useState } from "react";
|
||||
import Link from "next/link";
|
||||
|
||||
import { SignInButton, SignedIn, SignedOut, useAuth } from "@clerk/nextjs";
|
||||
import { SignInButton, SignedIn, SignedOut, useAuth } from "@/auth/clerk";
|
||||
import {
|
||||
type ColumnDef,
|
||||
type SortingState,
|
||||
|
||||
@@ -3,9 +3,9 @@ import "./globals.css";
|
||||
import type { Metadata } from "next";
|
||||
import type { ReactNode } from "react";
|
||||
|
||||
import { ClerkProvider } from "@clerk/nextjs";
|
||||
import { IBM_Plex_Sans, Sora } from "next/font/google";
|
||||
|
||||
import { AuthProvider } from "@/components/providers/AuthProvider";
|
||||
import { QueryProvider } from "@/components/providers/QueryProvider";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
@@ -29,14 +29,14 @@ const headingFont = Sora({
|
||||
|
||||
export default function RootLayout({ children }: { children: ReactNode }) {
|
||||
return (
|
||||
<ClerkProvider>
|
||||
<html lang="en">
|
||||
<body
|
||||
className={`${bodyFont.variable} ${headingFont.variable} min-h-screen bg-app text-strong antialiased`}
|
||||
>
|
||||
<html lang="en">
|
||||
<body
|
||||
className={`${bodyFont.variable} ${headingFont.variable} min-h-screen bg-app text-strong antialiased`}
|
||||
>
|
||||
<AuthProvider>
|
||||
<QueryProvider>{children}</QueryProvider>
|
||||
</body>
|
||||
</html>
|
||||
</ClerkProvider>
|
||||
</AuthProvider>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
"use client";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
|
||||
import { SignInButton, SignedIn, SignedOut, useAuth, useUser } from "@clerk/nextjs";
|
||||
import { SignInButton, SignedIn, SignedOut, useAuth, useUser } from "@/auth/clerk";
|
||||
import { Globe, Info, RotateCcw, Save, User } from "lucide-react";
|
||||
|
||||
import { ApiError } from "@/api/mutator";
|
||||
|
||||
70
frontend/src/auth/clerk.tsx
Normal file
70
frontend/src/auth/clerk.tsx
Normal file
@@ -0,0 +1,70 @@
|
||||
"use client";
|
||||
|
||||
import type { ReactNode } from "react";
|
||||
|
||||
// NOTE: We intentionally keep this file very small and dependency-free.
|
||||
// It provides CI/secretless-build safe fallbacks for Clerk hooks/components.
|
||||
|
||||
import {
|
||||
ClerkProvider,
|
||||
SignedIn as ClerkSignedIn,
|
||||
SignedOut as ClerkSignedOut,
|
||||
SignInButton as ClerkSignInButton,
|
||||
SignOutButton as ClerkSignOutButton,
|
||||
useAuth as clerkUseAuth,
|
||||
useUser as clerkUseUser,
|
||||
} from "@clerk/nextjs";
|
||||
|
||||
import type { ComponentProps } from "react";
|
||||
|
||||
export function isClerkEnabled(): boolean {
|
||||
// Invariant: Clerk is disabled ONLY when the publishable key is absent.
|
||||
// If a key is present, we assume Clerk is intended to be enabled and we let
|
||||
// Clerk fail fast if the key is invalid/misconfigured.
|
||||
return Boolean(process.env.NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY);
|
||||
}
|
||||
|
||||
export function SignedIn(props: { children: ReactNode }) {
|
||||
if (!isClerkEnabled()) return null;
|
||||
return <ClerkSignedIn>{props.children}</ClerkSignedIn>;
|
||||
}
|
||||
|
||||
export function SignedOut(props: { children: ReactNode }) {
|
||||
if (!isClerkEnabled()) return <>{props.children}</>;
|
||||
return <ClerkSignedOut>{props.children}</ClerkSignedOut>;
|
||||
}
|
||||
|
||||
// Keep the same prop surface as Clerk components so call sites don't need edits.
|
||||
export function SignInButton(props: ComponentProps<typeof ClerkSignInButton>) {
|
||||
if (!isClerkEnabled()) return null;
|
||||
return <ClerkSignInButton {...props} />;
|
||||
}
|
||||
|
||||
export function SignOutButton(props: ComponentProps<typeof ClerkSignOutButton>) {
|
||||
if (!isClerkEnabled()) return null;
|
||||
return <ClerkSignOutButton {...props} />;
|
||||
}
|
||||
|
||||
export function useUser() {
|
||||
if (!isClerkEnabled()) {
|
||||
return { isLoaded: true, isSignedIn: false, user: null } as const;
|
||||
}
|
||||
return clerkUseUser();
|
||||
}
|
||||
|
||||
export function useAuth() {
|
||||
if (!isClerkEnabled()) {
|
||||
return {
|
||||
isLoaded: true,
|
||||
isSignedIn: false,
|
||||
userId: null,
|
||||
sessionId: null,
|
||||
getToken: async () => null,
|
||||
} as const;
|
||||
}
|
||||
return clerkUseAuth();
|
||||
}
|
||||
|
||||
// Re-export ClerkProvider for places that want to mount it, but strongly prefer
|
||||
// gating via isClerkEnabled() at call sites.
|
||||
export { ClerkProvider };
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import { useCallback, useMemo, useState } from "react";
|
||||
|
||||
import { useAuth } from "@clerk/nextjs";
|
||||
import { useAuth } from "@/auth/clerk";
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
|
||||
import { Clock } from "lucide-react";
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { SignInButton, SignedIn, SignedOut } from "@clerk/nextjs";
|
||||
import { SignInButton, SignedIn, SignedOut } from "@/auth/clerk";
|
||||
|
||||
import { HeroCopy } from "@/components/molecules/HeroCopy";
|
||||
import { Button } from "@/components/ui/button";
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import Image from "next/image";
|
||||
import { SignOutButton, useUser } from "@clerk/nextjs";
|
||||
import { SignOutButton, useUser } from "@/auth/clerk";
|
||||
import { LogOut } from "lucide-react";
|
||||
|
||||
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
|
||||
|
||||
30
frontend/src/components/providers/AuthProvider.tsx
Normal file
30
frontend/src/components/providers/AuthProvider.tsx
Normal file
@@ -0,0 +1,30 @@
|
||||
"use client";
|
||||
|
||||
import { ClerkProvider } from "@clerk/nextjs";
|
||||
import type { ReactNode } from "react";
|
||||
|
||||
function isLikelyValidClerkPublishableKey(key: string | undefined): key is string {
|
||||
if (!key) return false;
|
||||
// Clerk publishable keys look like: pk_test_... or pk_live_...
|
||||
// In CI we want builds to stay secretless; if the key isn't present/valid,
|
||||
// we skip Clerk entirely so `next build` can prerender.
|
||||
//
|
||||
// Note: Clerk appears to validate key *contents*, not just shape. We therefore
|
||||
// use a conservative heuristic to avoid treating obvious placeholders as valid.
|
||||
const m = /^pk_(test|live)_([A-Za-z0-9]+)$/.exec(key);
|
||||
if (!m) return false;
|
||||
const body = m[2];
|
||||
if (body.length < 16) return false;
|
||||
if (/^0+$/.test(body)) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
export function AuthProvider({ children }: { children: ReactNode }) {
|
||||
const publishableKey = process.env.NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY;
|
||||
|
||||
if (!isLikelyValidClerkPublishableKey(publishableKey)) {
|
||||
return <>{children}</>;
|
||||
}
|
||||
|
||||
return <ClerkProvider publishableKey={publishableKey}>{children}</ClerkProvider>;
|
||||
}
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import type { ReactNode } from "react";
|
||||
|
||||
import { SignedIn, useUser } from "@clerk/nextjs";
|
||||
import { SignedIn, useUser } from "@/auth/clerk";
|
||||
|
||||
import { BrandMark } from "@/components/atoms/BrandMark";
|
||||
import { UserMenu } from "@/components/organisms/UserMenu";
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import type { ReactNode } from "react";
|
||||
|
||||
import { SignedIn } from "@clerk/nextjs";
|
||||
import { SignedIn } from "@/auth/clerk";
|
||||
|
||||
import { BrandMark } from "@/components/atoms/BrandMark";
|
||||
import { UserMenu } from "@/components/organisms/UserMenu";
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { clerkMiddleware } from "@clerk/nextjs/server";
|
||||
|
||||
export default clerkMiddleware();
|
||||
const isClerkEnabled = () => Boolean(process.env.NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY);
|
||||
|
||||
export default isClerkEnabled() ? clerkMiddleware() : () => NextResponse.next();
|
||||
|
||||
export const config = {
|
||||
matcher: [
|
||||
|
||||
Reference in New Issue
Block a user