B2B-88: add starter kit structure and elements

This commit is contained in:
devmc-ee
2025-06-08 16:18:30 +03:00
parent 657a36a298
commit e7b25600cb
1280 changed files with 77893 additions and 5688 deletions

View File

@@ -0,0 +1,14 @@
# Supabase - @kit/supabase
This package is responsible for managing the Supabase client and various utilities related to Supabase.
Make sure the app installs the `@kit/supabase` package before using it.
```json
{
"name": "my-app",
"dependencies": {
"@kit/supabase": "*"
}
}
```

View File

@@ -0,0 +1,3 @@
import eslintConfigBase from '@kit/eslint-config/base.js';
export default eslintConfigBase;

17
packages/supabase/node_modules/.bin/next generated vendored Executable file
View File

@@ -0,0 +1,17 @@
#!/bin/sh
basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
case `uname` in
*CYGWIN*) basedir=`cygpath -w "$basedir"`;;
esac
if [ -z "$NODE_PATH" ]; then
export NODE_PATH="/Users/devmcee/dev/mountbirch/MRB2B/node_modules/.pnpm/next@15.3.2_@babel+core@7.27.4_@opentelemetry+api@1.9.0_babel-plugin-react-compiler@19.1.0-rc_krzs4il3c2axvegn27goximifi/node_modules/next/dist/bin/node_modules:/Users/devmcee/dev/mountbirch/MRB2B/node_modules/.pnpm/next@15.3.2_@babel+core@7.27.4_@opentelemetry+api@1.9.0_babel-plugin-react-compiler@19.1.0-rc_krzs4il3c2axvegn27goximifi/node_modules/next/dist/node_modules:/Users/devmcee/dev/mountbirch/MRB2B/node_modules/.pnpm/next@15.3.2_@babel+core@7.27.4_@opentelemetry+api@1.9.0_babel-plugin-react-compiler@19.1.0-rc_krzs4il3c2axvegn27goximifi/node_modules/next/node_modules:/Users/devmcee/dev/mountbirch/MRB2B/node_modules/.pnpm/next@15.3.2_@babel+core@7.27.4_@opentelemetry+api@1.9.0_babel-plugin-react-compiler@19.1.0-rc_krzs4il3c2axvegn27goximifi/node_modules:/Users/devmcee/dev/mountbirch/MRB2B/node_modules/.pnpm/node_modules"
else
export NODE_PATH="/Users/devmcee/dev/mountbirch/MRB2B/node_modules/.pnpm/next@15.3.2_@babel+core@7.27.4_@opentelemetry+api@1.9.0_babel-plugin-react-compiler@19.1.0-rc_krzs4il3c2axvegn27goximifi/node_modules/next/dist/bin/node_modules:/Users/devmcee/dev/mountbirch/MRB2B/node_modules/.pnpm/next@15.3.2_@babel+core@7.27.4_@opentelemetry+api@1.9.0_babel-plugin-react-compiler@19.1.0-rc_krzs4il3c2axvegn27goximifi/node_modules/next/dist/node_modules:/Users/devmcee/dev/mountbirch/MRB2B/node_modules/.pnpm/next@15.3.2_@babel+core@7.27.4_@opentelemetry+api@1.9.0_babel-plugin-react-compiler@19.1.0-rc_krzs4il3c2axvegn27goximifi/node_modules/next/node_modules:/Users/devmcee/dev/mountbirch/MRB2B/node_modules/.pnpm/next@15.3.2_@babel+core@7.27.4_@opentelemetry+api@1.9.0_babel-plugin-react-compiler@19.1.0-rc_krzs4il3c2axvegn27goximifi/node_modules:/Users/devmcee/dev/mountbirch/MRB2B/node_modules/.pnpm/node_modules:$NODE_PATH"
fi
if [ -x "$basedir/node" ]; then
exec "$basedir/node" "$basedir/../next/dist/bin/next" "$@"
else
exec node "$basedir/../next/dist/bin/next" "$@"
fi

1
packages/supabase/node_modules/@kit/eslint-config generated vendored Symbolic link
View File

@@ -0,0 +1 @@
../../../../tooling/eslint

1
packages/supabase/node_modules/@kit/prettier-config generated vendored Symbolic link
View File

@@ -0,0 +1 @@
../../../../tooling/prettier

1
packages/supabase/node_modules/@kit/tsconfig generated vendored Symbolic link
View File

