This commit is contained in:
giancarlo
2024-03-24 02:23:22 +08:00
parent 648d77b430
commit bce3479368
589 changed files with 37067 additions and 9596 deletions

View File

@@ -0,0 +1,23 @@
import type { SupabaseClient } from '@supabase/supabase-js';
const ASSURANCE_LEVEL_2 = 'aal2';
/**
* @name checkRequiresMultiFactorAuthentication
* @description Checks if the current session requires multi-factor authentication.
* We do it by checking that the next assurance level is AAL2 and that the current assurance level is not AAL2.
* @param client
*/
export async function checkRequiresMultiFactorAuthentication(
client: SupabaseClient,
) {
const assuranceLevel = await client.auth.mfa.getAuthenticatorAssuranceLevel();
if (assuranceLevel.error) {
throw new Error(assuranceLevel.error.message);
}
const { nextLevel, currentLevel } = assuranceLevel.data;
return nextLevel === ASSURANCE_LEVEL_2 && nextLevel !== currentLevel;
}

View File

@@ -0,0 +1,20 @@
import { invariant } from '@epic-web/invariant';
import { createBrowserClient } from '@supabase/ssr';
import { Database } from '../database.types';
let client: ReturnType<typeof createBrowserClient>;
export function getSupabaseBrowserClient<GenericSchema = Database>() {
const SUPABASE_URL = process.env.NEXT_PUBLIC_SUPABASE_URL;
const SUPABASE_ANON_KEY = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY;
invariant(SUPABASE_URL, `Supabase URL was not provided`);
invariant(SUPABASE_ANON_KEY, `Supabase Anon key was not provided`);
if (client) return client;
client = createBrowserClient<GenericSchema>(SUPABASE_URL, SUPABASE_ANON_KEY);
return client;
}

View File

@@ -0,0 +1,65 @@
import { type NextRequest, NextResponse } from 'next/server';
import { type CookieOptions, createServerClient } from '@supabase/ssr';
import { Database } from '../database.types';
import { getSupabaseClientKeys } from '../get-supabase-client-keys';
/**
* Creates a middleware client for Supabase.
*
* @param {NextRequest} request - The Next.js request object.
* @param {NextResponse} response - The Next.js response object.
*/
export function createMiddlewareClient<GenericSchema = Database>(
request: NextRequest,
response: NextResponse,
) {
const keys = getSupabaseClientKeys();
return createServerClient<GenericSchema>(keys.url, keys.anonKey, {
cookies: getCookieStrategy(request, response),
});
}
function getCookieStrategy(request: NextRequest, response: NextResponse) {
return {
set: (name: string, value: string, options: CookieOptions) => {
request.cookies.set({ name, value, ...options });
response = NextResponse.next({
request: {
headers: request.headers,
},
});
response.cookies.set({
name,
value,
...options,
});
},
get: (name: string) => {
return request.cookies.get(name)?.value;
},
remove: (name: string, options: CookieOptions) => {
request.cookies.set({
name,
value: '',
...options,
});
response = NextResponse.next({
request: {
headers: request.headers,
},
});
response.cookies.set({
name,
value: '',
...options,
});
},
};
}

View File

@@ -0,0 +1,61 @@
import { cookies } from 'next/headers';
import type { CookieOptions } from '@supabase/ssr';
import { createServerClient } from '@supabase/ssr';
import 'server-only';
import { Database } from '../database.types';
import { getSupabaseClientKeys } from '../get-supabase-client-keys';
/**
* @name getSupabaseRouteHandlerClient
* @description Get a Supabase client for use in the Route Handler Routes
*/
export function getSupabaseRouteHandlerClient<GenericSchema = Database>(
params = {
admin: false,
},
) {
const keys = getSupabaseClientKeys();
if (params.admin) {
const serviceRoleKey = process.env.SUPABASE_SERVICE_ROLE_KEY;
if (process.env.NODE_ENV !== 'production') {
console.warn(
`[Dev Only] You are using the Supabase Service Role. Make sure it's the right call.`,
);
}
if (!serviceRoleKey) {
throw new Error('Supabase Service Role Key not provided');
}
return createServerClient<GenericSchema>(keys.url, serviceRoleKey, {
auth: {
persistSession: false,
},
cookies: {},
});
}
return createServerClient<GenericSchema>(keys.url, keys.anonKey, {
cookies: getCookiesStrategy(),
});
}
function getCookiesStrategy() {
const cookieStore = cookies();
return {
set: (name: string, value: string, options: CookieOptions) => {
cookieStore.set({ name, value, ...options });
},
get: (name: string) => {
return cookieStore.get(name)?.value;
},
remove: (name: string, options: CookieOptions) => {
cookieStore.set({ name, value: '', ...options });
},
};
}

View File

