Initial commit: WaMarket MVP

Marketplace inversée (Next.js 16 App Router + Supabase Cloud) : auth,
schéma + RLS, publication de besoins avec upload d'images, feed avec
filtres, offres, acceptation via RPC, messagerie temps réel,
dashboards, et Dockerfile pour déploiement Coolify.
This commit is contained in:
2026-07-30 14:33:17 +00:00
commit af81124d48
82 changed files with 16824 additions and 0 deletions
+19
View File
@@ -0,0 +1,19 @@
import "server-only";
import { createClient as createSupabaseClient } from "@supabase/supabase-js";
import type { Database } from "@/lib/database.types";
// Uses the service role key — bypasses RLS entirely. Only import this from
// trusted server-only code (e.g. the signup trigger fallback, admin tooling).
// Never expose this client or its key to the browser.
export function createAdminClient() {
return createSupabaseClient<Database>(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.SUPABASE_SERVICE_ROLE_KEY!,
{
auth: {
autoRefreshToken: false,
persistSession: false,
},
}
);
}
+9
View File
@@ -0,0 +1,9 @@
import { createBrowserClient } from "@supabase/ssr";
import type { Database } from "@/lib/database.types";
export function createClient() {
return createBrowserClient<Database>(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
);
}
+55
View File
@@ -0,0 +1,55 @@
import { createServerClient } from "@supabase/ssr";
import { NextResponse, type NextRequest } from "next/server";
const PROTECTED_PREFIXES = ["/dashboard", "/messages", "/needs/new"];
function isProtectedPath(pathname: string) {
if (PROTECTED_PREFIXES.some((prefix) => pathname.startsWith(prefix))) {
return true;
}
// /needs/[id]/edit
return /^\/needs\/[^/]+\/edit(\/|$)/.test(pathname);
}
export async function updateSession(request: NextRequest) {
let supabaseResponse = NextResponse.next({ request });
const supabase = createServerClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
{
cookies: {
getAll() {
return request.cookies.getAll();
},
setAll(cookiesToSet, headers) {
cookiesToSet.forEach(({ name, value }) =>
request.cookies.set(name, value)
);
supabaseResponse = NextResponse.next({ request });
cookiesToSet.forEach(({ name, value, options }) =>
supabaseResponse.cookies.set(name, value, options)
);
Object.entries(headers).forEach(([key, value]) =>
supabaseResponse.headers.set(key, value)
);
},
},
}
);
// Do not run code between createServerClient and getClaims() — this
// refreshes the session token on every request, which is what keeps
// users from being randomly logged out.
const { data } = await supabase.auth.getClaims();
const user = data?.claims;
if (!user && isProtectedPath(request.nextUrl.pathname)) {
const url = request.nextUrl.clone();
url.pathname = "/auth/login";
url.searchParams.set("redirect_to", request.nextUrl.pathname);
return NextResponse.redirect(url);
}
return supabaseResponse;
}
+29
View File
@@ -0,0 +1,29 @@
import { createServerClient } from "@supabase/ssr";
import { cookies } from "next/headers";
import type { Database } from "@/lib/database.types";
export async function createClient() {
const cookieStore = await cookies();
return createServerClient<Database>(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
{
cookies: {
getAll() {
return cookieStore.getAll();
},
setAll(cookiesToSet) {
try {
cookiesToSet.forEach(({ name, value, options }) =>
cookieStore.set(name, value, options)
);
} catch {
// Called from a Server Component — safe to ignore since the
// proxy already refreshes the session on every request.
}
},
},
}
);
}