@@ -0,0 +1 @@
../../../../tooling/typescript

1
packages/supabase/node_modules/@supabase/ssr generated vendored Symbolic link
View File

@@ -0,0 +1 @@
../../../../node_modules/.pnpm/@supabase+ssr@0.6.1_@supabase+supabase-js@2.49.4/node_modules/@supabase/ssr

1
packages/supabase/node_modules/@supabase/supabase-js generated vendored Symbolic link
View File

@@ -0,0 +1 @@
../../../../node_modules/.pnpm/@supabase+supabase-js@2.49.4/node_modules/@supabase/supabase-js

1
packages/supabase/node_modules/@tanstack/react-query generated vendored Symbolic link
View File

@@ -0,0 +1 @@
../../../../node_modules/.pnpm/@tanstack+react-query@5.76.1_react@19.1.0/node_modules/@tanstack/react-query

1
packages/supabase/node_modules/@types/react generated vendored Symbolic link
View File

@@ -0,0 +1 @@
../../../../node_modules/.pnpm/@types+react@19.1.4/node_modules/@types/react

1
packages/supabase/node_modules/next generated vendored Symbolic link
View File

@@ -0,0 +1 @@
../../../node_modules/.pnpm/next@15.3.2_@babel+core@7.27.4_@opentelemetry+api@1.9.0_babel-plugin-react-compiler@19.1.0-rc_krzs4il3c2axvegn27goximifi/node_modules/next

1
packages/supabase/node_modules/react generated vendored Symbolic link
View File

@@ -0,0 +1 @@
../../../node_modules/.pnpm/react@19.1.0/node_modules/react

1
packages/supabase/node_modules/server-only generated vendored Symbolic link
View File

@@ -0,0 +1 @@
../../../node_modules/.pnpm/server-only@0.0.1/node_modules/server-only

1
packages/supabase/node_modules/zod generated vendored Symbolic link
View File

@@ -0,0 +1 @@
../../../node_modules/.pnpm/zod@3.25.56/node_modules/zod

View File

@@ -0,0 +1,43 @@
{
"name": "@kit/supabase",
"private": true,
"version": "0.1.0",
"scripts": {
"clean": "git clean -xdf .turbo node_modules",
"format": "prettier --check \"**/*.{ts,tsx}\"",
"lint": "eslint .",
"typecheck": "tsc --noEmit"
},
"prettier": "@kit/prettier-config",
"exports": {
"./server-client": "./src/clients/server-client.ts",
"./server-admin-client": "./src/clients/server-admin-client.ts",
"./middleware-client": "./src/clients/middleware-client.ts",
"./browser-client": "./src/clients/browser-client.ts",
"./check-requires-mfa": "./src/check-requires-mfa.ts",
"./require-user": "./src/require-user.ts",
"./hooks/*": "./src/hooks/*.ts",
"./database": "./src/database.types.ts",
"./auth": "./src/auth.ts"
},
"devDependencies": {
"@kit/eslint-config": "workspace:*",
"@kit/prettier-config": "workspace:*",
"@kit/tsconfig": "workspace:*",
"@supabase/ssr": "^0.6.1",
"@supabase/supabase-js": "2.49.4",
"@tanstack/react-query": "5.76.1",
"@types/react": "19.1.4",
"next": "15.3.2",
"react": "19.1.0",
"server-only": "^0.0.1",
"zod": "^3.24.4"
},
"typesVersions": {
"*": {
"*": [
"src/*"
]
}
}
}

View File