@@ -0,0 +1,67 @@
import { cookies } from 'next/headers';
import { createServerClient } from '@supabase/ssr';
import 'server-only';
import { Database } from '../database.types';
import { getSupabaseClientKeys } from '../get-supabase-client-keys';
const createServerSupabaseClient = <GenericSchema = Database>() => {
const keys = getSupabaseClientKeys();
return createServerClient<GenericSchema>(keys.url, keys.anonKey, {
cookies: getCookiesStrategy(),
});
};
export const getSupabaseServerActionClient = <
GenericSchema = Database,
>(params?: {
admin: false;
}) => {
const keys = getSupabaseClientKeys();
const admin = params?.admin ?? false;
if (admin) {
const serviceRoleKey = process.env.SUPABASE_SERVICE_ROLE_KEY;
if (process.env.NODE_ENV !== 'production') {
console.warn(
`[Dev Only] You are using the Supabase Service Role. Make sure it's the right call.`,
);
}
if (!serviceRoleKey) {
throw new Error('Supabase Service Role Key not provided');
}
return createServerClient<GenericSchema>(keys.url, serviceRoleKey, {
auth: {
persistSession: false,
},
cookies: {},
});
}
return createServerSupabaseClient<GenericSchema>();
};
function getCookiesStrategy() {
const cookieStore = cookies();
return {
get: (name: string) => {
return cookieStore.get(name)?.value;
},
set: (name: string, value: string, options: object) => {
cookieStore.set({ name, value, ...options });
},
remove: (name: string, options: object) => {
cookieStore.set({
name,
value: '',
...options,
});
},
};
}

View File

@@ -0,0 +1,54 @@
import { cookies } from 'next/headers';
import { createServerClient } from '@supabase/ssr';
import 'server-only';
import { Database } from '../database.types';
import { getSupabaseClientKeys } from '../get-supabase-client-keys';
/**
* @name getSupabaseServerComponentClient
* @description Get a Supabase client for use in the Server Components
*/
export const getSupabaseServerComponentClient = <GenericSchema = Database>(
params = {
admin: false,
},
) => {
const keys = getSupabaseClientKeys();
if (params.admin) {
const serviceRoleKey = process.env.SUPABASE_SERVICE_ROLE_KEY;
if (process.env.NODE_ENV !== 'production') {
console.warn(
`[Dev Only] You are using the Supabase Service Role. Make sure it's the right call.`,
);
}
if (!serviceRoleKey) {
throw new Error('Supabase Service Role Key not provided');
}
return createServerClient<GenericSchema>(keys.url, serviceRoleKey, {
auth: {
persistSession: false,
},
cookies: {},
});
}
return createServerClient<GenericSchema>(keys.url, keys.anonKey, {
cookies: getCookiesStrategy(),
});
};
function getCookiesStrategy() {
const cookieStore = cookies();
return {
get: (name: string) => {
return cookieStore.get(name)?.value;
},
};
}

View File

@@ -0,0 +1,96 @@
'use client';
import { useEffect } from 'react';
import { usePathname, useRouter } from 'next/navigation';
import { isBrowser } from '@supabase/ssr';
import { useSupabase } from '../hooks/use-supabase';
import {
useRevalidateUserSession,
useUserSession,
} from '../hooks/use-user-session';
function AuthRedirectListener({
children,
appHomePath,
}: React.PropsWithChildren<{
appHomePath: string;
}>) {
const client = useSupabase();
const pathName = usePathname();
const router = useRouter();
const revalidateUserSession = useRevalidateUserSession();
const session = useUserSession();
const accessToken = session.data?.access_token;
useEffect(() => {
// keep this running for the whole session unless the component was unmounted
const listener = client.auth.onAuthStateChange((_, user) => {
// log user out if user is falsy
// and if the current path is a private route
const shouldRedirectUser = !user && isPrivateRoute(pathName);
if (shouldRedirectUser) {
// send user away when signed out
window.location.assign('/');
return;
}
if (accessToken) {
const isOutOfSync = user?.access_token !== accessToken;
if (isOutOfSync) {
void router.refresh();
}
}
if (user && isPrivateRoute(pathName)) {
return revalidateUserSession();
}
});
// destroy listener on un-mounts
return () => listener.data.subscription.unsubscribe();
}, [
client.auth,
router,
accessToken,
revalidateUserSession,
pathName,
appHomePath,
]);
return <>{children}</>;
}
export function AuthChangeListener({
children,
appHomePath,
}: React.PropsWithChildren<{
appHomePath: string;
privateRoutes?: string[];
}>) {
const shouldActivateListener = isBrowser();
// we only activate the listener if
// we are rendering in the browser
if (!shouldActivateListener) {
return <>{children}</>;
}
return (
<AuthRedirectListener appHomePath={appHomePath}>
{children}
</AuthRedirectListener>
);
}
function isPrivateRoute(path: string) {
// TODO: use config
const prefixes = ['/home', '/admin', '/password-reset'];
return prefixes.some((prefix) => path.startsWith(prefix));
}

View File

