mirror of
https://github.com/supabase/supabase.git
synced 2026-10-05 09:25:06 +03:00
doc: Add Realtime Authorization Demo (#22500)
Add the Realtime Authorization Demo --------- Co-authored-by: Wen Bo Xie <5532241+w3b6x9@users.noreply.github.com> Co-authored-by: Wen Bo Xie <wenbo.xie3@gmail.com>
This commit is contained in:
28 files changed
+3182
No files matched your search
@@ -0,0 +1,2 @@
|
||||
NEXT_PUBLIC_SUPABASE_URL=
|
||||
NEXT_PUBLIC_SUPABASE_ANON_KEY=
|
||||
@@ -0,0 +1,36 @@
|
||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
/.pnp
|
||||
.pnp.js
|
||||
.yarn/install-state.gz
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
# next.js
|
||||
/.next/
|
||||
/out/
|
||||
|
||||
# production
|
||||
/build
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
*.pem
|
||||
|
||||
# debug
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
# local env files
|
||||
.env*.local
|
||||
|
||||
# vercel
|
||||
.vercel
|
||||
|
||||
# typescript
|
||||
*.tsbuildinfo
|
||||
next-env.d.ts
|
||||
@@ -0,0 +1 @@
|
||||
nodejs 20.12.0
|
||||
@@ -0,0 +1,233 @@
|
||||
# SupaSecureSlack
|
||||
|
||||
Example application on how you can use Realtime Authorization feature to limit access to Realtime [Channels](https://supabase.com/docs/guides/realtime/concepts#channels) and limit access to the [Broadcast](https://supabase.com/docs/guides/realtime/broadcast) feature.
|
||||
|
||||
You can provide feedback on our [Github Discussion](https://github.com/orgs/supabase/discussions/22484).
|
||||
|
||||
## Objective
|
||||
|
||||
Build a chat system using Realtime Broadcast with Authorized Channels where users can create rooms and invite existing users to a room.
|
||||
|
||||
Each room restricts the number of users authorized by applying RLS policies applied to `public` schema tables you'll be creating and the auto-generated `realtime` schema tables.
|
||||
|
||||
## Run It
|
||||
|
||||
1. Create a `.env.local` file with the required variables by running `cp .env.example .env.local`.
|
||||
2. [Create a new Supabase project](https://supabase.com/dashboard/new/_).
|
||||
3. Refer to the [Database Setup](#database-setup) section to create the necessary tables and policies.
|
||||
4. Copy the project's `URL` and `anon` API key from your project's [API Settings](https://supabase.com/dashboard/project/_/settings/api), and paste them into your `.env.local`.
|
||||
5. `npm install`
|
||||
6. `npm run dev`
|
||||
|
||||
## How It Looks
|
||||
|
||||
In this scenario both users are able to access it:
|
||||

|
||||
And here one of the user does not have access because their RLS rules made the user be denied access
|
||||

|
||||
|
||||
## Schema
|
||||
|
||||
We're taking advantage of Supabase's protected schemas to feed data to our application, namely `realtime.channels` and `auth.users`, so we can easily have cascading changes if a Channel / user is deleted.
|
||||
|
||||
We also need `public.profiles` with a trigger so we have a way to offer a public email to be used to invite people to the chat room.
|
||||
|
||||
Here's an overview of the two `public` tables we'll be creating for this demo:
|
||||
|
||||

|
||||
|
||||
## Database Setup
|
||||
|
||||
### Create Tables
|
||||
|
||||
#### `public.profiles`
|
||||
|
||||
```sql
|
||||
CREATE TABLE public.profiles (
|
||||
email text NOT NULL,
|
||||
user_id uuid REFERENCES auth.users (id)
|
||||
);
|
||||
```
|
||||
|
||||
#### `public.rooms_users`
|
||||
|
||||
```sql
|
||||
CREATE TABLE public.rooms_users (
|
||||
name text REFERENCES realtime.channels (name),
|
||||
user_id uuid REFERENCES auth.users (id),
|
||||
created_at timestamptz DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
```
|
||||
|
||||
### Create RLS Policies
|
||||
|
||||
We do have a lot of RLS policies to set up so we will group them by `schema.table` below.
|
||||
|
||||
The most noteworthy ones are the policies created for `realtime.broadcasts` table as they will limit access to rooms by checking if an entry for a given room name and user id exists in the `public.rooms_users` table.
|
||||
|
||||
> ⚠️ All the RLS policies here are meant for this demo. You may refer to them but make sure that your policies are tailored to your use case and secure your application.
|
||||
|
||||
#### `public.profiles`
|
||||
|
||||
```sql
|
||||
CREATE POLICY "authenticated users can view all profiles"
|
||||
ON "public"."profiles"
|
||||
AS PERMISSIVE FOR SELECT
|
||||
TO authenticated
|
||||
USING (true);
|
||||
|
||||
CREATE POLICY "supabase_auth_admin user can insert profile"
|
||||
ON "public"."profiles"
|
||||
AS PERMISSIVE FOR INSERT
|
||||
TO supabase_auth_admin
|
||||
WITH CHECK (true);
|
||||
```
|
||||
|
||||
#### `public.rooms_users`
|
||||
|
||||
```sql
|
||||
CREATE POLICY "authenticated users can read rooms_users"
|
||||
ON "public"."rooms_users"
|
||||
AS PERMISSIVE FOR SELECT
|
||||
TO authenticated
|
||||
USING (TRUE);
|
||||
|
||||
CREATE POLICY "authenticated can add rooms_users"
|
||||
ON "public"."rooms_users"
|
||||
AS PERMISSIVE FOR INSERT
|
||||
TO authenticated
|
||||
WITH CHECK (TRUE);
|
||||
```
|
||||
|
||||
#### `realtime.channels`
|
||||
|
||||
```sql
|
||||
CREATE POLICY "authenticated users can see all channels"
|
||||
ON "realtime"."channels"
|
||||
AS PERMISSIVE FOR SELECT
|
||||
TO authenticated
|
||||
USING (TRUE);
|
||||
|
||||
CREATE POLICY "authenticated can create channel"
|
||||
ON "realtime"."channels"
|
||||
AS PERMISSIVE FOR INSERT
|
||||
TO authenticated
|
||||
WITH CHECK (TRUE);
|
||||
```
|
||||
|
||||
#### `realtime.presences`
|
||||
|
||||
```sql
|
||||
CREATE POLICY "authenticated users can see all presences"
|
||||
ON "realtime"."presences"
|
||||
AS PERMISSIVE FOR SELECT
|
||||
TO authenticated
|
||||
USING (TRUE);
|
||||
|
||||
CREATE POLICY "authenticated can track presences"
|
||||
ON "realtime"."presences"
|
||||
AS PERMISSIVE FOR UPDATE
|
||||
TO authenticated
|
||||
USING (TRUE);
|
||||
```
|
||||
|
||||
#### `realtime.broadcasts`
|
||||
|
||||
```sql
|
||||
CREATE POLICY "authenticated user can enter room"
|
||||
ON "realtime"."broadcasts"
|
||||
AS PERMISSIVE FOR SELECT
|
||||
TO authenticated
|
||||
USING (
|
||||
EXISTS (
|
||||
SELECT
|
||||
ru.user_id
|
||||
FROM
|
||||
rooms_users ru
|
||||
WHERE
|
||||
ru.user_id = auth.uid()
|
||||
AND ru.name = realtime.channel_name()
|
||||
)
|
||||
);
|
||||
|
||||
CREATE POLICY "authenticated user can broadcast in room"
|
||||
ON "realtime"."broadcasts"
|
||||
AS PERMISSIVE FOR UPDATE
|
||||
TO authenticated
|
||||
USING (
|
||||
EXISTS (
|
||||
SELECT
|
||||
ru.user_id
|
||||
FROM
|
||||
rooms_users ru
|
||||
WHERE
|
||||
ru.user_id = auth.uid()
|
||||
AND ru.name = realtime.channel_name()
|
||||
)
|
||||
);
|
||||
```
|
||||
|
||||
### Create Database Function and Trigger
|
||||
|
||||
We need to create a database function and trigger to add an entry to `public.profiles` whenever a new user is created:
|
||||
|
||||
```sql
|
||||
CREATE OR REPLACE FUNCTION insert_user () RETURNS TRIGGER AS
|
||||
$$
|
||||
BEGIN
|
||||
INSERT INTO public.profiles (user_id, email) VALUES (NEW.id, NEW.email); RETURN NEW;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
CREATE OR REPLACE TRIGGER "on_new_auth_create_profile"
|
||||
AFTER INSERT ON auth.users FOR EACH ROW
|
||||
EXECUTE FUNCTION insert_user ();
|
||||
|
||||
GRANT EXECUTE ON FUNCTION insert_user () TO supabase_auth_admin;
|
||||
```
|
||||
|
||||
## Coding Concerns
|
||||
|
||||
### Create Channels Requirement
|
||||
|
||||
Before Realtime Authorization, Realtime Broadcast and Presence Channels were exposed to any client with a valid JWT, such as `anon` token.
|
||||
|
||||
If you want your Channel secured and adhere to RLS policies then you will need to create the Channel before allowing your clients to connect to the Channel.
|
||||
|
||||
> ⚠️ This will also impact access to `postgres_changes` Channels on connect.
|
||||
|
||||
You can create a Channel by making the following method call:
|
||||
|
||||
```ts
|
||||
await supabase.realtime.createChannel(name)
|
||||
```
|
||||
|
||||
You can check this code at the [create-room-modal.tsx](components/create-room-modal.tsx) component.
|
||||
|
||||
Also we're using the `next` version of `@supabase/realtime-js` which we need to override in `@supabase/supabase-js` in our [package.json](package.json):
|
||||
|
||||
```js
|
||||
// ...
|
||||
"overrides": {
|
||||
"@supabase/supabase-js": {
|
||||
"@supabase/realtime-js": "^2.10.0-next.6"
|
||||
}
|
||||
},
|
||||
// ...
|
||||
```
|
||||
|
||||
Or you can make a `POST` request to your project's `/realtime/v1/api/channels` endpoint:
|
||||
|
||||
```sh
|
||||
curl -v -X POST 'https://<project_ref>.supabase.co/realtime/v1/api/channels'\
|
||||
--header 'apikey: <anon_token>'\
|
||||
--header 'Authorization: Bearer <access_token>' \
|
||||
--header 'Content-Type: application/json'\
|
||||
--data-raw '{ "name": "<channel name>" }'
|
||||
```
|
||||
|
||||
### Connecting to Broadcast is Essentially the Same
|
||||
|
||||
The biggest difference between Broadcast and Broadcast Authorization is the latter's requirement of creating the Channel before clients subscribe to the Channel. Other than that, your clients can connect to Realtime Channel as they normally would and Realtime server will check RLS policies on client connect.
|
||||
|
||||
You can check this code at the [protected/page.tsx](app/protected/page.tsx).
|
||||
@@ -0,0 +1,19 @@
|
||||
import { createClient } from "@/utils/supabase/server";
|
||||
import { NextResponse } from "next/server";
|
||||
|
||||
export async function GET(request: Request) {
|
||||
// The `/auth/callback` route is required for the server-side auth flow implemented
|
||||
// by the SSR package. It exchanges an auth code for the user's session.
|
||||
// https://supabase.com/docs/guides/auth/server-side/nextjs
|
||||
const requestUrl = new URL(request.url);
|
||||
const code = requestUrl.searchParams.get("code");
|
||||
const origin = requestUrl.origin;
|
||||
|
||||
if (code) {
|
||||
const supabase = createClient();
|
||||
await supabase.auth.exchangeCodeForSession(code);
|
||||
}
|
||||
|
||||
// URL to redirect to after sign up process completes
|
||||
return NextResponse.redirect(`${origin}/protected`);
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 25 KiB |
@@ -0,0 +1,42 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
@layer base {
|
||||
:root {
|
||||
--background: 200 20% 98%;
|
||||
--btn-background: 200 10% 91%;
|
||||
--btn-background-hover: 200 10% 89%;
|
||||
--foreground: 200 50% 3%;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
--background: 200 50% 3%;
|
||||
--btn-background: 200 10% 9%;
|
||||
--btn-background-hover: 200 10% 12%;
|
||||
--foreground: 200 20% 96%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@layer base {
|
||||
* {
|
||||
@apply border-foreground/20;
|
||||
}
|
||||
}
|
||||
|
||||
.animate-in {
|
||||
animation: animateIn 0.3s ease 0.15s both;
|
||||
}
|
||||
|
||||
@keyframes animateIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(10px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
import { GeistSans } from "geist/font/sans";
|
||||
import "./globals.css";
|
||||
|
||||
const defaultUrl = process.env.VERCEL_URL
|
||||
? `https://${process.env.VERCEL_URL}`
|
||||
: "http://localhost:3000";
|
||||
|
||||
export const metadata = {
|
||||
metadataBase: new URL(defaultUrl),
|
||||
title: "Next.js and Supabase Starter Kit",
|
||||
description: "The fastest way to build apps with Next.js and Supabase",
|
||||
};
|
||||
|
||||
export default function RootLayout({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<html lang="en" className={GeistSans.className}>
|
||||
<body className="bg-background text-foreground">
|
||||
<main className="h-screen flex flex-col items-center">{children}</main>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
import Link from "next/link";
|
||||
import { headers } from "next/headers";
|
||||
import { createClient } from "@/utils/supabase/server";
|
||||
import { redirect } from "next/navigation";
|
||||
import { SubmitButton } from "../../components/submit-button";
|
||||
|
||||
export default function Login({
|
||||
searchParams,
|
||||
}: {
|
||||
searchParams: { message: string };
|
||||
}) {
|
||||
const signIn = async (formData: FormData) => {
|
||||
"use server";
|
||||
|
||||
const email = formData.get("email") as string;
|
||||
const password = formData.get("password") as string;
|
||||
const supabase = createClient();
|
||||
|
||||
const { error } = await supabase.auth.signInWithPassword({
|
||||
email,
|
||||
password,
|
||||
});
|
||||
|
||||
if (error) {
|
||||
return redirect("/login?message=Could not authenticate user");
|
||||
}
|
||||
|
||||
return redirect("/protected");
|
||||
};
|
||||
|
||||
const signUp = async (formData: FormData) => {
|
||||
"use server";
|
||||
|
||||
const origin = headers().get("origin");
|
||||
const email = formData.get("email") as string;
|
||||
const password = formData.get("password") as string;
|
||||
const supabase = createClient();
|
||||
|
||||
const { error } = await supabase.auth.signUp({
|
||||
email,
|
||||
password,
|
||||
options: {
|
||||
emailRedirectTo: `${origin}/auth/callback`,
|
||||
},
|
||||
});
|
||||
|
||||
if (error) {
|
||||
return redirect("/login?message=Could not authenticate user");
|
||||
}
|
||||
|
||||
return redirect("/login?message=Check email to continue sign in process");
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex-1 flex flex-col w-full px-8 sm:max-w-md justify-center gap-2">
|
||||
<Link
|
||||
href="/"
|
||||
className="absolute left-8 top-8 py-2 px-4 rounded-md no-underline text-foreground bg-btn-background hover:bg-btn-background-hover flex items-center group text-sm"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
className="mr-2 h-4 w-4 transition-transform group-hover:-translate-x-1"
|
||||
>
|
||||
<polyline points="15 18 9 12 15 6" />
|
||||
</svg>{" "}
|
||||
Back
|
||||
</Link>
|
||||
|
||||
<form className="animate-in flex-1 flex flex-col w-full justify-center gap-2 text-foreground">
|
||||
<label className="text-md" htmlFor="email">
|
||||
Email
|
||||
</label>
|
||||
<input
|
||||
className="rounded-md px-4 py-2 bg-inherit border mb-6"
|
||||
name="email"
|
||||
placeholder="you@example.com"
|
||||
required
|
||||
/>
|
||||
<label className="text-md" htmlFor="password">
|
||||
Password
|
||||
</label>
|
||||
<input
|
||||
className="rounded-md px-4 py-2 bg-inherit border mb-6"
|
||||
type="password"
|
||||
name="password"
|
||||
placeholder="••••••••"
|
||||
required
|
||||
/>
|
||||
<SubmitButton
|
||||
formAction={signIn}
|
||||
className="bg-green-700 rounded-md px-4 py-2 text-foreground mb-2"
|
||||
pendingText="Signing In..."
|
||||
>
|
||||
Sign In
|
||||
</SubmitButton>
|
||||
<SubmitButton
|
||||
formAction={signUp}
|
||||
className="border border-foreground/20 rounded-md px-4 py-2 text-foreground mb-2"
|
||||
pendingText="Signing Up..."
|
||||
>
|
||||
Sign Up
|
||||
</SubmitButton>
|
||||
{searchParams?.message && (
|
||||
<p className="mt-4 p-4 bg-foreground/10 text-foreground text-center">
|
||||
{searchParams.message}
|
||||
</p>
|
||||
)}
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
import SupabaseLogo from '@/components/SupabaseLogo'
|
||||
import AuthButton from '../components/AuthButton'
|
||||
import Link from 'next/link'
|
||||
import { createClient } from '@/utils/supabase/server'
|
||||
|
||||
export default async function Index() {
|
||||
const supabase = createClient()
|
||||
|
||||
const {
|
||||
data: { user },
|
||||
} = await supabase.auth.getUser()
|
||||
|
||||
return (
|
||||
<main className="flex flex-col gap-6 items-center h-full mt-40">
|
||||
<h2 className="flex items-center gap-6">
|
||||
<SupabaseLogo />
|
||||
</h2>
|
||||
{user ? (
|
||||
<Link
|
||||
href="/protected"
|
||||
className="py-2 px-3 flex rounded-md no-underline bg-btn-background hover:bg-btn-background-hover"
|
||||
>
|
||||
Chat
|
||||
</Link>
|
||||
) : (
|
||||
<AuthButton />
|
||||
)}
|
||||
</main>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,167 @@
|
||||
'use client'
|
||||
import CreateRoomModal from '@/components/create-room-modal'
|
||||
import { createClientComponentClient } from '@supabase/auth-helpers-nextjs'
|
||||
import { RealtimeChannel } from '@supabase/realtime-js'
|
||||
import { useState, useEffect } from 'react'
|
||||
|
||||
export default function Chat() {
|
||||
const [userId, setUserId] = useState<string | null>(null)
|
||||
const [loading, setLoading] = useState<boolean>(true)
|
||||
const [rooms, setRooms] = useState<any>([])
|
||||
const [selectedRoom, setSelectedRoom] = useState<string | undefined>()
|
||||
const [channel, setChannel] = useState<RealtimeChannel | null>(null)
|
||||
const [showModal, setShowModal] = useState<boolean>(false)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
|
||||
const supabase = createClientComponentClient()
|
||||
|
||||
const getChannels = async () => {
|
||||
await supabase.auth.getUser()
|
||||
const token = (await supabase.auth.getSession()).data.session?.access_token
|
||||
if (!token) return
|
||||
|
||||
supabase.realtime.setAuth(token)
|
||||
const channels = await supabase.realtime.listChannels()
|
||||
setRooms(channels.map((channel) => channel.name))
|
||||
setLoading(false)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
getChannels()
|
||||
supabase.auth.getUser().then((user) => setUserId(user.data.user?.id || null))
|
||||
}, [supabase])
|
||||
|
||||
useEffect(() => {
|
||||
if (document.getElementById('chat')) {
|
||||
document.getElementById('chat')!.innerHTML = ''
|
||||
}
|
||||
|
||||
if (selectedRoom) {
|
||||
channel?.unsubscribe()
|
||||
|
||||
let newChannel = supabase.realtime.channel(selectedRoom, {
|
||||
config: { broadcast: { self: true } },
|
||||
})
|
||||
|
||||
newChannel
|
||||
.on('broadcast', { event: 'message' }, ({ payload: payload }) => {
|
||||
const bubble = document.createElement('div')
|
||||
const mine =
|
||||
payload.user_id == userId ? ['bg-green-600', 'self-end'] : ['bg-blue-600', 'self-start']
|
||||
const style = [
|
||||
'flex',
|
||||
'gap-2',
|
||||
'items-center',
|
||||
'rounded-xl',
|
||||
'text-white',
|
||||
'text-bold',
|
||||
'w-2/3',
|
||||
'p-2',
|
||||
].concat(mine)
|
||||
bubble.classList.add(...style)
|
||||
bubble.innerHTML = payload.message
|
||||
document.getElementById('chat')!.appendChild(bubble)
|
||||
})
|
||||
.subscribe((status, err) => {
|
||||
if (status == 'SUBSCRIBED') {
|
||||
setChannel(newChannel)
|
||||
setLoading(false)
|
||||
setError(null)
|
||||
}
|
||||
if (status == 'CLOSED') {
|
||||
setChannel(null)
|
||||
}
|
||||
if (status == 'CHANNEL_ERROR') {
|
||||
console.log({ status, err })
|
||||
setError(err?.message || null)
|
||||
setLoading(false)
|
||||
}
|
||||
})
|
||||
}
|
||||
}, [selectedRoom])
|
||||
|
||||
return (
|
||||
<div className="flex w-full h-full p-10">
|
||||
{loading && (
|
||||
<div className="fixed top-0 left-0 right-0 flex flex-col h-full w-full justify-center items-center align-middle gap-2 z-10 bg-[#000000CC]">
|
||||
<div className="animate-spin rounded-full h-32 w-32 border-b-2 border-foreground"></div>
|
||||
</div>
|
||||
)}
|
||||
{showModal ? <CreateRoomModal /> : ''}
|
||||
<div className="flex w-full h-full gap-4">
|
||||
<div className="grow-0 flex flex-col gap-2">
|
||||
<div className="bg-white h-full rounded-sm text-slate-900">
|
||||
<div className="flex flex-col sm:max-w-md gap-2 text-foreground">
|
||||
{rooms?.map((room: string) => {
|
||||
return (
|
||||
<button
|
||||
key={room}
|
||||
onClick={() => {
|
||||
setLoading(true)
|
||||
setSelectedRoom(room)
|
||||
}}
|
||||
className={
|
||||
selectedRoom == room
|
||||
? 'bg-green-600 rounded-sm pointer p-1 text-white'
|
||||
: 'rounded-sm cursor-pointer hover:bg-green-100 p-1 text-black'
|
||||
}
|
||||
>
|
||||
# {room}
|
||||
</button>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
className="border border-foreground/20 rounded-md px-4 py-2 text-foreground"
|
||||
onClick={() => setShowModal(!showModal)}
|
||||
>
|
||||
Create Room
|
||||
</button>
|
||||
</div>
|
||||
<div className="grow flex flex-col gap-2">
|
||||
{error ? (
|
||||
<div className="bg-white h-full rounded-md text-slate-900 p-1 flex justify-center items-center">
|
||||
<h1 className="text-xl font-bold">You do not have access to this room</h1>
|
||||
</div>
|
||||
) : (
|
||||
<div
|
||||
className="bg-white h-full rounded-md text-slate-900 p-1 flex flex-col gap-2"
|
||||
id="chat"
|
||||
/>
|
||||
)}
|
||||
|
||||
<form
|
||||
className="flex text-foreground w-full gap-2"
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault()
|
||||
const form = e.target as HTMLFormElement
|
||||
const target = form.elements[0] as HTMLInputElement
|
||||
const message = target.value
|
||||
channel?.send({
|
||||
type: 'broadcast',
|
||||
event: 'message',
|
||||
payload: { message, user_id: userId },
|
||||
})
|
||||
target.value = ''
|
||||
}}
|
||||
>
|
||||
<label className="hidden" htmlFor="message" />
|
||||
<input
|
||||
name="message"
|
||||
className="grow rounded-md text-black p-2"
|
||||
placeholder="Insert your message"
|
||||
disabled={!channel}
|
||||
></input>
|
||||
<button
|
||||
type="submit"
|
||||
className="border border-foreground/20 rounded-md px-4 py-2 text-foreground"
|
||||
>
|
||||
Send
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 677 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 661 KiB |
@@ -0,0 +1,37 @@
|
||||
import { createClient } from "@/utils/supabase/server";
|
||||
import Link from "next/link";
|
||||
import { redirect } from "next/navigation";
|
||||
|
||||
export default async function AuthButton() {
|
||||
const supabase = createClient();
|
||||
|
||||
const {
|
||||
data: { user },
|
||||
} = await supabase.auth.getUser();
|
||||
|
||||
const signOut = async () => {
|
||||
"use server";
|
||||
|
||||
const supabase = createClient();
|
||||
await supabase.auth.signOut();
|
||||
return redirect("/login");
|
||||
};
|
||||
|
||||
return user ? (
|
||||
<div className="flex items-center gap-4">
|
||||
{user.email}{" "}
|
||||
<form action={signOut}>
|
||||
<button className="py-2 px-4 rounded-md no-underline bg-btn-background hover:bg-btn-background-hover">
|
||||
Logout
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
) : (
|
||||
<Link
|
||||
href="/login"
|
||||
className="py-2 px-3 flex rounded-md no-underline bg-btn-background hover:bg-btn-background-hover"
|
||||
>
|
||||
Login
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
export default function SupabaseLogo() {
|
||||
return (
|
||||
<svg
|
||||
aria-label="Supabase logo"
|
||||
width="140"
|
||||
height="30"
|
||||
viewBox="0 0 115 23"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<g clipPath="url(#clip0_4671_51136)">
|
||||
<g clipPath="url(#clip1_4671_51136)">
|
||||
<path
|
||||
d="M13.4028 21.8652C12.8424 22.5629 11.7063 22.1806 11.6928 21.2898L11.4954 8.25948H20.3564C21.9614 8.25948 22.8565 10.0924 21.8585 11.3353L13.4028 21.8652Z"
|
||||
fill="url(#paint0_linear_4671_51136)"
|
||||
/>
|
||||
<path
|
||||
d="M13.4028 21.8652C12.8424 22.5629 11.7063 22.1806 11.6928 21.2898L11.4954 8.25948H20.3564C21.9614 8.25948 22.8565 10.0924 21.8585 11.3353L13.4028 21.8652Z"
|
||||
fill="url(#paint1_linear_4671_51136)"
|
||||
fillOpacity="0.2"
|
||||
/>
|
||||
<path
|
||||
d="M9.79895 0.89838C10.3593 0.200591 11.4954 0.582929 11.5089 1.47383L11.5955 14.5041H2.84528C1.24026 14.5041 0.345103 12.6711 1.34316 11.4283L9.79895 0.89838Z"
|
||||
fill="#3ECF8E"
|
||||
/>
|
||||
</g>
|
||||
<path
|
||||
d="M30.5894 13.3913C30.7068 14.4766 31.7052 16.3371 34.6026 16.3371C37.1279 16.3371 38.3418 14.7479 38.3418 13.1976C38.3418 11.8022 37.3824 10.6588 35.4836 10.2712L34.1131 9.98049C33.5846 9.88359 33.2323 9.5929 33.2323 9.12777C33.2323 8.58512 33.7804 8.17818 34.4656 8.17818C35.5618 8.17818 35.9729 8.89521 36.0513 9.45725L38.2243 8.97275C38.1069 7.94561 37.1867 6.22083 34.446 6.22083C32.3709 6.22083 30.844 7.63555 30.844 9.34094C30.844 10.6781 31.6856 11.7828 33.5454 12.1898L34.8179 12.4805C35.5618 12.6355 35.8555 12.9844 35.8555 13.4107C35.8555 13.9146 35.4444 14.3603 34.583 14.3603C33.4476 14.3603 32.8797 13.6626 32.8212 12.9068L30.5894 13.3913Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<path
|
||||
d="M46.6623 16.0464H49.1486C49.1094 15.717 49.0506 15.0581 49.0506 14.3216V6.51154H46.4468V12.0542C46.4468 13.1588 45.7813 13.934 44.6263 13.934C43.4126 13.934 42.8643 13.0813 42.8643 12.0154V6.51154H40.2606V12.5387C40.2606 14.6123 41.5918 16.2984 43.9215 16.2984C44.9393 16.2984 46.0556 15.9108 46.5841 15.0193C46.5841 15.4069 46.6231 15.8526 46.6623 16.0464Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<path
|
||||
d="M54.433 19.7286V15.1162C54.9027 15.7558 55.8817 16.279 57.213 16.279C59.9341 16.279 61.7545 14.1472 61.7545 11.2596C61.7545 8.43021 60.1298 6.29842 57.3108 6.29842C55.8623 6.29842 54.7855 6.93792 54.3548 7.67439V6.51159H51.8295V19.7286H54.433ZM59.19 11.279C59.19 12.9845 58.133 13.9728 56.8017 13.9728C55.4708 13.9728 54.394 12.9651 54.394 11.279C54.394 9.59299 55.4708 8.6046 56.8017 8.6046C58.133 8.6046 59.19 9.59299 59.19 11.279Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<path
|
||||
d="M63.229 13.4495C63.229 14.9417 64.4818 16.3177 66.5375 16.3177C67.9662 16.3177 68.8865 15.6588 69.3758 14.9029C69.3758 15.2712 69.4149 15.7944 69.4737 16.0464H71.862C71.8033 15.7169 71.7449 15.0386 71.7449 14.5348V9.84482C71.7449 7.92622 70.6093 6.22083 67.5555 6.22083C64.9713 6.22083 63.5811 7.86807 63.4248 9.36033L65.7347 9.84482C65.8131 9.0115 66.4395 8.29445 67.5747 8.29445C68.6713 8.29445 69.1998 8.85646 69.1998 9.53475C69.1998 9.86421 69.0238 10.1355 68.4755 10.2131L66.1068 10.5619C64.5015 10.7945 63.229 11.744 63.229 13.4495ZM67.0854 14.3991C66.2438 14.3991 65.8325 13.8565 65.8325 13.2945C65.8325 12.558 66.361 12.1898 67.0268 12.0929L69.1998 11.7634V12.1898C69.1998 13.8759 68.1818 14.3991 67.0854 14.3991Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<path
|
||||
d="M76.895 16.0465V14.8837C77.4038 15.6976 78.4217 16.279 79.7531 16.279C82.4941 16.279 84.2951 14.1278 84.2951 11.2403C84.2951 8.4108 82.6701 6.25965 79.851 6.25965C78.4217 6.25965 77.3648 6.8798 76.934 7.55806V2.01546H74.3696V16.0465H76.895ZM81.6911 11.2596C81.6911 13.0038 80.6341 13.9728 79.3028 13.9728C77.9912 13.9728 76.895 12.9845 76.895 11.2596C76.895 9.51543 77.9912 8.56584 79.3028 8.56584C80.6341 8.56584 81.6911 9.51543 81.6911 11.2596Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<path
|
||||
d="M85.7692 13.4495C85.7692 14.9417 87.022 16.3177 89.0776 16.3177C90.5065 16.3177 91.4269 15.6588 91.916 14.9029C91.916 15.2712 91.9554 15.7944 92.014 16.0464H94.4023C94.3439 15.7169 94.2851 15.0386 94.2851 14.5348V9.84482C94.2851 7.92622 93.1495 6.22083 90.0955 6.22083C87.5115 6.22083 86.1216 7.86807 85.965 9.36033L88.2747 9.84482C88.3533 9.0115 88.9798 8.29445 90.1149 8.29445C91.2115 8.29445 91.74 8.85646 91.74 9.53475C91.74 9.86421 91.5638 10.1355 91.0156 10.2131L88.647 10.5619C87.0418 10.7945 85.7692 11.744 85.7692 13.4495ZM89.6258 14.3991C88.784 14.3991 88.3727 13.8565 88.3727 13.2945C88.3727 12.558 88.9012 12.1898 89.5671 12.0929L91.74 11.7634V12.1898C91.74 13.8759 90.722 14.3991 89.6258 14.3991Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<path
|
||||
d="M96.087 13.3913C96.2042 14.4766 97.2028 16.3371 100.1 16.3371C102.626 16.3371 103.839 14.7479 103.839 13.1976C103.839 11.8022 102.88 10.6588 100.981 10.2712L99.6105 9.98049C99.082 9.88359 98.7299 9.5929 98.7299 9.12777C98.7299 8.58512 99.2778 8.17818 99.963 8.17818C101.06 8.17818 101.471 8.89521 101.549 9.45725L103.722 8.97275C103.604 7.94561 102.684 6.22083 99.9436 6.22083C97.8683 6.22083 96.3416 7.63555 96.3416 9.34094C96.3416 10.6781 97.183 11.7828 99.043 12.1898L100.316 12.4805C101.06 12.6355 101.353 12.9844 101.353 13.4107C101.353 13.9146 100.942 14.3603 100.081 14.3603C98.9451 14.3603 98.3776 13.6626 98.3188 12.9068L96.087 13.3913Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<path
|
||||
d="M107.794 10.1937C107.852 9.32158 108.596 8.31381 109.947 8.31381C111.435 8.31381 112.062 9.24406 112.101 10.1937H107.794ZM112.355 12.6743C112.042 13.527 111.376 14.1278 110.163 14.1278C108.87 14.1278 107.794 13.2169 107.735 11.9573H114.626C114.626 11.9184 114.665 11.5309 114.665 11.1626C114.665 8.10064 112.884 6.22083 109.908 6.22083C107.441 6.22083 105.17 8.19753 105.17 11.2402C105.17 14.4572 107.5 16.3371 110.143 16.3371C112.512 16.3371 114.039 14.9611 114.528 13.3138L112.355 12.6743Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</g>
|
||||
<defs>
|
||||
<linearGradient
|
||||
id="paint0_linear_4671_51136"
|
||||
x1="11.4954"
|
||||
y1="11.1486"
|
||||
x2="19.3439"
|
||||
y2="14.4777"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop stopColor="#249361" />
|
||||
<stop offset="1" stopColor="#3ECF8E" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="paint1_linear_4671_51136"
|
||||
x1="8.00382"
|
||||
y1="6.42177"
|
||||
x2="11.5325"
|
||||
y2="13.1398"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop />
|
||||
<stop offset="1" stopOpacity="0" />
|
||||
</linearGradient>
|
||||
<clipPath id="clip0_4671_51136">
|
||||
<rect
|
||||
width="113.85"
|
||||
height="21.8943"
|
||||
fill="currentColor"
|
||||
transform="translate(0.922119 0.456161)"
|
||||
/>
|
||||
</clipPath>
|
||||
<clipPath id="clip1_4671_51136">
|
||||
<rect
|
||||
width="21.3592"
|
||||
height="21.8943"
|
||||
fill="currentColor"
|
||||
transform="translate(0.919006 0.497101)"
|
||||
/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
'use client'
|
||||
import { createClient } from '@/utils/supabase/client'
|
||||
import { redirect } from 'next/navigation'
|
||||
import { SubmitButton } from './submit-button'
|
||||
import { useEffect, useState } from 'react'
|
||||
|
||||
export default function CreateRoomModal() {
|
||||
const supabase = createClient()
|
||||
const [emails, setEmails] = useState<{ email: string; user_id: string }[] | undefined>([])
|
||||
useEffect(() => {
|
||||
supabase
|
||||
.from('profiles')
|
||||
.select('*')
|
||||
.then((res) => setEmails(res.data as { email: string; user_id: string }[]))
|
||||
}, [])
|
||||
|
||||
const createRoom = async (formData: FormData) => {
|
||||
const name = formData.get('name') as string
|
||||
const ids = formData.getAll('emails[]')
|
||||
await supabase.auth.getUser()
|
||||
const token = (await supabase.auth.getSession()).data.session!.access_token
|
||||
supabase.realtime.setAuth(token)
|
||||
await supabase.realtime.createChannel(name)
|
||||
await supabase.from('rooms_users').insert(ids.map((user_id) => ({ user_id, name })))
|
||||
return redirect(`/protected`)
|
||||
}
|
||||
|
||||
const close = async () => {
|
||||
return redirect(`/protected`)
|
||||
}
|
||||
return (
|
||||
<div className="fixed top-0 left-0 right-0 flex flex-col h-full w-full justify-center items-center align-middle gap-2 z-10 bg-[#000000AA]">
|
||||
<form className="flex flex-col sm:max-w-md gap-2 text-foreground bg-background rounded-md p-4">
|
||||
<label className="text-md" htmlFor="email">
|
||||
Room
|
||||
</label>
|
||||
<input
|
||||
className="rounded-md px-4 py-2 bg-inherit border mb-6"
|
||||
name="name"
|
||||
placeholder="room_1"
|
||||
/>
|
||||
{emails && (
|
||||
<div className="flex flex-col">
|
||||
{emails?.map(({ email, user_id }) => (
|
||||
<div key={`container_${email}`}>
|
||||
<input key={`label_${email}`} type="checkbox" name="emails[]" value={user_id} />
|
||||
|
||||
<label key={email} htmlFor="emails[]">
|
||||
{email}
|
||||
</label>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
<SubmitButton
|
||||
formAction={createRoom}
|
||||
className="border border-foreground/20 rounded-md px-4 py-2 text-foreground mb-2"
|
||||
pendingText="Creating"
|
||||
>
|
||||
Create Room
|
||||
</SubmitButton>
|
||||
<SubmitButton
|
||||
formAction={close}
|
||||
className="border border-foreground/20 rounded-md px-4 py-2 text-foreground mb-2"
|
||||
pendingText="Closing"
|
||||
>
|
||||
Close
|
||||
</SubmitButton>
|
||||
</form>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
"use client";
|
||||
|
||||
import { useFormStatus } from "react-dom";
|
||||
import { type ComponentProps } from "react";
|
||||
|
||||
type Props = ComponentProps<"button"> & {
|
||||
pendingText?: string;
|
||||
};
|
||||
|
||||
export function SubmitButton({ children, pendingText, ...props }: Props) {
|
||||
const { pending, action } = useFormStatus();
|
||||
|
||||
const isPending = pending && action === props.formAction;
|
||||
|
||||
return (
|
||||
<button {...props} type="submit" aria-disabled={pending}>
|
||||
{isPending ? pendingText : children}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
import { type NextRequest } from "next/server";
|
||||
import { updateSession } from "@/utils/supabase/middleware";
|
||||
|
||||
export async function middleware(request: NextRequest) {
|
||||
return await updateSession(request);
|
||||
}
|
||||
|
||||
export const config = {
|
||||
matcher: [
|
||||
/*
|
||||
* Match all request paths except:
|
||||
* - _next/static (static files)
|
||||
* - _next/image (image optimization files)
|
||||
* - favicon.ico (favicon file)
|
||||
* - images - .svg, .png, .jpg, .jpeg, .gif, .webp
|
||||
* Feel free to modify this pattern to include more paths.
|
||||
*/
|
||||
"/((?!_next/static|_next/image|favicon.ico|.*\\.(?:svg|png|jpg|jpeg|gif|webp)$).*)",
|
||||
],
|
||||
};
|
||||
@@ -0,0 +1,4 @@
|
||||
/** @type {import('next').NextConfig} */
|
||||
const nextConfig = { productionBrowserSourceMaps: true }
|
||||
|
||||
module.exports = nextConfig
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,36 @@
|
||||
{
|
||||
"private": true,
|
||||
"engines" : {
|
||||
"node": ">=20.0.0"
|
||||
},
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
"build": "next build",
|
||||
"start": "next start"
|
||||
},
|
||||
"dependencies": {
|
||||
"@supabase/auth-helpers-nextjs": "^0.9.0",
|
||||
"@supabase/realtime-js": "^2.10.0-next.5",
|
||||
"@supabase/ssr": "latest",
|
||||
"@supabase/supabase-js": "latest",
|
||||
"autoprefixer": "10.4.17",
|
||||
"geist": "^1.2.1",
|
||||
"next": "latest",
|
||||
"postcss": "8.4.33",
|
||||
"react": "18.2.0",
|
||||
"react-dom": "18.2.0",
|
||||
"tailwindcss": "3.4.1",
|
||||
"typescript": "5.3.3"
|
||||
},
|
||||
"overrides": {
|
||||
"@supabase/supabase-js": {
|
||||
"@supabase/realtime-js": "^2.10.0-next.6"
|
||||
}
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "20.11.5",
|
||||
"@types/react": "18.2.48",
|
||||
"@types/react-dom": "18.2.18",
|
||||
"encoding": "^0.1.13"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
module.exports = {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
};
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 108 KiB |
@@ -0,0 +1,17 @@
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
module.exports = {
|
||||
content: ['./app/**/*.{js,ts,jsx,tsx,mdx}', './components/**/*.{js,ts,jsx,tsx,mdx}'],
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
background: '#343434',
|
||||
foreground: 'hsl(var(--foreground))',
|
||||
btn: {
|
||||
background: 'hsl(var(--btn-background))',
|
||||
'background-hover': 'hsl(var(--btn-background-hover))',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: [],
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "es5",
|
||||
"lib": ["dom", "dom.iterable", "esnext"],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"noEmit": true,
|
||||
"esModuleInterop": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "node",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"jsx": "preserve",
|
||||
"incremental": true,
|
||||
"plugins": [
|
||||
{
|
||||
"name": "next"
|
||||
}
|
||||
],
|
||||
"paths": {
|
||||
"@/*": ["./*"]
|
||||
}
|
||||
},
|
||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
import { createBrowserClient } from "@supabase/ssr";
|
||||
|
||||
export const createClient = () =>
|
||||
createBrowserClient(
|
||||
process.env.NEXT_PUBLIC_SUPABASE_URL!,
|
||||
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
|
||||
);
|
||||
@@ -0,0 +1,78 @@
|
||||
import { createServerClient, type CookieOptions } from "@supabase/ssr";
|
||||
import { type NextRequest, NextResponse } from "next/server";
|
||||
|
||||
export const updateSession = async (request: NextRequest) => {
|
||||
// This `try/catch` block is only here for the interactive tutorial.
|
||||
// Feel free to remove once you have Supabase connected.
|
||||
try {
|
||||
// Create an unmodified response
|
||||
let response = NextResponse.next({
|
||||
request: {
|
||||
headers: request.headers,
|
||||
},
|
||||
});
|
||||
|
||||
const supabase = createServerClient(
|
||||
process.env.NEXT_PUBLIC_SUPABASE_URL!,
|
||||
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
|
||||
{
|
||||
cookies: {
|
||||
get(name: string) {
|
||||
return request.cookies.get(name)?.value;
|
||||
},
|
||||
set(name: string, value: string, options: CookieOptions) {
|
||||
// If the cookie is updated, update the cookies for the request and response
|
||||
request.cookies.set({
|
||||
name,
|
||||
value,
|
||||
...options,
|
||||
});
|
||||
response = NextResponse.next({
|
||||
request: {
|
||||
headers: request.headers,
|
||||
},
|
||||
});
|
||||
response.cookies.set({
|
||||
name,
|
||||
value,
|
||||
...options,
|
||||
});
|
||||
},
|
||||
remove(name: string, options: CookieOptions) {
|
||||
// If the cookie is removed, update the cookies for the request and response
|
||||
request.cookies.set({
|
||||
name,
|
||||
value: "",
|
||||
...options,
|
||||
});
|
||||
response = NextResponse.next({
|
||||
request: {
|
||||
headers: request.headers,
|
||||
},
|
||||
});
|
||||
response.cookies.set({
|
||||
name,
|
||||
value: "",
|
||||
...options,
|
||||
});
|
||||
},
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
// This will refresh session if expired - required for Server Components
|
||||
// https://supabase.com/docs/guides/auth/server-side/nextjs
|
||||
await supabase.auth.getUser();
|
||||
|
||||
return response;
|
||||
} catch (e) {
|
||||
// If you are here, a Supabase client could not be created!
|
||||
// This is likely because you have not set up environment variables.
|
||||
// Check out http://localhost:3000 for Next Steps.
|
||||
return NextResponse.next({
|
||||
request: {
|
||||
headers: request.headers,
|
||||
},
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,36 @@
|
||||
import { createServerClient, type CookieOptions } from "@supabase/ssr";
|
||||
import { cookies } from "next/headers";
|
||||
|
||||
export const createClient = () => {
|
||||
const cookieStore = cookies();
|
||||
|
||||
return createServerClient(
|
||||
process.env.NEXT_PUBLIC_SUPABASE_URL!,
|
||||
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
|
||||
{
|
||||
cookies: {
|
||||
get(name: string) {
|
||||
return cookieStore.get(name)?.value;
|
||||
},
|
||||
set(name: string, value: string, options: CookieOptions) {
|
||||
try {
|
||||
cookieStore.set({ name, value, ...options });
|
||||
} catch (error) {
|
||||
// The `set` method was called from a Server Component.
|
||||
// This can be ignored if you have middleware refreshing
|
||||
// user sessions.
|
||||
}
|
||||
},
|
||||
remove(name: string, options: CookieOptions) {
|
||||
try {
|
||||
cookieStore.set({ name, value: "", ...options });
|
||||
} catch (error) {
|
||||
// The `delete` 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