@@ -0,0 +1,292 @@
import 'server-only';
import {
AuthError,
type EmailOtpType,
SupabaseClient,
} from '@supabase/supabase-js';
/**
* @name createAuthCallbackService
* @description Creates an instance of the AuthCallbackService
* @param client
*/
export function createAuthCallbackService(client: SupabaseClient) {
return new AuthCallbackService(client);
}
/**
* @name AuthCallbackService
* @description Service for handling auth callbacks in Supabase
*/
class AuthCallbackService {
constructor(private readonly client: SupabaseClient) {}
/**
* @name verifyTokenHash
* @description Verifies the token hash and type and redirects the user to the next page
* This should be used when using a token hash to verify the user's email
* @param request
* @param params
*/
async verifyTokenHash(
request: Request,
params: {
joinTeamPath: string;
redirectPath: string;
errorPath?: string;
},
): Promise<URL> {
const url = new URL(request.url);
const searchParams = url.searchParams;
const host = request.headers.get('host');
// set the host to the request host since outside of Vercel it gets set as "localhost" or "0.0.0.0"
this.adjustUrlHostForLocalDevelopment(url, host);
url.pathname = params.redirectPath;
const token_hash = searchParams.get('token_hash');
const type = searchParams.get('type') as EmailOtpType | null;
const callbackParam =
searchParams.get('next') ?? searchParams.get('callback');
let nextPath: string | null = null;
const callbackUrl = callbackParam ? new URL(callbackParam) : null;
// if we have a callback url, we check if it has a next path
if (callbackUrl) {
// if we have a callback url, we check if it has a next path
const callbackNextPath = callbackUrl.searchParams.get('next');
// if we have a next path in the callback url, we use that
if (callbackNextPath) {
nextPath = callbackNextPath;
} else {
nextPath = callbackUrl.pathname;
}
}
const inviteToken = callbackUrl?.searchParams.get('invite_token');
const errorPath = params.errorPath ?? '/auth/callback/error';
// remove the query params from the url
searchParams.delete('token_hash');
searchParams.delete('type');
searchParams.delete('next');
// if we have a next path, we redirect to that path
if (nextPath) {
url.pathname = nextPath;
}
// if we have an invite token, we append it to the redirect url
if (inviteToken) {
// if we have an invite token, we redirect to the join team page
// instead of the default next url. This is because the user is trying
// to join a team and we want to make sure they are redirected to the
// correct page.
url.pathname = params.joinTeamPath;
searchParams.set('invite_token', inviteToken);
const emailParam = callbackUrl?.searchParams.get('email');
if (emailParam) {
searchParams.set('email', emailParam);
}
}
if (token_hash && type) {
const { error } = await this.client.auth.verifyOtp({
type,
token_hash,
});
if (!error) {
return url;
}
if (error.code) {
url.searchParams.set('code', error.code);
}
const errorMessage = getAuthErrorMessage({
error: error.message,
code: error.code,
});
url.searchParams.set('error', errorMessage);
}
// return the user to an error page with some instructions
url.pathname = errorPath;
return url;
}
/**
* @name exchangeCodeForSession
* @description Exchanges the auth code for a session and redirects the user to the next page or an error page
* @param request
* @param params
*/
async exchangeCodeForSession(
request: Request,
params: {
joinTeamPath: string;
redirectPath: string;
errorPath?: string;
},
): Promise<{
nextPath: string;
}> {
const requestUrl = new URL(request.url);
const searchParams = requestUrl.searchParams;
const authCode = searchParams.get('code');
const error = searchParams.get('error');
const nextUrlPathFromParams = searchParams.get('next');
const inviteToken = searchParams.get('invite_token');
const errorPath = params.errorPath ?? '/auth/callback/error';
let nextUrl = nextUrlPathFromParams ?? params.redirectPath;
// if we have an invite token, we redirect to the join team page
// instead of the default next url. This is because the user is trying
// to join a team and we want to make sure they are redirected to the
// correct page.
if (inviteToken) {
const emailParam = searchParams.get('email');
const urlParams = new URLSearchParams({
invite_token: inviteToken,
email: emailParam ?? '',
});
nextUrl = `${params.joinTeamPath}?${urlParams.toString()}`;
}
if (authCode) {
try {
const { error } =
await this.client.auth.exchangeCodeForSession(authCode);
// if we have an error, we redirect to the error page
if (error) {
return onError({
code: error.code,
error: error.message,
path: errorPath,
});
}
} catch (error) {
console.error(
{
error,
name: `auth.callback`,
},
`An error occurred while exchanging code for session`,
);
const message = error instanceof Error ? error.message : error;
return onError({
code: (error as AuthError)?.code,
error: message as string,
path: errorPath,
});
}
}
if (error) {
return onError({
error,
path: errorPath,
});
}
return {
nextPath: nextUrl,
};
}
private adjustUrlHostForLocalDevelopment(url: URL, host: string | null) {
if (this.isLocalhost(url.host) && !this.isLocalhost(host)) {
url.host = host as string;
url.port = '';
}
}
private isLocalhost(host: string | null) {
if (!host) {
return false;
}
return (
host.includes('localhost:') ||
host.includes('0.0.0.0:') ||
host.includes('127.0.0.1:')
);
}
}
function onError({
error,
path,
code,
}: {
error: string;
path: string;
code?: string;
}) {
const errorMessage = getAuthErrorMessage({ error, code });
console.error(
{
error,
name: `auth.callback`,
},
`An error occurred while signing user in`,
);
const searchParams = new URLSearchParams({
error: errorMessage,
code: code ?? '',
});
const nextPath = `${path}?${searchParams.toString()}`;
return {
nextPath,
};
}
/**
* Checks if the given error message indicates a verifier error.
* We check for this specific error because it's highly likely that the
* user is trying to sign in using a different browser than the one they
* used to request the sign in link. This is a common mistake, so we
* want to provide a helpful error message.
*/
function isVerifierError(error: string) {
return error.includes('both auth code and code verifier should be non-empty');
}
function getAuthErrorMessage(params: { error: string; code?: string }) {
// this error arises when the user tries to sign in with an expired email link
if (params.code) {
if (params.code === 'otp_expired') {
return 'auth:errors.otp_expired';
}
}
// this error arises when the user is trying to sign in with a different
// browser than the one they used to request the sign in link
if (isVerifierError(params.error)) {
return 'auth:errors.codeVerifierMismatch';
}
// fallback to the default error message
return `auth:authenticationErrorAlertBody`;
}