@@ -0,0 +1,948 @@
export type Json =
| string
| number
| boolean
| null
| { [key: string]: Json | undefined }
| Json[]
export type Database = {
graphql_public: {
Tables: {
[_ in never]: never
}
Views: {
[_ in never]: never
}
Functions: {
graphql: {
Args: {
operationName?: string
query?: string
variables?: Json
extensions?: Json
}
Returns: Json
}
}
Enums: {
[_ in never]: never
}
CompositeTypes: {
[_ in never]: never
}
}
public: {
Tables: {
account_roles: {
Row: {
account_id: string
id: number
role: Database["public"]["Enums"]["account_role"]
}
Insert: {
account_id: string
id?: number
role: Database["public"]["Enums"]["account_role"]
}
Update: {
account_id?: string
id?: number
role?: Database["public"]["Enums"]["account_role"]
}
Relationships: [
{
foreignKeyName: "account_roles_account_id_fkey"
columns: ["account_id"]
isOneToOne: false
referencedRelation: "accounts"
referencedColumns: ["id"]
},
{
foreignKeyName: "account_roles_account_id_fkey"
columns: ["account_id"]
isOneToOne: false
referencedRelation: "user_account_workspace"
referencedColumns: ["id"]
},
{
foreignKeyName: "account_roles_account_id_fkey"
columns: ["account_id"]
isOneToOne: false
referencedRelation: "user_accounts"
referencedColumns: ["id"]
},
]
}
accounts: {
Row: {
created_at: string | null
created_by: string | null
email: string | null
id: string
is_personal_account: boolean
name: string
picture_url: string | null
primary_owner_user_id: string
slug: string | null
updated_at: string | null
updated_by: string | null
}
Insert: {
created_at?: string | null
created_by?: string | null
email?: string | null
id?: string
is_personal_account?: boolean
name: string
picture_url?: string | null
primary_owner_user_id?: string
slug?: string | null
updated_at?: string | null
updated_by?: string | null
}
Update: {
created_at?: string | null
created_by?: string | null
email?: string | null
id?: string
is_personal_account?: boolean
name?: string
picture_url?: string | null
primary_owner_user_id?: string
slug?: string | null
updated_at?: string | null
updated_by?: string | null
}
Relationships: [
{
foreignKeyName: "accounts_created_by_fkey"
columns: ["created_by"]
isOneToOne: false
referencedRelation: "users"
referencedColumns: ["id"]
},
{
foreignKeyName: "accounts_primary_owner_user_id_fkey"
columns: ["primary_owner_user_id"]
isOneToOne: false
referencedRelation: "users"
referencedColumns: ["id"]
},
{
foreignKeyName: "accounts_updated_by_fkey"
columns: ["updated_by"]
isOneToOne: false
referencedRelation: "users"
referencedColumns: ["id"]
},
]
}
accounts_memberships: {
Row: {
account_id: string
account_role: Database["public"]["Enums"]["account_role"]
created_at: string
created_by: string | null
updated_at: string
updated_by: string | null
user_id: string
}
Insert: {
account_id: string
account_role: Database["public"]["Enums"]["account_role"]
created_at?: string
created_by?: string | null
updated_at?: string
updated_by?: string | null
user_id: string
}
Update: {
account_id?: string
account_role?: Database["public"]["Enums"]["account_role"]
created_at?: string
created_by?: string | null
updated_at?: string
updated_by?: string | null
user_id?: string
}
Relationships: [
{
foreignKeyName: "accounts_memberships_account_id_fkey"
columns: ["account_id"]
isOneToOne: false
referencedRelation: "accounts"
referencedColumns: ["id"]
},
{
foreignKeyName: "accounts_memberships_account_id_fkey"
columns: ["account_id"]
isOneToOne: false
referencedRelation: "user_account_workspace"
referencedColumns: ["id"]
},
{
foreignKeyName: "accounts_memberships_account_id_fkey"
columns: ["account_id"]
isOneToOne: false
referencedRelation: "user_accounts"
referencedColumns: ["id"]
},
{
foreignKeyName: "accounts_memberships_created_by_fkey"
columns: ["created_by"]
isOneToOne: false
referencedRelation: "users"
referencedColumns: ["id"]
},
{
foreignKeyName: "accounts_memberships_updated_by_fkey"
columns: ["updated_by"]
isOneToOne: false
referencedRelation: "users"
referencedColumns: ["id"]
},
{
foreignKeyName: "accounts_memberships_user_id_fkey"
columns: ["user_id"]
isOneToOne: false
referencedRelation: "users"
referencedColumns: ["id"]
},
]
}
billing_customers: {
Row: {
account_id: string
customer_id: string
email: string | null
id: number
provider: Database["public"]["Enums"]["billing_provider"]
}
Insert: {
account_id: string
customer_id: string
email?: string | null
id?: number
provider: Database["public"]["Enums"]["billing_provider"]
}
Update: {
account_id?: string
customer_id?: string
email?: string | null
id?: number
provider?: Database["public"]["Enums"]["billing_provider"]
}
Relationships: [
{
foreignKeyName: "billing_customers_account_id_fkey"
columns: ["account_id"]
isOneToOne: false
referencedRelation: "accounts"
referencedColumns: ["id"]
},
{
foreignKeyName: "billing_customers_account_id_fkey"
columns: ["account_id"]
isOneToOne: false
referencedRelation: "user_account_workspace"
referencedColumns: ["id"]
},
{
foreignKeyName: "billing_customers_account_id_fkey"
columns: ["account_id"]
isOneToOne: false
referencedRelation: "user_accounts"
referencedColumns: ["id"]
},
]
}
config: {
Row: {
billing_provider:
| Database["public"]["Enums"]["billing_provider"]
| null
enable_account_billing: boolean | null
enable_organization_accounts: boolean | null
enable_organization_billing: boolean | null
}
Insert: {
billing_provider?:
| Database["public"]["Enums"]["billing_provider"]
| null
enable_account_billing?: boolean | null
enable_organization_accounts?: boolean | null
enable_organization_billing?: boolean | null
}
Update: {
billing_provider?:
| Database["public"]["Enums"]["billing_provider"]
| null
enable_account_billing?: boolean | null
enable_organization_accounts?: boolean | null
enable_organization_billing?: boolean | null
}
Relationships: []
}
invitations: {
Row: {
account_id: string
created_at: string
email: string
id: number
invite_token: string
invited_by: string
role: Database["public"]["Enums"]["account_role"]
updated_at: string
}
Insert: {
account_id: string
created_at?: string
email: string
id?: number
invite_token: string
invited_by: string
role: Database["public"]["Enums"]["account_role"]
updated_at?: string
}
Update: {
account_id?: string
created_at?: string
email?: string
id?: number
invite_token?: string
invited_by?: string
role?: Database["public"]["Enums"]["account_role"]
updated_at?: string
}
Relationships: [
{
foreignKeyName: "invitations_account_id_fkey"
columns: ["account_id"]
isOneToOne: false
referencedRelation: "accounts"
referencedColumns: ["id"]
},
{
foreignKeyName: "invitations_account_id_fkey"
columns: ["account_id"]
isOneToOne: false
referencedRelation: "user_account_workspace"
referencedColumns: ["id"]
},
{
foreignKeyName: "invitations_account_id_fkey"
columns: ["account_id"]
isOneToOne: false
referencedRelation: "user_accounts"
referencedColumns: ["id"]
},
{
foreignKeyName: "invitations_invited_by_fkey"
columns: ["invited_by"]
isOneToOne: false
referencedRelation: "users"
referencedColumns: ["id"]
},
]
}
role_permissions: {
Row: {
id: number
permission: Database["public"]["Enums"]["app_permissions"]
role: Database["public"]["Enums"]["account_role"]
}
Insert: {
id?: number
permission: Database["public"]["Enums"]["app_permissions"]
role: Database["public"]["Enums"]["account_role"]
}
Update: {
id?: number
permission?: Database["public"]["Enums"]["app_permissions"]
role?: Database["public"]["Enums"]["account_role"]
}
Relationships: []
}
subscriptions: {
Row: {
account_id: string
billing_customer_id: number
billing_provider: Database["public"]["Enums"]["billing_provider"]
cancel_at_period_end: boolean
created_at: string
currency: string
id: string
interval: string
interval_count: number
last_payment_at: string | null
period_ends_at: string | null
period_starts_at: string | null
price_amount: number | null
product_id: string
status: Database["public"]["Enums"]["subscription_status"]
trial_ends_at: string | null
trial_starts_at: string | null
updated_at: string
variant_id: string
}
Insert: {
account_id: string
billing_customer_id: number
billing_provider: Database["public"]["Enums"]["billing_provider"]
cancel_at_period_end: boolean
created_at?: string
currency: string
id: string
interval: string
interval_count: number
last_payment_at?: string | null
period_ends_at?: string | null
period_starts_at?: string | null
price_amount?: number | null
product_id: string
status: Database["public"]["Enums"]["subscription_status"]
trial_ends_at?: string | null
trial_starts_at?: string | null
updated_at?: string
variant_id: string
}
Update: {
account_id?: string
billing_customer_id?: number
billing_provider?: Database["public"]["Enums"]["billing_provider"]
cancel_at_period_end?: boolean
created_at?: string
currency?: string
id?: string
interval?: string
interval_count?: number
last_payment_at?: string | null
period_ends_at?: string | null
period_starts_at?: string | null
price_amount?: number | null
product_id?: string
status?: Database["public"]["Enums"]["subscription_status"]
trial_ends_at?: string | null
trial_starts_at?: string | null
updated_at?: string
variant_id?: string
}
Relationships: [
{
foreignKeyName: "subscriptions_account_id_fkey"
columns: ["account_id"]
isOneToOne: false
referencedRelation: "accounts"
referencedColumns: ["id"]
},
{
foreignKeyName: "subscriptions_account_id_fkey"
columns: ["account_id"]
isOneToOne: false
referencedRelation: "user_account_workspace"
referencedColumns: ["id"]
},
{
foreignKeyName: "subscriptions_account_id_fkey"
columns: ["account_id"]
isOneToOne: false
referencedRelation: "user_accounts"
referencedColumns: ["id"]
},
{
foreignKeyName: "subscriptions_billing_customer_id_fkey"
columns: ["billing_customer_id"]
isOneToOne: false
referencedRelation: "billing_customers"
referencedColumns: ["id"]
},
]
}
}
Views: {
user_account_workspace: {
Row: {
id: string | null
name: string | null
picture_url: string | null
subscription_status:
| Database["public"]["Enums"]["subscription_status"]
| null
}
Relationships: []
}
user_accounts: {
Row: {
id: string | null
name: string | null
picture_url: string | null
role: Database["public"]["Enums"]["account_role"] | null
slug: string | null
}
Relationships: []
}
}
Functions: {
add_invitations_to_account:
| {
Args: {
account_slug: string
invitations: Database["public"]["CompositeTypes"]["invitation"][]
}
Returns: Database["public"]["Tables"]["invitations"]["Row"][]
}
| {
Args: {
account_slug: string
invitations: Database["public"]["CompositeTypes"]["invitation"][]
}
Returns: {
account_id: string
created_at: string
email: string
id: number
invite_token: string
invited_by: string
role: Database["public"]["Enums"]["account_role"]
updated_at: string
}
}
create_account: {
Args: {
account_name: string
}
Returns: {
created_at: string | null
created_by: string | null
email: string | null
id: string
is_personal_account: boolean
name: string
picture_url: string | null
primary_owner_user_id: string
slug: string | null
updated_at: string | null
updated_by: string | null
}
}
create_invitation: {
Args: {
account_id: string
email: string
role: Database["public"]["Enums"]["account_role"]
}
Returns: {
account_id: string
created_at: string
email: string
id: number
invite_token: string
invited_by: string
role: Database["public"]["Enums"]["account_role"]
updated_at: string
}
}
get_account_invitations: {
Args: {
account_slug: string
}
Returns: {
email: string
account_id: string
invited_by: string
role: Database["public"]["Enums"]["account_role"]
created_at: string
updated_at: string
inviter_name: string
inviter_email: string
}[]
}
get_account_members: {
Args: {
account_slug: string
}
Returns: {
id: string
user_id: string
account_id: string
role: Database["public"]["Enums"]["account_role"]
primary_owner_user_id: string
name: string
email: string
picture_url: string
created_at: string
updated_at: string
}[]
}
get_config: {
Args: Record<PropertyKey, never>
Returns: Json
}
get_user_accounts: {
Args: Record<PropertyKey, never>
Returns: {
created_at: string | null
created_by: string | null
email: string | null
id: string
is_personal_account: boolean
name: string
picture_url: string | null
primary_owner_user_id: string
slug: string | null
updated_at: string | null
updated_by: string | null
}[]
}
has_permission: {
Args: {
user_id: string
account_id: string
permission_name: Database["public"]["Enums"]["app_permissions"]
}
Returns: boolean
}
has_role_on_account: {
Args: {
account_id: string
account_role?: Database["public"]["Enums"]["account_role"]
}
Returns: boolean
}
is_account_owner: {
Args: {
account_id: string
}
Returns: boolean
}
is_set: {
Args: {
field_name: string
}
Returns: boolean
}
is_team_member: {
Args: {
account_id: string
user_id: string
}
Returns: boolean
}
organization_account_workspace: {
Args: {
account_slug: string
}
Returns: {
id: string
name: string
picture_url: string
slug: string
role: Database["public"]["Enums"]["account_role"]
primary_owner_user_id: string
subscription_status: Database["public"]["Enums"]["subscription_status"]
permissions: Database["public"]["Enums"]["app_permissions"][]
}[]
}
unaccent: {
Args: {
"": string
}
Returns: string
}
unaccent_init: {
Args: {
"": unknown
}
Returns: unknown
}
}
Enums: {
account_role: "owner" | "member"
app_permissions:
| "roles.manage"
| "billing.manage"
| "settings.manage"
| "members.manage"
| "invites.manage"
billing_provider: "stripe" | "lemon-squeezy" | "paddle"
subscription_status:
| "active"
| "trialing"
| "past_due"
| "canceled"
| "unpaid"
| "incomplete"
| "incomplete_expired"
| "paused"
}
CompositeTypes: {
invitation: {
email: string | null
role: Database["public"]["Enums"]["account_role"] | null
}
}
}
storage: {
Tables: {
buckets: {
Row: {
allowed_mime_types: string[] | null
avif_autodetection: boolean | null
created_at: string | null
file_size_limit: number | null
id: string
name: string
owner: string | null
owner_id: string | null
public: boolean | null
updated_at: string | null
}
Insert: {
allowed_mime_types?: string[] | null
avif_autodetection?: boolean | null
created_at?: string | null
file_size_limit?: number | null
id: string
name: string
owner?: string | null
owner_id?: string | null
public?: boolean | null
updated_at?: string | null
}
Update: {
allowed_mime_types?: string[] | null
avif_autodetection?: boolean | null
created_at?: string | null
file_size_limit?: number | null
id?: string
name?: string
owner?: string | null
owner_id?: string | null
public?: boolean | null
updated_at?: string | null
}
Relationships: []
}
migrations: {
Row: {
executed_at: string | null
hash: string
id: number
name: string
}
Insert: {
executed_at?: string | null
hash: string
id: number
name: string
}
Update: {
executed_at?: string | null
hash?: string
id?: number
name?: string
}
Relationships: []
}
objects: {
Row: {
bucket_id: string | null
created_at: string | null
id: string
last_accessed_at: string | null
metadata: Json | null
name: string | null
owner: string | null
owner_id: string | null
path_tokens: string[] | null
updated_at: string | null
version: string | null
}
Insert: {
bucket_id?: string | null
created_at?: string | null
id?: string
last_accessed_at?: string | null
metadata?: Json | null
name?: string | null
owner?: string | null
owner_id?: string | null
path_tokens?: string[] | null
updated_at?: string | null
version?: string | null
}
Update: {
bucket_id?: string | null
created_at?: string | null
id?: string
last_accessed_at?: string | null
metadata?: Json | null
name?: string | null
owner?: string | null
owner_id?: string | null
path_tokens?: string[] | null
updated_at?: string | null
version?: string | null
}
Relationships: [
{
foreignKeyName: "objects_bucketId_fkey"
columns: ["bucket_id"]
isOneToOne: false
referencedRelation: "buckets"
referencedColumns: ["id"]
},
]
}
}
Views: {
[_ in never]: never
}
Functions: {
can_insert_object: {
Args: {
bucketid: string
name: string
owner: string
metadata: Json
}
Returns: undefined
}
extension: {
Args: {
name: string
}
Returns: string
}
filename: {
Args: {
name: string
}
Returns: string
}
foldername: {
Args: {
name: string
}
Returns: string[]
}
get_size_by_bucket: {
Args: Record<PropertyKey, never>
Returns: {
size: number
bucket_id: string
}[]
}
search: {
Args: {
prefix: string
bucketname: string
limits?: number
levels?: number
offsets?: number
search?: string
sortcolumn?: string
sortorder?: string
}
Returns: {
name: string
id: string
updated_at: string
created_at: string
last_accessed_at: string
metadata: Json
}[]
}
}
Enums: {
[_ in never]: never
}
CompositeTypes: {
[_ in never]: never
}
}
}
type PublicSchema = Database[Extract<keyof Database, "public">]
export type Tables<
PublicTableNameOrOptions extends
| keyof (PublicSchema["Tables"] & PublicSchema["Views"])
| { schema: keyof Database },
TableName extends PublicTableNameOrOptions extends { schema: keyof Database }
? keyof (Database[PublicTableNameOrOptions["schema"]]["Tables"] &
Database[PublicTableNameOrOptions["schema"]]["Views"])
: never = never,
> = PublicTableNameOrOptions extends { schema: keyof Database }
? (Database[PublicTableNameOrOptions["schema"]]["Tables"] &
Database[PublicTableNameOrOptions["schema"]]["Views"])[TableName] extends {
Row: infer R
}
? R
: never
: PublicTableNameOrOptions extends keyof (PublicSchema["Tables"] &
PublicSchema["Views"])
? (PublicSchema["Tables"] &
PublicSchema["Views"])[PublicTableNameOrOptions] extends {
Row: infer R
}
? R
: never
: never
export type TablesInsert<
PublicTableNameOrOptions extends
| keyof PublicSchema["Tables"]
| { schema: keyof Database },
TableName extends PublicTableNameOrOptions extends { schema: keyof Database }
? keyof Database[PublicTableNameOrOptions["schema"]]["Tables"]
: never = never,
> = PublicTableNameOrOptions extends { schema: keyof Database }
? Database[PublicTableNameOrOptions["schema"]]["Tables"][TableName] extends {
Insert: infer I
}
? I
: never
: PublicTableNameOrOptions extends keyof PublicSchema["Tables"]
? PublicSchema["Tables"][PublicTableNameOrOptions] extends {
Insert: infer I
}
? I
: never
: never
export type TablesUpdate<
PublicTableNameOrOptions extends
| keyof PublicSchema["Tables"]
| { schema: keyof Database },
TableName extends PublicTableNameOrOptions extends { schema: keyof Database }
? keyof Database[PublicTableNameOrOptions["schema"]]["Tables"]
: never = never,
> = PublicTableNameOrOptions extends { schema: keyof Database }
? Database[PublicTableNameOrOptions["schema"]]["Tables"][TableName] extends {
Update: infer U
}
? U
: never
: PublicTableNameOrOptions extends keyof PublicSchema["Tables"]
? PublicSchema["Tables"][PublicTableNameOrOptions] extends {
Update: infer U
}
? U
: never
: never
export type Enums<
PublicEnumNameOrOptions extends
| keyof PublicSchema["Enums"]
| { schema: keyof Database },
EnumName extends PublicEnumNameOrOptions extends { schema: keyof Database }
? keyof Database[PublicEnumNameOrOptions["schema"]]["Enums"]
: never = never,
> = PublicEnumNameOrOptions extends { schema: keyof Database }
? Database[PublicEnumNameOrOptions["schema"]]["Enums"][EnumName]
: PublicEnumNameOrOptions extends keyof PublicSchema["Enums"]
? PublicSchema["Enums"][PublicEnumNameOrOptions]
: never

