mirror of
https://github.com/velocitatem/cvfs.git
synced 2026-05-31 16:53:38 +00:00
Initial commit
This commit is contained in:
28
apps/webapp/src/app/auth/confirm/route.ts
Normal file
28
apps/webapp/src/app/auth/confirm/route.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import { type EmailOtpType } from '@supabase/supabase-js'
|
||||
import { type NextRequest } from 'next/server'
|
||||
|
||||
import { createClient } from '@/utils/supabase/server'
|
||||
import { redirect } from 'next/navigation'
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
const { searchParams } = new URL(request.url)
|
||||
const token_hash = searchParams.get('token_hash')
|
||||
const type = searchParams.get('type') as EmailOtpType | null
|
||||
const next = searchParams.get('next') ?? '/'
|
||||
|
||||
if (token_hash && type) {
|
||||
const supabase = await createClient()
|
||||
|
||||
const { error } = await supabase.auth.verifyOtp({
|
||||
type,
|
||||
token_hash,
|
||||
})
|
||||
if (!error) {
|
||||
// redirect user to specified redirect URL or root of app
|
||||
redirect(next)
|
||||
}
|
||||
}
|
||||
|
||||
// redirect the user to an error page with some instructions
|
||||
redirect('/error')
|
||||
}
|
||||
10
apps/webapp/src/app/blog/page.tsx
Normal file
10
apps/webapp/src/app/blog/page.tsx
Normal file
@@ -0,0 +1,10 @@
|
||||
export default function BlogPage() {
|
||||
return (
|
||||
<main className="mx-auto max-w-3xl px-6 py-16">
|
||||
<h1 className="text-3xl font-semibold tracking-tight">Blog</h1>
|
||||
<p className="mt-4 text-sm text-neutral-600">
|
||||
Publish product updates, engineering notes, and launch posts here.
|
||||
</p>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
12
apps/webapp/src/app/dashboard/actions.ts
Normal file
12
apps/webapp/src/app/dashboard/actions.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
'use server'
|
||||
|
||||
import { revalidatePath } from 'next/cache'
|
||||
import { redirect } from 'next/navigation'
|
||||
import { createClient } from '@/utils/supabase/server'
|
||||
|
||||
export async function logout() {
|
||||
const supabase = await createClient()
|
||||
await supabase.auth.signOut()
|
||||
revalidatePath('/', 'layout')
|
||||
redirect('/login')
|
||||
}
|
||||
14
apps/webapp/src/app/dashboard/layout.tsx
Normal file
14
apps/webapp/src/app/dashboard/layout.tsx
Normal file
@@ -0,0 +1,14 @@
|
||||
export default function DashboardLayout({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
}) {
|
||||
return (
|
||||
<div>
|
||||
<nav>
|
||||
<h1>Dashboard</h1>
|
||||
</nav>
|
||||
<main>{children}</main>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
21
apps/webapp/src/app/dashboard/page.tsx
Normal file
21
apps/webapp/src/app/dashboard/page.tsx
Normal file
@@ -0,0 +1,21 @@
|
||||
import { redirect } from 'next/navigation'
|
||||
import { createClient } from '@/utils/supabase/server'
|
||||
import { logout } from './actions'
|
||||
|
||||
export default async function DashboardPage() {
|
||||
const supabase = await createClient()
|
||||
|
||||
const { data, error } = await supabase.auth.getUser()
|
||||
if (error || !data?.user) {
|
||||
redirect('/login')
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<p>Welcome, {data.user.email}</p>
|
||||
<form>
|
||||
<button formAction={logout}>Logout</button>
|
||||
</form>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
20
apps/webapp/src/app/error.tsx
Normal file
20
apps/webapp/src/app/error.tsx
Normal file
@@ -0,0 +1,20 @@
|
||||
'use client';
|
||||
|
||||
export default function Error({
|
||||
error,
|
||||
reset,
|
||||
}: {
|
||||
error: Error & { digest?: string };
|
||||
reset: () => void;
|
||||
}) {
|
||||
return (
|
||||
<div>
|
||||
{/* TODO: Style this error page when implementing in your project */}
|
||||
<h2>Something went wrong!</h2>
|
||||
<p>{error.message || 'An unexpected error occurred'}</p>
|
||||
<button onClick={() => reset()}>
|
||||
Try again
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
5
apps/webapp/src/app/error/page.tsx
Normal file
5
apps/webapp/src/app/error/page.tsx
Normal file
@@ -0,0 +1,5 @@
|
||||
'use client'
|
||||
|
||||
export default function ErrorPage() {
|
||||
return <p>Sorry, something went wrong</p>
|
||||
}
|
||||
BIN
apps/webapp/src/app/favicon.ico
Normal file
BIN
apps/webapp/src/app/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 25 KiB |
26
apps/webapp/src/app/globals.css
Normal file
26
apps/webapp/src/app/globals.css
Normal file
@@ -0,0 +1,26 @@
|
||||
@import "tailwindcss";
|
||||
|
||||
:root {
|
||||
--background: #ffffff;
|
||||
--foreground: #171717;
|
||||
}
|
||||
|
||||
@theme inline {
|
||||
--color-background: var(--background);
|
||||
--color-foreground: var(--foreground);
|
||||
--font-sans: var(--font-geist-sans);
|
||||
--font-mono: var(--font-geist-mono);
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
--background: #0a0a0a;
|
||||
--foreground: #ededed;
|
||||
}
|
||||
}
|
||||
|
||||
body {
|
||||
background: var(--background);
|
||||
color: var(--foreground);
|
||||
font-family: Arial, Helvetica, sans-serif;
|
||||
}
|
||||
8
apps/webapp/src/app/instruments/page.tsx
Normal file
8
apps/webapp/src/app/instruments/page.tsx
Normal file
@@ -0,0 +1,8 @@
|
||||
import { createClient } from '@/utils/supabase/server';
|
||||
|
||||
export default async function Instruments() {
|
||||
const supabase = await createClient();
|
||||
const { data: instruments } = await supabase.from("instruments").select();
|
||||
|
||||
return <pre>{JSON.stringify(instruments, null, 2)}</pre>
|
||||
}
|
||||
29
apps/webapp/src/app/layout.tsx
Normal file
29
apps/webapp/src/app/layout.tsx
Normal file
@@ -0,0 +1,29 @@
|
||||
import type { Metadata } from "next";
|
||||
import "./globals.css";
|
||||
import Header from "@/components/Header";
|
||||
import Footer from "@/components/Footer";
|
||||
|
||||
const fontVariables = "font-sans";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Ultiplate - Ultimate Boilerplate",
|
||||
description: "AI-native template for any project with deployment ready setup",
|
||||
};
|
||||
|
||||
export default function RootLayout({
|
||||
children,
|
||||
}: Readonly<{
|
||||
children: React.ReactNode;
|
||||
}>) {
|
||||
return (
|
||||
<html lang="en">
|
||||
<body className={`${fontVariables} antialiased`}>
|
||||
<Header />
|
||||
<main className="min-h-screen">
|
||||
{children}
|
||||
</main>
|
||||
<Footer />
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
46
apps/webapp/src/app/login/actions.ts
Normal file
46
apps/webapp/src/app/login/actions.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
'use server'
|
||||
|
||||
import { revalidatePath } from 'next/cache'
|
||||
import { redirect } from 'next/navigation'
|
||||
|
||||
import { createClient } from '@/utils/supabase/server'
|
||||
|
||||
export async function login(formData: FormData) {
|
||||
const supabase = await createClient()
|
||||
|
||||
// type-casting here for convenience
|
||||
// in practice, you should validate your inputs
|
||||
const data = {
|
||||
email: formData.get('email') as string,
|
||||
password: formData.get('password') as string,
|
||||
}
|
||||
|
||||
const { error } = await supabase.auth.signInWithPassword(data)
|
||||
|
||||
if (error) {
|
||||
redirect('/error')
|
||||
}
|
||||
|
||||
revalidatePath('/', 'layout')
|
||||
redirect('/')
|
||||
}
|
||||
|
||||
export async function signup(formData: FormData) {
|
||||
const supabase = await createClient()
|
||||
|
||||
// type-casting here for convenience
|
||||
// in practice, you should validate your inputs
|
||||
const data = {
|
||||
email: formData.get('email') as string,
|
||||
password: formData.get('password') as string,
|
||||
}
|
||||
|
||||
const { error } = await supabase.auth.signUp(data)
|
||||
|
||||
if (error) {
|
||||
redirect('/error')
|
||||
}
|
||||
|
||||
revalidatePath('/', 'layout')
|
||||
redirect('/')
|
||||
}
|
||||
14
apps/webapp/src/app/login/page.tsx
Normal file
14
apps/webapp/src/app/login/page.tsx
Normal file
@@ -0,0 +1,14 @@
|
||||
import { login, signup } from './actions'
|
||||
|
||||
export default function LoginPage() {
|
||||
return (
|
||||
<form>
|
||||
<label htmlFor="email">Email:</label>
|
||||
<input id="email" name="email" type="email" required />
|
||||
<label htmlFor="password">Password:</label>
|
||||
<input id="password" name="password" type="password" required />
|
||||
<button formAction={login}>Log in</button>
|
||||
<button formAction={signup}>Sign up</button>
|
||||
</form>
|
||||
)
|
||||
}
|
||||
12
apps/webapp/src/app/not-found.tsx
Normal file
12
apps/webapp/src/app/not-found.tsx
Normal file
@@ -0,0 +1,12 @@
|
||||
import Link from 'next/link';
|
||||
|
||||
export default function NotFound() {
|
||||
return (
|
||||
<div>
|
||||
{/* TODO: Style this 404 page when implementing in your project */}
|
||||
<h2>Not Found</h2>
|
||||
<p>Could not find requested resource</p>
|
||||
<Link href="/">Return Home</Link>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
17
apps/webapp/src/app/page.tsx
Normal file
17
apps/webapp/src/app/page.tsx
Normal file
@@ -0,0 +1,17 @@
|
||||
import Hero from "@/components/Hero";
|
||||
import FeaturesGrid from "@/components/FeaturesGrid";
|
||||
import Testimonials1 from "@/components/Testimonials1";
|
||||
import Pricing from "@/components/Pricing";
|
||||
//import FAQ from "@/components/FAQ";
|
||||
//import CTA from "@/components/CTA";
|
||||
|
||||
export default function Home() {
|
||||
return (
|
||||
<div>
|
||||
<Hero />
|
||||
<FeaturesGrid />
|
||||
<Testimonials1 />
|
||||
<Pricing />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
10
apps/webapp/src/app/privacy-policy/page.tsx
Normal file
10
apps/webapp/src/app/privacy-policy/page.tsx
Normal file
@@ -0,0 +1,10 @@
|
||||
export default function PrivacyPolicyPage() {
|
||||
return (
|
||||
<main className="mx-auto max-w-3xl px-6 py-16">
|
||||
<h1 className="text-3xl font-semibold tracking-tight">Privacy Policy</h1>
|
||||
<p className="mt-4 text-sm text-neutral-600">
|
||||
Describe what data you collect, how it is used, and how users can request deletion.
|
||||
</p>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
10
apps/webapp/src/app/tos/page.tsx
Normal file
10
apps/webapp/src/app/tos/page.tsx
Normal file
@@ -0,0 +1,10 @@
|
||||
export default function TermsOfServicePage() {
|
||||
return (
|
||||
<main className="mx-auto max-w-3xl px-6 py-16">
|
||||
<h1 className="text-3xl font-semibold tracking-tight">Terms of Service</h1>
|
||||
<p className="mt-4 text-sm text-neutral-600">
|
||||
Add your product terms, responsibilities, and legal limitations in this page.
|
||||
</p>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
22
apps/webapp/src/components/FeaturesGrid.tsx
Normal file
22
apps/webapp/src/components/FeaturesGrid.tsx
Normal file
@@ -0,0 +1,22 @@
|
||||
import { getLocale } from "@/libs/locales";
|
||||
|
||||
export default function FeaturesGrid() {
|
||||
const { common } = getLocale('en');
|
||||
|
||||
return (
|
||||
<section>
|
||||
{/* TODO: Style this features grid when implementing in your project */}
|
||||
<div>
|
||||
<h2>{common.features.title}</h2>
|
||||
<div>
|
||||
{common.features.items.map((feature, index) => (
|
||||
<div key={index}>
|
||||
<h3>{feature.title}</h3>
|
||||
<p>{feature.description}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
37
apps/webapp/src/components/Footer.tsx
Normal file
37
apps/webapp/src/components/Footer.tsx
Normal file
@@ -0,0 +1,37 @@
|
||||
import Link from "next/link";
|
||||
import { getLocale } from "@/libs/locales";
|
||||
|
||||
export default function Footer() {
|
||||
const { common } = getLocale('en');
|
||||
|
||||
return (
|
||||
<footer>
|
||||
{/* TODO: Style this footer when implementing in your project */}
|
||||
<div>
|
||||
<div>
|
||||
<div>
|
||||
<h3>{common.footer.brand}</h3>
|
||||
<p>{common.footer.description}</p>
|
||||
</div>
|
||||
<div>
|
||||
<h4>{common.footer.legal.title}</h4>
|
||||
<ul>
|
||||
<li><Link href="/privacy-policy">{common.footer.legal.privacyPolicy}</Link></li>
|
||||
<li><Link href="/tos">{common.footer.legal.termsOfService}</Link></li>
|
||||
</ul>
|
||||
</div>
|
||||
<div>
|
||||
<h4>{common.footer.company.title}</h4>
|
||||
<ul>
|
||||
<li><Link href="/blog">{common.footer.company.blog}</Link></li>
|
||||
<li><Link href="/dashboard">{common.footer.company.dashboard}</Link></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<p>{common.footer.copyright}</p>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
);
|
||||
}
|
||||
27
apps/webapp/src/components/Header.tsx
Normal file
27
apps/webapp/src/components/Header.tsx
Normal file
@@ -0,0 +1,27 @@
|
||||
import Link from "next/link";
|
||||
import { getLocale } from "@/libs/locales";
|
||||
|
||||
export default function Header() {
|
||||
const { common } = getLocale('en');
|
||||
|
||||
return (
|
||||
<header>
|
||||
{/* TODO: Style this header when implementing in your project */}
|
||||
<div>
|
||||
<div>
|
||||
<div>
|
||||
<Link href="/">{common.header.brand}</Link>
|
||||
</div>
|
||||
<nav>
|
||||
<Link href="/">{common.header.nav.home}</Link>
|
||||
<Link href="/dashboard">{common.header.nav.dashboard}</Link>
|
||||
<Link href="/blog">{common.header.nav.blog}</Link>
|
||||
</nav>
|
||||
<div>
|
||||
<Link href="/login">{common.header.actions.signIn}</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
);
|
||||
}
|
||||
24
apps/webapp/src/components/Hero.tsx
Normal file
24
apps/webapp/src/components/Hero.tsx
Normal file
@@ -0,0 +1,24 @@
|
||||
import Link from "next/link";
|
||||
import { getLocale } from "@/libs/locales";
|
||||
|
||||
export default function Hero() {
|
||||
const { common } = getLocale('en');
|
||||
|
||||
return (
|
||||
<section>
|
||||
{/* TODO: Style this hero section when implementing in your project */}
|
||||
<div>
|
||||
<h1>{common.hero.title}</h1>
|
||||
<p>{common.hero.description}</p>
|
||||
<div>
|
||||
<Link href="/dashboard">
|
||||
<button>{common.hero.actions.getStarted}</button>
|
||||
</Link>
|
||||
<Link href="/blog">
|
||||
<button>{common.hero.actions.learnMore}</button>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
47
apps/webapp/src/components/Pricing.tsx
Normal file
47
apps/webapp/src/components/Pricing.tsx
Normal file
@@ -0,0 +1,47 @@
|
||||
export default function Pricing() {
|
||||
const plans = [
|
||||
{
|
||||
name: "Open Source",
|
||||
price: "Free",
|
||||
features: [
|
||||
"All boilerplate code",
|
||||
"Docker configurations",
|
||||
"Basic ML setup",
|
||||
"Community support"
|
||||
]
|
||||
},
|
||||
{
|
||||
name: "Pro",
|
||||
price: "$49/month",
|
||||
features: [
|
||||
"Everything in Open Source",
|
||||
"Advanced configurations",
|
||||
"Priority support",
|
||||
"Custom integrations"
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
return (
|
||||
<section>
|
||||
{/* TODO: Style this pricing section when implementing in your project */}
|
||||
<div>
|
||||
<h2>Pricing</h2>
|
||||
<div>
|
||||
{plans.map((plan, index) => (
|
||||
<div key={index}>
|
||||
<h3>{plan.name}</h3>
|
||||
<p>{plan.price}</p>
|
||||
<ul>
|
||||
{plan.features.map((feature, featureIndex) => (
|
||||
<li key={featureIndex}>{feature}</li>
|
||||
))}
|
||||
</ul>
|
||||
<button>Choose Plan</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
25
apps/webapp/src/components/Testimonials1.tsx
Normal file
25
apps/webapp/src/components/Testimonials1.tsx
Normal file
@@ -0,0 +1,25 @@
|
||||
import { getLocale } from "@/libs/locales";
|
||||
|
||||
export default function Testimonials1() {
|
||||
const { common } = getLocale('en');
|
||||
|
||||
return (
|
||||
<section>
|
||||
{/* TODO: Style this testimonials section when implementing in your project */}
|
||||
<div>
|
||||
<h2>{common.testimonials.title}</h2>
|
||||
<div>
|
||||
{common.testimonials.items.map((testimonial, index) => (
|
||||
<div key={index}>
|
||||
<p>{`"${testimonial.content}"`}</p>
|
||||
<div>
|
||||
<p>{testimonial.name}</p>
|
||||
<p>{testimonial.role}</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
32
apps/webapp/src/libs/locales.ts
Normal file
32
apps/webapp/src/libs/locales.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import commonEn from '@/locales/en/common.json';
|
||||
|
||||
// TODO: Add more languages as needed
|
||||
const locales = {
|
||||
en: {
|
||||
common: commonEn
|
||||
}
|
||||
};
|
||||
|
||||
export function getLocale(locale: string = 'en') {
|
||||
return locales[locale as keyof typeof locales] || locales.en;
|
||||
}
|
||||
|
||||
export function t(key: string, locale: string = 'en') {
|
||||
const translations = getLocale(locale);
|
||||
const keys = key.split('.');
|
||||
|
||||
let value: unknown = translations;
|
||||
for (const k of keys) {
|
||||
if (typeof value !== 'object' || value === null || !(k in value)) {
|
||||
return key;
|
||||
}
|
||||
|
||||
value = (value as Record<string, unknown>)[k];
|
||||
}
|
||||
|
||||
if (typeof value === 'string' || typeof value === 'number') {
|
||||
return String(value);
|
||||
}
|
||||
|
||||
return key;
|
||||
}
|
||||
85
apps/webapp/src/locales/en/common.json
Normal file
85
apps/webapp/src/locales/en/common.json
Normal file
@@ -0,0 +1,85 @@
|
||||
{
|
||||
"header": {
|
||||
"brand": "UltiPlate",
|
||||
"nav": {
|
||||
"home": "Home",
|
||||
"dashboard": "Dashboard",
|
||||
"blog": "Blog"
|
||||
},
|
||||
"actions": {
|
||||
"signIn": "Sign In"
|
||||
}
|
||||
},
|
||||
"footer": {
|
||||
"brand": "UltiPlate",
|
||||
"description": "Ultimate boilerplate for any project",
|
||||
"legal": {
|
||||
"title": "Legal",
|
||||
"privacyPolicy": "Privacy Policy",
|
||||
"termsOfService": "Terms of Service"
|
||||
},
|
||||
"company": {
|
||||
"title": "Company",
|
||||
"blog": "Blog",
|
||||
"dashboard": "Dashboard"
|
||||
},
|
||||
"copyright": "© 2024 UltiPlate. All rights reserved."
|
||||
},
|
||||
"hero": {
|
||||
"title": "Welcome to UltiPlate",
|
||||
"description": "The ultimate boilerplate for any project - from web apps to ML projects, all deployable with Docker.",
|
||||
"actions": {
|
||||
"getStarted": "Get Started",
|
||||
"learnMore": "Learn More"
|
||||
}
|
||||
},
|
||||
"features": {
|
||||
"title": "Features",
|
||||
"items": [
|
||||
{
|
||||
"title": "Docker Ready",
|
||||
"description": "Everything containerized and ready to deploy"
|
||||
},
|
||||
{
|
||||
"title": "ML Support",
|
||||
"description": "Built-in machine learning project structure"
|
||||
},
|
||||
{
|
||||
"title": "Multiple Backends",
|
||||
"description": "FastAPI, Flask, and more backend options"
|
||||
},
|
||||
{
|
||||
"title": "Centralized Logging",
|
||||
"description": "Loki-based logging across all services"
|
||||
},
|
||||
{
|
||||
"title": "Database Ready",
|
||||
"description": "Redis, MinIO, and database integrations"
|
||||
},
|
||||
{
|
||||
"title": "Web Apps",
|
||||
"description": "Next.js and Streamlit app templates"
|
||||
}
|
||||
]
|
||||
},
|
||||
"testimonials": {
|
||||
"title": "What People Say",
|
||||
"items": [
|
||||
{
|
||||
"name": "Alex Developer",
|
||||
"role": "Full Stack Developer",
|
||||
"content": "UltiPlate saved me weeks of setup time. Everything just works out of the box."
|
||||
},
|
||||
{
|
||||
"name": "Sarah ML Engineer",
|
||||
"role": "ML Engineer",
|
||||
"content": "The ML project structure is perfect. From notebooks to production in minutes."
|
||||
},
|
||||
{
|
||||
"name": "Mike DevOps",
|
||||
"role": "DevOps Engineer",
|
||||
"content": "Docker integration is seamless. Deploy anywhere with confidence."
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
8
apps/webapp/src/utils/supabase/client.ts
Normal file
8
apps/webapp/src/utils/supabase/client.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import { createBrowserClient } from '@supabase/ssr'
|
||||
|
||||
export function createClient() {
|
||||
return createBrowserClient(
|
||||
process.env.NEXT_PUBLIC_SUPABASE_URL!,
|
||||
process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY!
|
||||
)
|
||||
}
|
||||
66
apps/webapp/src/utils/supabase/middleware.ts
Normal file
66
apps/webapp/src/utils/supabase/middleware.ts
Normal file
@@ -0,0 +1,66 @@
|
||||
import { createServerClient } from '@supabase/ssr'
|
||||
import { NextResponse, type NextRequest } from 'next/server'
|
||||
|
||||
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_PUBLISHABLE_KEY!,
|
||||
{
|
||||
cookies: {
|
||||
getAll() {
|
||||
return request.cookies.getAll()
|
||||
},
|
||||
setAll(cookiesToSet) {
|
||||
cookiesToSet.forEach(({ name, value }) => request.cookies.set(name, value))
|
||||
supabaseResponse = NextResponse.next({
|
||||
request,
|
||||
})
|
||||
cookiesToSet.forEach(({ name, value, options }) =>
|
||||
supabaseResponse.cookies.set(name, value, options)
|
||||
)
|
||||
},
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
// Do not run code between createServerClient and
|
||||
// supabase.auth.getUser(). A simple mistake could make it very hard to debug
|
||||
// issues with users being randomly logged out.
|
||||
|
||||
// IMPORTANT: DO NOT REMOVE auth.getUser()
|
||||
|
||||
const {
|
||||
data: { user },
|
||||
} = await supabase.auth.getUser()
|
||||
|
||||
if (
|
||||
!user &&
|
||||
!request.nextUrl.pathname.startsWith('/login') &&
|
||||
!request.nextUrl.pathname.startsWith('/auth') &&
|
||||
!request.nextUrl.pathname.startsWith('/error')
|
||||
) {
|
||||
// no user, potentially respond by redirecting the user to the login page
|
||||
const url = request.nextUrl.clone()
|
||||
url.pathname = '/login'
|
||||
return NextResponse.redirect(url)
|
||||
}
|
||||
|
||||
// IMPORTANT: You *must* return the supabaseResponse object as it is.
|
||||
// If you're creating a new response object with NextResponse.next() make sure to:
|
||||
// 1. Pass the request in it, like so:
|
||||
// const myNewResponse = NextResponse.next({ request })
|
||||
// 2. Copy over the cookies, like so:
|
||||
// myNewResponse.cookies.setAll(supabaseResponse.cookies.getAll())
|
||||
// 3. Change the myNewResponse object to fit your needs, but avoid changing
|
||||
// the cookies!
|
||||
// 4. Finally:
|
||||
// return myNewResponse
|
||||
// If this is not done, you may be causing the browser and server to go out
|
||||
// of sync and terminate the user's session prematurely!
|
||||
|
||||
return supabaseResponse
|
||||
}
|
||||
29
apps/webapp/src/utils/supabase/server.ts
Normal file
29
apps/webapp/src/utils/supabase/server.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import { createServerClient } from '@supabase/ssr'
|
||||
import { cookies } from 'next/headers'
|
||||
|
||||
export async function createClient() {
|
||||
const cookieStore = await cookies()
|
||||
|
||||
return createServerClient(
|
||||
process.env.NEXT_PUBLIC_SUPABASE_URL!,
|
||||
process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY!,
|
||||
{
|
||||
cookies: {
|
||||
getAll() {
|
||||
return cookieStore.getAll()
|
||||
},
|
||||
setAll(cookiesToSet) {
|
||||
try {
|
||||
cookiesToSet.forEach(({ name, value, options }) =>
|
||||
cookieStore.set(name, value, options)
|
||||
)
|
||||
} catch {
|
||||
// The `setAll` method was called from a Server Component.
|
||||
// This can be ignored if you have middleware refreshing
|
||||
// user sessions.
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user