View File

@@ -0,0 +1 @@
export * from './auth-callback.service';

View File

@@ -0,0 +1,31 @@
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,
) {
// Suppress the getSession warning. Remove when the issue is fixed.
// https://github.com/supabase/auth-js/issues/873
// @ts-expect-error: suppressGetSessionWarning is not part of the public API
client.auth.suppressGetSessionWarning = true;
const assuranceLevel = await client.auth.mfa.getAuthenticatorAssuranceLevel();
// @ts-expect-error: suppressGetSessionWarning is not part of the public API
client.auth.suppressGetSessionWarning = false;
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,14 @@
import { createBrowserClient } from '@supabase/ssr';
import { Database } from '../database.types';
import { getSupabaseClientKeys } from '../get-supabase-client-keys';
/**
* @name getSupabaseBrowserClient
* @description Get a Supabase client for use in the Browser
*/
export function getSupabaseBrowserClient<GenericSchema = Database>() {
const keys = getSupabaseClientKeys();
return createBrowserClient<GenericSchema>(keys.url, keys.anonKey);
}

View File

@@ -0,0 +1,38 @@
import 'server-only';
import { type NextRequest, NextResponse } from 'next/server';
import { 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: {
getAll() {
return request.cookies.getAll();
},
setAll(cookiesToSet) {
cookiesToSet.forEach(({ name, value }) =>
request.cookies.set(name, value),
);
cookiesToSet.forEach(({ name, value, options }) =>
response.cookies.set(name, value, options),
);
},
},
});
}

View File

@@ -0,0 +1,28 @@
import 'server-only';
import { createClient } from '@supabase/supabase-js';
import { Database } from '../database.types';
import {
getServiceRoleKey,
warnServiceRoleKeyUsage,
} from '../get-service-role-key';
import { getSupabaseClientKeys } from '../get-supabase-client-keys';
/**
* @name getSupabaseServerAdminClient
* @description Get a Supabase client for use in the Server with admin access to the database.
*/
export function getSupabaseServerAdminClient<GenericSchema = Database>() {
warnServiceRoleKeyUsage();
const url = getSupabaseClientKeys().url;
return createClient<GenericSchema>(url, getServiceRoleKey(), {
auth: {
persistSession: false,
detectSessionInUrl: false,
autoRefreshToken: false,
},
});
}

View File