View File

@@ -0,0 +1,20 @@
import { invariant } from '@epic-web/invariant';
/**
* Returns and validates the Supabase client keys from the environment.
*/
export function getSupabaseClientKeys() {
const env = process.env;
invariant(env.NEXT_PUBLIC_SUPABASE_URL, `Supabase URL not provided`);
invariant(
env.NEXT_PUBLIC_SUPABASE_ANON_KEY,
`Supabase Anon Key not provided`,
);
return {
url: env.NEXT_PUBLIC_SUPABASE_URL,
anonKey: env.NEXT_PUBLIC_SUPABASE_ANON_KEY,
};
}

View File

@@ -0,0 +1,26 @@
import { useQuery } from '@tanstack/react-query';
import { useSupabase } from './use-supabase';
import { useFactorsMutationKey } from './use-user-factors-mutation-key';
function useFetchAuthFactors() {
const client = useSupabase();
const queryKey = useFactorsMutationKey();
const queryFn = async () => {
const { data, error } = await client.auth.mfa.listFactors();
if (error) {
throw error;
}
return data;
};
return useQuery({
queryKey,
queryFn,
});
}
export default useFetchAuthFactors;

View File

@@ -0,0 +1,40 @@
import { useMutation } from '@tanstack/react-query';
import { useSupabase } from './use-supabase';
interface Params {
email: string;
redirectTo: string;
}
/**
* @name useRequestResetPassword
* @description Requests a password reset for a user. This function will
* trigger a password reset email to be sent to the user's email address.
* After the user clicks the link in the email, they will be redirected to
* /password-reset where their password can be updated.
*/
export function useRequestResetPassword() {
const client = useSupabase();
const mutationKey = ['auth', 'reset-password'];
const mutationFn = async (params: Params) => {
const { error, data } = await client.auth.resetPasswordForEmail(
params.email,
{
redirectTo: params.redirectTo,
},
);
if (error) {
throw error;
}
return data;
};
return useMutation({
mutationFn,
mutationKey,
});
}

View File

@@ -0,0 +1,33 @@
import { useMutation } from '@tanstack/react-query';
import { useSupabase } from './use-supabase';
interface Credentials {
email: string;
password: string;
}
export function useSignInWithEmailPassword() {
const client = useSupabase();
const mutationKey = ['auth', 'sign-in-with-email-password'];
const mutationFn = async (credentials: Credentials) => {
const response = await client.auth.signInWithPassword(credentials);
if (response.error) {
throw response.error.message;
}
const user = response.data?.user;
const identities = user?.identities ?? [];
// if the user has no identities, it means that the email is taken
if (identities.length === 0) {
throw new Error('User already registered');
}
return response.data;
};
return useMutation({ mutationKey, mutationFn });
}

View File

@@ -0,0 +1,46 @@
import type {
AuthError,
SignInWithPasswordlessCredentials,
} from '@supabase/gotrue-js';
import { useMutation } from '@tanstack/react-query';
import { useSupabase } from './use-supabase';
export function useSignInWithOtp() {
const client = useSupabase();
const mutationKey = ['auth', 'sign-in-with-otp'];
const mutationFn = async (credentials: SignInWithPasswordlessCredentials) => {
const result = await client.auth.signInWithOtp(credentials);
if (result.error) {
if (shouldIgnoreError(result.error)) {
console.warn(
`Ignoring error during development: ${result.error.message}`,
);
return {} as never;
}
throw result.error.message;
}
return result.data;
};
return useMutation({
mutationFn,
mutationKey,
});
}
export default useSignInWithOtp;
function shouldIgnoreError(error: AuthError) {
return isSmsProviderNotSetupError(error);
}
function isSmsProviderNotSetupError(error: AuthError) {
return error.message.includes(`sms Provider could not be found`);
}