@@ -0,0 +1,39 @@
import 'server-only';
import { cookies } from 'next/headers';
import { createServerClient } from '@supabase/ssr';
import { Database } from '../database.types';
import { getSupabaseClientKeys } from '../get-supabase-client-keys';
/**
* @name getSupabaseServerClient
* @description Creates a Supabase client for use in the Server.
*/
export function getSupabaseServerClient<GenericSchema = Database>() {
const keys = getSupabaseClientKeys();
return createServerClient<GenericSchema>(keys.url, keys.anonKey, {
cookies: {
async getAll() {
const cookieStore = await cookies();
return cookieStore.getAll();
},
async setAll(cookiesToSet) {
const cookieStore = await cookies();
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.
}
},
},
});
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,33 @@
import 'server-only';
import { z } from 'zod';
const message =
'Invalid Supabase Service Role Key. Please add the environment variable SUPABASE_SERVICE_ROLE_KEY.';
/**
* @name getServiceRoleKey
* @description Get the Supabase Service Role Key.
* ONLY USE IN SERVER-SIDE CODE. DO NOT EXPOSE THIS TO CLIENT-SIDE CODE.
*/
export function getServiceRoleKey() {
return z
.string({
required_error: message,
})
.min(1, {
message: message,
})
.parse(process.env.SUPABASE_SERVICE_ROLE_KEY);
}
/**
* Displays a warning message if the Supabase Service Role is being used.
*/
export function warnServiceRoleKeyUsage() {
if (process.env.NODE_ENV !== 'production') {
console.warn(
`[Dev Only] This is a simple warning to let you know you are using the Supabase Service Role. Make sure it's the right call.`,
);
}
}

View File

@@ -0,0 +1,24 @@
import { z } from 'zod';
/**
* Returns and validates the Supabase client keys from the environment.
*/
export function getSupabaseClientKeys() {
return z
.object({
url: z.string({
description: `This is the URL of your hosted Supabase instance. Please provide the variable NEXT_PUBLIC_SUPABASE_URL.`,
required_error: `Please provide the variable NEXT_PUBLIC_SUPABASE_URL`,
}),
anonKey: z
.string({
description: `This is the anon key provided by Supabase. It is a public key used client-side. Please provide the variable NEXT_PUBLIC_SUPABASE_ANON_KEY.`,
required_error: `Please provide the variable NEXT_PUBLIC_SUPABASE_ANON_KEY`,
})
.min(1),
})
.parse({
url: process.env.NEXT_PUBLIC_SUPABASE_URL,
anonKey: process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY,
});
}

View File

@@ -0,0 +1,82 @@
'use client';
import { useEffect } from 'react';
import { usePathname } from 'next/navigation';
import type { AuthChangeEvent, Session } from '@supabase/supabase-js';
import { useSupabase } from './use-supabase';
/**
* @name PRIVATE_PATH_PREFIXES
* @description A list of private path prefixes
*/
const PRIVATE_PATH_PREFIXES = ['/home', '/admin', '/join', '/update-password'];
/**
* @name AUTH_PATHS
* @description A list of auth paths
*/
const AUTH_PATHS = ['/auth'];
/**
* @name useAuthChangeListener
* @param privatePathPrefixes - A list of private path prefixes
* @param appHomePath - The path to redirect to when the user is signed out
* @param onEvent - Callback function to be called when an auth event occurs
*/
export function useAuthChangeListener({
privatePathPrefixes = PRIVATE_PATH_PREFIXES,
appHomePath,
onEvent,
}: {
appHomePath: string;
privatePathPrefixes?: string[];
onEvent?: (event: AuthChangeEvent, user: Session | null) => void;
}) {
const client = useSupabase();
const pathName = usePathname();
useEffect(() => {
// keep this running for the whole session unless the component was unmounted
const listener = client.auth.onAuthStateChange((event, user) => {
if (onEvent) {
onEvent(event, user);
}
// log user out if user is falsy
// and if the current path is a private route
const shouldRedirectUser =
!user && isPrivateRoute(pathName, privatePathPrefixes);
if (shouldRedirectUser) {
// send user away when signed out
window.location.assign('/');
return;
}
// revalidate user session when user signs in or out
if (event === 'SIGNED_OUT') {
// sometimes Supabase sends SIGNED_OUT event
// but in the auth path, so we ignore it
if (AUTH_PATHS.some((path) => pathName.startsWith(path))) {
return;
}
window.location.reload();
}
});
// destroy listener on un-mounts
return () => listener.data.subscription.unsubscribe();
}, [client.auth, pathName, appHomePath, privatePathPrefixes, onEvent]);
}
/**
* Determines if a given path is a private route.
*/
function isPrivateRoute(path: string, privatePathPrefixes: string[]) {
return privatePathPrefixes.some((prefix) => path.startsWith(prefix));
}

View File

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

View File

@@ -0,0 +1,42 @@
import { useMutation } from '@tanstack/react-query';
import { useSupabase } from './use-supabase';
interface RequestPasswordResetMutationParams {
email: string;
redirectTo: string;
captchaToken?: 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: RequestPasswordResetMutationParams) => {
const { error, data } = await client.auth.resetPasswordForEmail(
params.email,
{
redirectTo: params.redirectTo,
captchaToken: params.captchaToken,
},
);
if (error) {
throw error;
}
return data;
};
return useMutation({
mutationFn,
mutationKey,
});
}