View File

@@ -0,0 +1,25 @@
import type { SignInWithOAuthCredentials } from '@supabase/gotrue-js';
import { useMutation } from '@tanstack/react-query';
import { useSupabase } from './use-supabase';
export function useSignInWithProvider() {
const client = useSupabase();
const mutationKey = ['auth', 'sign-in-with-provider'];
const mutationFn = async (credentials: SignInWithOAuthCredentials) => {
const response = await client.auth.signInWithOAuth(credentials);
if (response.error) {
throw response.error.message;
}
return response.data;
};
return useMutation({
mutationFn,
mutationKey,
});
}

View File

@@ -0,0 +1,16 @@
import { useMutation } from '@tanstack/react-query';
import { useSupabase } from './use-supabase';
import { useRevalidateUserSession } from './use-user-session';
export function useSignOut() {
const client = useSupabase();
const revalidateUserSession = useRevalidateUserSession();
return useMutation({
mutationFn: async () => {
await client.auth.signOut();
await revalidateUserSession();
},
});
}

View File

@@ -0,0 +1,44 @@
import { useMutation } from '@tanstack/react-query';
import { useSupabase } from './use-supabase';
interface Credentials {
email: string;
password: string;
emailRedirectTo: string;
}
export function useSignUpWithEmailAndPassword() {
const client = useSupabase();
const mutationKey = ['auth', 'sign-up-with-email-password'];
const mutationFn = async (params: Credentials) => {
const { emailRedirectTo, ...credentials } = params;
const response = await client.auth.signUp({
...credentials,
options: {
emailRedirectTo,
},
});
if (response.error) {
throw response.error.message;
}
const user = response.data?.user;
const identities = user?.identities ?? [];
// if the user has no identities, it means that the email is taken
if (identities.length === 0) {
throw new Error('User already registered');
}
return response.data;
};
return useMutation({
mutationKey,
mutationFn,
});
}