View File

@@ -0,0 +1,30 @@
import type { SignInWithPasswordCredentials } from '@supabase/supabase-js';
import { useMutation } from '@tanstack/react-query';
import { useSupabase } from './use-supabase';
export function useSignInWithEmailPassword() {
const client = useSupabase();
const mutationKey = ['auth', 'sign-in-with-email-password'];
const mutationFn = async (credentials: SignInWithPasswordCredentials) => {
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,43 @@
import type { SignInWithPasswordlessCredentials } from '@supabase/supabase-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.message)) {
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: string) {
return isSmsProviderNotSetupError(error);
}
function isSmsProviderNotSetupError(error: string) {
return error.includes(`sms Provider could not be found`);
}

View File

@@ -0,0 +1,25 @@
import type { SignInWithOAuthCredentials } from '@supabase/supabase-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,13 @@
import { useMutation } from '@tanstack/react-query';
import { useSupabase } from './use-supabase';
export function useSignOut() {
const client = useSupabase();
return useMutation({
mutationFn: () => {
return client.auth.signOut();
},
});
}

View File

@@ -0,0 +1,46 @@
import { useMutation } from '@tanstack/react-query';
import { useSupabase } from './use-supabase';
interface Credentials {
email: string;
password: string;
emailRedirectTo: string;
captchaToken?: string;
}
export function useSignUpWithEmailAndPassword() {
const client = useSupabase();
const mutationKey = ['auth', 'sign-up-with-email-password'];
const mutationFn = async (params: Credentials) => {
const { emailRedirectTo, captchaToken, ...credentials } = params;
const response = await client.auth.signUp({
...credentials,
options: {
emailRedirectTo,
captchaToken,
},
});
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<Db = Database>() {
return useMemo(() => getSupabaseBrowserClient<Db>(), []);
}

View File

@@ -0,0 +1,31 @@
import type { UserAttributes } from '@supabase/supabase-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 = ['supabase: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,3 @@
export function useFactorsMutationKey(userId: string) {
return ['mfa-factors', userId];
}

View File

@@ -0,0 +1,35 @@
import type { User } from '@supabase/supabase-js';
import { useQuery } from '@tanstack/react-query';
import { useSupabase } from './use-supabase';
const queryKey = ['supabase:user'];
export function useUser(initialData?: User | null) {
const client = useSupabase();
const queryFn = async () => {
const response = await client.auth.getUser();
// this is most likely a session error or the user is not logged in
if (response.error) {
return undefined;
}
if (response.data?.user) {
return response.data.user;
}
return Promise.reject(new Error('Unexpected result format'));
};
return useQuery({
queryFn,
queryKey,
initialData,
refetchInterval: false,
refetchOnMount: false,
refetchOnWindowFocus: false,
});
}

View File

@@ -0,0 +1,26 @@
import type { VerifyOtpParams } from '@supabase/supabase-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,69 @@
import type { SupabaseClient, User } from '@supabase/supabase-js';
import { checkRequiresMultiFactorAuthentication } from './check-requires-mfa';
const MULTI_FACTOR_AUTH_VERIFY_PATH = '/auth/verify';
const SIGN_IN_PATH = '/auth/sign-in';
/**
* @name requireUser
* @description Require a session to be present in the request
* @param client
*/
export async function requireUser(client: SupabaseClient): Promise<
| {
error: null;
data: User;
}
| (
| {
error: AuthenticationError;
data: null;
redirectTo: string;
}
| {
error: MultiFactorAuthError;
data: null;
redirectTo: string;
}
)
> {
const { data, error } = await client.auth.getUser();
if (!data.user || 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,
};
}
return {
error: null,
data: data.user,
};
}
class AuthenticationError extends Error {
constructor() {
super(`Authentication required`);
}
}
class MultiFactorAuthError extends Error {
constructor() {
super(`Multi-factor authentication required`);
}
}

View File

@@ -0,0 +1,8 @@
{
"extends": "@kit/tsconfig/base.json",
"compilerOptions": {
"tsBuildInfoFile": "node_modules/.cache/tsbuildinfo.json"
},
"include": ["*.ts", "src"],
"exclude": ["node_modules"]
}