View File

@@ -0,0 +1,8 @@
import { useMemo } from 'react';
import { getSupabaseBrowserClient } from '../clients/browser.client';
import { Database } from '../database.types';
export function useSupabase<Schema = Database>() {
return useMemo(() => getSupabaseBrowserClient<Schema>(), []);
}

View File

@@ -0,0 +1,31 @@
import type { UserAttributes } from '@supabase/gotrue-js';
import { useMutation } from '@tanstack/react-query';
import { useSupabase } from './use-supabase';
type Params = UserAttributes & { redirectTo: string };
export function useUpdateUser() {
const client = useSupabase();
const mutationKey = ['auth', 'update-user'];
const mutationFn = async (attributes: Params) => {
const { redirectTo, ...params } = attributes;
const response = await client.auth.updateUser(params, {
emailRedirectTo: redirectTo,
});
if (response.error) {
throw response.error;
}
return response.data;
};
return useMutation({
mutationKey,
mutationFn,
});
}

View File

@@ -0,0 +1,8 @@
import { useUserSession } from './use-user-session';
export function useFactorsMutationKey() {
const user = useUserSession();
const userId = user?.data?.user.id;
return ['mfa-factors', userId];
}

View File

@@ -0,0 +1,34 @@
import { useCallback } from 'react';
import { useQuery, useQueryClient } from '@tanstack/react-query';
import { useSupabase } from './use-supabase';
const queryKey = ['supabase:session'];
export function useUserSession() {
const supabase = useSupabase();
const queryFn = async () => {
const { data, error } = await supabase.auth.getSession();
console.log(data, error);
if (error) {
throw error;
}
return data.session;
};
return useQuery({ queryKey, queryFn });
}
export function useRevalidateUserSession() {
const client = useQueryClient();
return useCallback(
() =>
client.invalidateQueries({
queryKey,
}),
[client],
);
}

View File

@@ -0,0 +1,27 @@
import { useQuery } from '@tanstack/react-query';
import { useSupabase } from './use-supabase';
export function useUser() {
const client = useSupabase();
const queryKey = ['user'];
const queryFn = async () => {
const response = await client.auth.getUser();
if (response.error) {
return Promise.reject(response.error);
}
if (response.data?.user) {
return response.data.user;
}
return Promise.reject('Unexpected result format');
};
return useQuery({
queryFn,
queryKey,
});
}

View File

@@ -0,0 +1,26 @@
import type { VerifyOtpParams } from '@supabase/gotrue-js';
import { useMutation } from '@tanstack/react-query';
import { useSupabase } from './use-supabase';
export function useVerifyOtp() {
const client = useSupabase();
const mutationKey = ['verify-otp'];
const mutationFn = async (params: VerifyOtpParams) => {
const { data, error } = await client.auth.verifyOtp(params);
if (error) {
throw error;
}
return data;
};
return useMutation({
mutationFn,
mutationKey,
});
}

View File

@@ -0,0 +1,94 @@
import type { Session, SupabaseClient } from '@supabase/supabase-js';
import { z } from 'zod';
import { checkRequiresMultiFactorAuthentication } from './check-requires-mfa';
const MULTI_FACTOR_AUTH_VERIFY_PATH = z
.string()
.default('/auth/verify')
.parse(process.env.MULTI_FACTOR_AUTH_VERIFY_PATH);
const SIGN_IN_PATH = z
.string()
.default('/auth/sign-in')
.parse(process.env.SIGN_IN_PATH);
/**
* @name requireAuth
* @description Require a session to be present in the request
* @param client
* @param verifyFromServer
*/
export async function requireAuth(
client: SupabaseClient,
verifyFromServer = true,
): Promise<
| {
error: null;
data: Session;
}
| (
| {
error: AuthenticationError;
data: null;
redirectTo: string;
}
| {
error: MultiFactorAuthError;
data: null;
redirectTo: string;
}
)
> {
const { data, error } = await client.auth.getSession();
if (!data.session || error) {
return {
data: null,
error: new AuthenticationError(),
redirectTo: SIGN_IN_PATH,
};
}
const requiresMfa = await checkRequiresMultiFactorAuthentication(client);
// If the user requires multi-factor authentication,
// redirect them to the page where they can verify their identity.
if (requiresMfa) {
return {
data: null,
error: new MultiFactorAuthError(),
redirectTo: MULTI_FACTOR_AUTH_VERIFY_PATH,
};
}
if (verifyFromServer) {
const { data: user, error } = await client.auth.getUser();
if (!user || error) {
return {
data: null,
error: new AuthenticationError(),
redirectTo: SIGN_IN_PATH,
};
}
}
return {
error: null,
data: data.session,
};
}
class AuthenticationError extends Error {
constructor() {
super(`Authentication required`);
}
}
class MultiFactorAuthError extends Error {
constructor() {
super(`Multi-factor authentication required`);
}
}