Refactor authentication flow and improve code organization
The update implemented a redirect functionality in the multi-factor authentication flow for a better user experience. It also involved a refactoring of some parts of the code, substituting direct routing paths with path configs for easier future modifications. Import statements were adjusted for better code organization and readability.
This commit is contained in:
@@ -206,7 +206,9 @@ function generateDemoData() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return [data, data[data.length - 1].value] as [typeof data, string];
|
const lastValue = data[data.length - 1]?.value;
|
||||||
|
|
||||||
|
return [data, lastValue] as [typeof data, string];
|
||||||
}
|
}
|
||||||
|
|
||||||
function Chart(
|
function Chart(
|
||||||
|
|||||||
@@ -26,7 +26,6 @@ export function TeamAccountCheckoutForm(params: { accountId: string }) {
|
|||||||
if (checkoutToken) {
|
if (checkoutToken) {
|
||||||
return (
|
return (
|
||||||
<EmbeddedCheckout
|
<EmbeddedCheckout
|
||||||
load
|
|
||||||
checkoutToken={checkoutToken}
|
checkoutToken={checkoutToken}
|
||||||
provider={billingConfig.provider}
|
provider={billingConfig.provider}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import Post from '~/(marketing)/blog/_components/post';
|
|||||||
import appConfig from '~/config/app.config';
|
import appConfig from '~/config/app.config';
|
||||||
import { withI18n } from '~/lib/i18n/with-i18n';
|
import { withI18n } from '~/lib/i18n/with-i18n';
|
||||||
|
|
||||||
export function generateMetadata({
|
export async function generateMetadata({
|
||||||
params,
|
params,
|
||||||
}: {
|
}: {
|
||||||
params: { slug: string };
|
params: { slug: string };
|
||||||
@@ -17,7 +17,7 @@ export function generateMetadata({
|
|||||||
const post = allPosts.find((post) => post.slug === params.slug);
|
const post = allPosts.find((post) => post.slug === params.slug);
|
||||||
|
|
||||||
if (!post) {
|
if (!post) {
|
||||||
return;
|
notFound();
|
||||||
}
|
}
|
||||||
|
|
||||||
const { title, date, description, image, slug } = post;
|
const { title, date, description, image, slug } = post;
|
||||||
|
|||||||
@@ -5,8 +5,8 @@ import type { DocumentationPage } from 'contentlayer/generated';
|
|||||||
export interface ProcessedDocumentationPage extends DocumentationPage {
|
export interface ProcessedDocumentationPage extends DocumentationPage {
|
||||||
collapsible: boolean;
|
collapsible: boolean;
|
||||||
pathSegments: string[];
|
pathSegments: string[];
|
||||||
nextPage: ProcessedDocumentationPage | DocumentationPage | null;
|
nextPage: ProcessedDocumentationPage | DocumentationPage | undefined;
|
||||||
previousPage: ProcessedDocumentationPage | DocumentationPage | null;
|
previousPage: ProcessedDocumentationPage | DocumentationPage | undefined;
|
||||||
children: DocsTree;
|
children: DocsTree;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { PricingTable } from '@kit/billing/components/pricing-table';
|
import { PricingTable } from '@kit/billing-gateway/components';
|
||||||
|
|
||||||
import { SitePageHeader } from '~/(marketing)/_components/site-page-header';
|
import { SitePageHeader } from '~/(marketing)/_components/site-page-header';
|
||||||
import billingConfig from '~/config/billing.config';
|
import billingConfig from '~/config/billing.config';
|
||||||
|
|||||||
@@ -6,30 +6,37 @@ import { getSupabaseRouteHandlerClient } from '@kit/supabase/route-handler-clien
|
|||||||
|
|
||||||
import pathsConfig from '~/config/paths.config';
|
import pathsConfig from '~/config/paths.config';
|
||||||
|
|
||||||
|
const defaultNextUrl = pathsConfig.app.home;
|
||||||
|
|
||||||
export async function GET(request: NextRequest) {
|
export async function GET(request: NextRequest) {
|
||||||
const requestUrl = new URL(request.url);
|
const requestUrl = new URL(request.url);
|
||||||
const searchParams = requestUrl.searchParams;
|
const searchParams = requestUrl.searchParams;
|
||||||
|
|
||||||
const authCode = searchParams.get('code');
|
const authCode = searchParams.get('code');
|
||||||
const inviteCode = searchParams.get('inviteCode');
|
|
||||||
const error = searchParams.get('error');
|
const error = searchParams.get('error');
|
||||||
const nextUrl = searchParams.get('next') ?? pathsConfig.app.home;
|
const nextUrlPathFromParams = searchParams.get('next');
|
||||||
|
const inviteToken = searchParams.get('invite_token');
|
||||||
|
|
||||||
let userId: string | undefined = undefined;
|
let nextUrl = nextUrlPathFromParams ?? defaultNextUrl;
|
||||||
|
|
||||||
|
// 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) {
|
||||||
|
nextUrl = `${pathsConfig.app.joinTeam}?invite_token=${inviteToken}`;
|
||||||
|
}
|
||||||
|
|
||||||
if (authCode) {
|
if (authCode) {
|
||||||
const client = getSupabaseRouteHandlerClient();
|
const client = getSupabaseRouteHandlerClient();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const { error, data } =
|
const { error } = await client.auth.exchangeCodeForSession(authCode);
|
||||||
await client.auth.exchangeCodeForSession(authCode);
|
|
||||||
|
|
||||||
// if we have an error, we redirect to the error page
|
// if we have an error, we redirect to the error page
|
||||||
if (error) {
|
if (error) {
|
||||||
return onError({ error: error.message });
|
return onError({ error: error.message });
|
||||||
}
|
}
|
||||||
|
|
||||||
userId = data.user.id;
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
Logger.error(
|
Logger.error(
|
||||||
{
|
{
|
||||||
@@ -42,34 +49,6 @@ export async function GET(request: NextRequest) {
|
|||||||
|
|
||||||
return onError({ error: message as string });
|
return onError({ error: message as string });
|
||||||
}
|
}
|
||||||
|
|
||||||
if (inviteCode && userId) {
|
|
||||||
try {
|
|
||||||
Logger.info(
|
|
||||||
{
|
|
||||||
userId,
|
|
||||||
inviteCode,
|
|
||||||
},
|
|
||||||
`Attempting to accept user invite...`,
|
|
||||||
);
|
|
||||||
|
|
||||||
// if we have an invite code, we accept the invite
|
|
||||||
await acceptInviteFromEmailLink({ inviteCode, userId });
|
|
||||||
} catch (error) {
|
|
||||||
Logger.error(
|
|
||||||
{
|
|
||||||
userId,
|
|
||||||
inviteCode,
|
|
||||||
error,
|
|
||||||
},
|
|
||||||
`An error occurred while accepting user invite`,
|
|
||||||
);
|
|
||||||
|
|
||||||
const message = error instanceof Error ? error.message : error;
|
|
||||||
|
|
||||||
return onError({ error: message as string });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
@@ -79,37 +58,6 @@ export async function GET(request: NextRequest) {
|
|||||||
return redirect(nextUrl);
|
return redirect(nextUrl);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @name acceptInviteFromEmailLink
|
|
||||||
* @description If we find an invite code, we try to accept the invite
|
|
||||||
* received from the email link method
|
|
||||||
* @param params
|
|
||||||
*/
|
|
||||||
async function acceptInviteFromEmailLink(params: {
|
|
||||||
inviteCode: string;
|
|
||||||
userId: string | undefined;
|
|
||||||
}) {
|
|
||||||
if (!params.userId) {
|
|
||||||
Logger.error(params, `Attempted to accept invite, but no user id provided`);
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
Logger.info(params, `Found invite code. Accepting invite...`);
|
|
||||||
|
|
||||||
await acceptInviteToOrganization(
|
|
||||||
getSupabaseRouteHandlerClient({
|
|
||||||
admin: true,
|
|
||||||
}),
|
|
||||||
{
|
|
||||||
code: params.inviteCode,
|
|
||||||
userId: params.userId,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
Logger.info(params, `Invite successfully accepted`);
|
|
||||||
}
|
|
||||||
|
|
||||||
function onError({ error }: { error: string }) {
|
function onError({ error }: { error: string }) {
|
||||||
const errorMessage = getAuthErrorMessage(error);
|
const errorMessage = getAuthErrorMessage(error);
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,6 @@
|
|||||||
import { notFound } from 'next/navigation';
|
import { notFound } from 'next/navigation';
|
||||||
|
|
||||||
import { getSupabaseServerComponentClient } from '@kit/supabase/server-component-client';
|
import { getSupabaseServerComponentClient } from '@kit/supabase/server-component-client';
|
||||||
import { Heading } from '@kit/ui/heading';
|
|
||||||
import { If } from '@kit/ui/if';
|
|
||||||
import { Trans } from '@kit/ui/trans';
|
|
||||||
|
|
||||||
import { withI18n } from '~/lib/i18n/with-i18n';
|
import { withI18n } from '~/lib/i18n/with-i18n';
|
||||||
|
|
||||||
@@ -25,36 +22,7 @@ async function JoinTeamAccountPage({ searchParams }: Context) {
|
|||||||
notFound();
|
notFound();
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return <></>;
|
||||||
<>
|
|
||||||
<Heading level={4}>
|
|
||||||
<Trans
|
|
||||||
i18nKey={'auth:joinOrganizationHeading'}
|
|
||||||
values={{
|
|
||||||
organization: organization.name,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Heading>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<p className={'text-center'}>
|
|
||||||
<Trans
|
|
||||||
i18nKey={'auth:joinOrganizationSubHeading'}
|
|
||||||
values={{
|
|
||||||
organization: organization.name,
|
|
||||||
}}
|
|
||||||
components={{ b: <b /> }}
|
|
||||||
/>
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<p className={'text-center'}>
|
|
||||||
<If condition={!data.session}>
|
|
||||||
<Trans i18nKey={'auth:signUpToAcceptInvite'} />
|
|
||||||
</If>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default withI18n(JoinTeamAccountPage);
|
export default withI18n(JoinTeamAccountPage);
|
||||||
@@ -66,7 +34,7 @@ async function getInviteDataFromInviteToken(token: string) {
|
|||||||
|
|
||||||
const { data: invitation, error } = await adminClient
|
const { data: invitation, error } = await adminClient
|
||||||
.from('invitations')
|
.from('invitations')
|
||||||
.select('*')
|
.select()
|
||||||
.eq('invite_token', token)
|
.eq('invite_token', token)
|
||||||
.single();
|
.single();
|
||||||
|
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ const authConfig = AuthConfigSchema.parse({
|
|||||||
magicLink: false,
|
magicLink: false,
|
||||||
oAuth: ['google'],
|
oAuth: ['google'],
|
||||||
},
|
},
|
||||||
});
|
} satisfies z.infer<typeof AuthConfigSchema>);
|
||||||
|
|
||||||
export default authConfig;
|
export default authConfig;
|
||||||
|
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ const featuresFlagConfig = FeatureFlagsSchema.parse({
|
|||||||
process.env.NEXT_PUBLIC_ENABLE_ORGANIZATION_BILLING,
|
process.env.NEXT_PUBLIC_ENABLE_ORGANIZATION_BILLING,
|
||||||
false,
|
false,
|
||||||
),
|
),
|
||||||
});
|
} satisfies z.infer<typeof FeatureFlagsSchema>);
|
||||||
|
|
||||||
export default featuresFlagConfig;
|
export default featuresFlagConfig;
|
||||||
|
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ const PathsSchema = z.object({
|
|||||||
accountBilling: z.string().min(1),
|
accountBilling: z.string().min(1),
|
||||||
accountMembers: z.string().min(1),
|
accountMembers: z.string().min(1),
|
||||||
accountBillingReturn: z.string().min(1),
|
accountBillingReturn: z.string().min(1),
|
||||||
|
joinTeam: z.string().min(1),
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -41,7 +42,8 @@ const pathsConfig = PathsSchema.parse({
|
|||||||
accountBilling: `/home/[account]/billing`,
|
accountBilling: `/home/[account]/billing`,
|
||||||
accountMembers: `/home/[account]/members`,
|
accountMembers: `/home/[account]/members`,
|
||||||
accountBillingReturn: `/home/[account]/billing/return`,
|
accountBillingReturn: `/home/[account]/billing/return`,
|
||||||
|
joinTeam: '/join',
|
||||||
},
|
},
|
||||||
});
|
} satisfies z.infer<typeof PathsSchema>);
|
||||||
|
|
||||||
export default pathsConfig;
|
export default pathsConfig;
|
||||||
|
|||||||
@@ -57,11 +57,13 @@ export default withBundleAnalyzer({
|
|||||||
})(config);
|
})(config);
|
||||||
|
|
||||||
function getRemotePatterns() {
|
function getRemotePatterns() {
|
||||||
// add here the remote patterns for your images
|
/** @type {import('next').NextConfig['remotePatterns']} */
|
||||||
|
// add here the remote patterns for your images
|
||||||
const remotePatterns = [];
|
const remotePatterns = [];
|
||||||
|
|
||||||
if (SUPABASE_URL) {
|
if (SUPABASE_URL) {
|
||||||
const hostname = new URL(SUPABASE_URL).hostname;
|
const hostname = new URL(SUPABASE_URL).hostname;
|
||||||
|
|
||||||
remotePatterns.push({
|
remotePatterns.push({
|
||||||
protocol: 'https',
|
protocol: 'https',
|
||||||
hostname,
|
hostname,
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
"subscriptionTabSubheading": "Manage your Subscription and Billing",
|
"subscriptionTabSubheading": "Manage your Subscription and Billing",
|
||||||
"planCardTitle": "Your Plan",
|
"planCardTitle": "Your Plan",
|
||||||
"planCardDescription": "Below are the details of your current plan. You can change your plan or cancel your subscription at any time.",
|
"planCardDescription": "Below are the details of your current plan. You can change your plan or cancel your subscription at any time.",
|
||||||
"planRenewal": "Renews every {{interval}} at {{currency}} {{price}}",
|
"planRenewal": "Renews every {{interval}} at {{price}}",
|
||||||
"planDetails": "Plan Details",
|
"planDetails": "Plan Details",
|
||||||
"checkout": "Proceed to Checkout",
|
"checkout": "Proceed to Checkout",
|
||||||
"trialEndsOn": "Your trial ends on",
|
"trialEndsOn": "Your trial ends on",
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { BadgeCheck, CheckCircle2 } from 'lucide-react';
|
|||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
import { BillingSchema, getProductPlanPairFromId } from '@kit/billing';
|
import { BillingSchema, getProductPlanPairFromId } from '@kit/billing';
|
||||||
|
import { formatCurrency } from '@kit/shared/utils';
|
||||||
import { Database } from '@kit/supabase/database';
|
import { Database } from '@kit/supabase/database';
|
||||||
import {
|
import {
|
||||||
Accordion,
|
Accordion,
|
||||||
@@ -65,8 +66,7 @@ export function CurrentPlanCard({
|
|||||||
i18nKey="billing:planRenewal"
|
i18nKey="billing:planRenewal"
|
||||||
values={{
|
values={{
|
||||||
interval: subscription.interval,
|
interval: subscription.interval,
|
||||||
currency: product.currency,
|
price: formatCurrency(product.currency, plan.price),
|
||||||
price: plan.price,
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ export function EmbeddedCheckout(
|
|||||||
props: React.PropsWithChildren<{
|
props: React.PropsWithChildren<{
|
||||||
checkoutToken: string;
|
checkoutToken: string;
|
||||||
provider: BillingProvider;
|
provider: BillingProvider;
|
||||||
onClose: () => void;
|
onClose?: () => void;
|
||||||
}>,
|
}>,
|
||||||
) {
|
) {
|
||||||
const CheckoutComponent = useMemo(
|
const CheckoutComponent = useMemo(
|
||||||
|
|||||||
@@ -3,3 +3,4 @@ export * from './current-plan-card';
|
|||||||
export * from './embedded-checkout';
|
export * from './embedded-checkout';
|
||||||
export * from './billing-session-status';
|
export * from './billing-session-status';
|
||||||
export * from './billing-portal-card';
|
export * from './billing-portal-card';
|
||||||
|
export * from './pricing-table';
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { useForm } from 'react-hook-form';
|
|||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
import { BillingSchema } from '@kit/billing';
|
import { BillingSchema } from '@kit/billing';
|
||||||
|
import { formatCurrency } from '@kit/shared/utils';
|
||||||
import { Button } from '@kit/ui/button';
|
import { Button } from '@kit/ui/button';
|
||||||
import {
|
import {
|
||||||
Form,
|
Form,
|
||||||
@@ -233,10 +234,3 @@ function Price(props: React.PropsWithChildren) {
|
|||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatCurrency(currencyCode: string, value: string) {
|
|
||||||
return new Intl.NumberFormat('en-US', {
|
|
||||||
style: 'currency',
|
|
||||||
currency: currencyCode,
|
|
||||||
}).format(value);
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -7,14 +7,13 @@ import Link from 'next/link';
|
|||||||
import { CheckCircle, Sparkles } from 'lucide-react';
|
import { CheckCircle, Sparkles } from 'lucide-react';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
import { BillingSchema, getPlanIntervals } from '@kit/billing';
|
||||||
import { Button } from '@kit/ui/button';
|
import { Button } from '@kit/ui/button';
|
||||||
import { Heading } from '@kit/ui/heading';
|
import { Heading } from '@kit/ui/heading';
|
||||||
import { If } from '@kit/ui/if';
|
import { If } from '@kit/ui/if';
|
||||||
import { Trans } from '@kit/ui/trans';
|
import { Trans } from '@kit/ui/trans';
|
||||||
import { cn } from '@kit/ui/utils';
|
import { cn } from '@kit/ui/utils';
|
||||||
|
|
||||||
import { BillingSchema, getPlanIntervals } from '../create-billing-schema';
|
|
||||||
|
|
||||||
type Config = z.infer<typeof BillingSchema>;
|
type Config = z.infer<typeof BillingSchema>;
|
||||||
|
|
||||||
interface Paths {
|
interface Paths {
|
||||||
@@ -17,10 +17,14 @@
|
|||||||
"@kit/prettier-config": "0.1.0",
|
"@kit/prettier-config": "0.1.0",
|
||||||
"@kit/tailwind-config": "0.1.0",
|
"@kit/tailwind-config": "0.1.0",
|
||||||
"@kit/tsconfig": "0.1.0",
|
"@kit/tsconfig": "0.1.0",
|
||||||
"@kit/ui": "*"
|
"@kit/ui": "*",
|
||||||
|
"@kit/supabase": "*",
|
||||||
|
"@supabase/supabase-js": "2.40.0",
|
||||||
|
"lucide-react": "^0.363.0"
|
||||||
},
|
},
|
||||||
"exports": {
|
"exports": {
|
||||||
".": "./src/index.ts"
|
".": "./src/index.ts",
|
||||||
|
"./components/*": "./src/components/*"
|
||||||
},
|
},
|
||||||
"eslintConfig": {
|
"eslintConfig": {
|
||||||
"root": true,
|
"root": true,
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ interface Data {
|
|||||||
trialSubscriptions: number;
|
trialSubscriptions: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
function AdminDashboard({
|
export function AdminDashboard({
|
||||||
data,
|
data,
|
||||||
}: React.PropsWithChildren<{
|
}: React.PropsWithChildren<{
|
||||||
data: Data;
|
data: Data;
|
||||||
@@ -70,8 +70,6 @@ function AdminDashboard({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default AdminDashboard;
|
|
||||||
|
|
||||||
function Figure(props: React.PropsWithChildren) {
|
function Figure(props: React.PropsWithChildren) {
|
||||||
return <div className={'text-3xl font-bold'}>{props.children}</div>;
|
return <div className={'text-3xl font-bold'}>{props.children}</div>;
|
||||||
}
|
}
|
||||||
@@ -1,22 +1,23 @@
|
|||||||
import { notFound } from 'next/navigation';
|
import { notFound } from 'next/navigation';
|
||||||
|
|
||||||
import isUserSuperAdmin from '../../../app/admin/utils/is-user-super-admin';
|
import { getSupabaseServerComponentClient } from '@kit/supabase/server-component-client';
|
||||||
|
|
||||||
|
import { isSuperAdmin } from '../lib/is-super-admin';
|
||||||
|
|
||||||
type LayoutOrPageComponent<Params> = React.ComponentType<Params>;
|
type LayoutOrPageComponent<Params> = React.ComponentType<Params>;
|
||||||
|
|
||||||
function AdminGuard<Params extends object>(
|
export function AdminGuard<Params extends object>(
|
||||||
Component: LayoutOrPageComponent<Params>,
|
Component: LayoutOrPageComponent<Params>,
|
||||||
) {
|
) {
|
||||||
return async function AdminGuardServerComponentWrapper(params: Params) {
|
return async function AdminGuardServerComponentWrapper(params: Params) {
|
||||||
const isAdmin = await isUserSuperAdmin();
|
const client = getSupabaseServerComponentClient();
|
||||||
|
const isUserSuperAdmin = await isSuperAdmin(client);
|
||||||
|
|
||||||
// if the user is not a super-admin, we redirect to a 404
|
// if the user is not a super-admin, we redirect to a 404
|
||||||
if (!isAdmin) {
|
if (!isUserSuperAdmin) {
|
||||||
notFound();
|
notFound();
|
||||||
}
|
}
|
||||||
|
|
||||||
return <Component {...params} />;
|
return <Component {...params} />;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export default AdminGuard;
|
|
||||||
@@ -5,11 +5,10 @@ import { ArrowLeft } from 'lucide-react';
|
|||||||
import { Button } from '@kit/ui/button';
|
import { Button } from '@kit/ui/button';
|
||||||
import { PageHeader } from '@kit/ui/page';
|
import { PageHeader } from '@kit/ui/page';
|
||||||
|
|
||||||
function AdminHeader({
|
export function AdminHeader({
|
||||||
children,
|
children,
|
||||||
paths,
|
paths,
|
||||||
}: React.PropsWithChildren<{
|
}: React.PropsWithChildren<{
|
||||||
appHome: string;
|
|
||||||
paths: {
|
paths: {
|
||||||
appHome: string;
|
appHome: string;
|
||||||
};
|
};
|
||||||
@@ -28,5 +27,3 @@ function AdminHeader({
|
|||||||
</PageHeader>
|
</PageHeader>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default AdminHeader;
|
|
||||||
@@ -1,10 +1,8 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import { Home, User, Users } from 'lucide-react';
|
import { Home, User, Users } from 'lucide-react';
|
||||||
|
|
||||||
import { Sidebar, SidebarContent, SidebarItem } from '@kit/ui/sidebar';
|
import { Sidebar, SidebarContent, SidebarItem } from '@kit/ui/sidebar';
|
||||||
|
|
||||||
function AdminSidebar(props: { Logo: React.ReactNode }) {
|
export function AdminSidebar(props: { Logo: React.ReactNode }) {
|
||||||
return (
|
return (
|
||||||
<Sidebar>
|
<Sidebar>
|
||||||
<SidebarContent className={'mb-6 mt-4 pt-2'}>{props.Logo}</SidebarContent>
|
<SidebarContent className={'mb-6 mt-4 pt-2'}>{props.Logo}</SidebarContent>
|
||||||
@@ -28,5 +26,3 @@ function AdminSidebar(props: { Logo: React.ReactNode }) {
|
|||||||
</Sidebar>
|
</Sidebar>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default AdminSidebar;
|
|
||||||
1
packages/features/admin/src/index.ts
Normal file
1
packages/features/admin/src/index.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export * from './lib/is-super-admin';
|
||||||
19
packages/features/admin/src/lib/is-super-admin.ts
Normal file
19
packages/features/admin/src/lib/is-super-admin.ts
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
import { SupabaseClient } from '@supabase/supabase-js';
|
||||||
|
|
||||||
|
import { Database } from '@kit/supabase/database';
|
||||||
|
|
||||||
|
export async function isSuperAdmin(client: SupabaseClient<Database>) {
|
||||||
|
const { data, error } = await client.auth.getUser();
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!data.user) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const appMetadata = data.user.app_metadata;
|
||||||
|
|
||||||
|
return appMetadata?.role === 'super-admin';
|
||||||
|
}
|
||||||
@@ -72,7 +72,7 @@ export class AccountInvitationsService {
|
|||||||
role: params.role,
|
role: params.role,
|
||||||
})
|
})
|
||||||
.match({
|
.match({
|
||||||
id: params.id,
|
id: params.invitationId,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
|
|||||||
@@ -1,3 +1,10 @@
|
|||||||
export function isBrowser() {
|
export function isBrowser() {
|
||||||
return typeof window !== 'undefined';
|
return typeof window !== 'undefined';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function formatCurrency(currencyCode: string, value: string | number) {
|
||||||
|
return new Intl.NumberFormat('en-US', {
|
||||||
|
style: 'currency',
|
||||||
|
currency: currencyCode,
|
||||||
|
}).format(Number(value));
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
|
|
||||||
import { getSupabaseBrowserClient } from '../clients/browser.client';
|
import { getSupabaseBrowserClient } from '../clients/browser.client';
|
||||||
import { Database } from '../database.types';
|
|
||||||
|
|
||||||
export function useSupabase<Schema = Database>() {
|
export function useSupabase() {
|
||||||
return useMemo(() => getSupabaseBrowserClient<Schema>(), []);
|
return useMemo(() => getSupabaseBrowserClient(), []);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ import {
|
|||||||
TableRow,
|
TableRow,
|
||||||
} from '../shadcn/table';
|
} from '../shadcn/table';
|
||||||
import { cn } from '../utils';
|
import { cn } from '../utils';
|
||||||
import Trans from './trans';
|
import { Trans } from './trans';
|
||||||
|
|
||||||
interface ReactTableProps<T extends object> {
|
interface ReactTableProps<T extends object> {
|
||||||
data: T[];
|
data: T[];
|
||||||
|
|||||||
@@ -49,6 +49,11 @@ export const ImageUploadInput = forwardRef<React.ElementRef<'input'>, Props>(
|
|||||||
|
|
||||||
if (files?.length) {
|
if (files?.length) {
|
||||||
const file = files[0];
|
const file = files[0];
|
||||||
|
|
||||||
|
if (!file) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const data = URL.createObjectURL(file);
|
const data = URL.createObjectURL(file);
|
||||||
|
|
||||||
setState({
|
setState({
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ export default function isRouteActive(
|
|||||||
depth: number,
|
depth: number,
|
||||||
) {
|
) {
|
||||||
// we remove any eventual query param from the route's URL
|
// we remove any eventual query param from the route's URL
|
||||||
const currentRoutePath = currentRoute.split('?')[0];
|
const currentRoutePath = currentRoute.split('?')[0] ?? '';
|
||||||
|
|
||||||
if (!isRoot(currentRoutePath) && isRoot(targetLink)) {
|
if (!isRoot(currentRoutePath) && isRoot(targetLink)) {
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { forwardRef } from 'react';
|
|||||||
|
|
||||||
import Image from 'next/image';
|
import Image from 'next/image';
|
||||||
|
|
||||||
import { cn } from '../../shadcn';
|
import { cn } from '../../utils';
|
||||||
import { LazyRender } from '../lazy-render';
|
import { LazyRender } from '../lazy-render';
|
||||||
|
|
||||||
const NextImage: React.FC<{
|
const NextImage: React.FC<{
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import type { MDXComponents } from 'mdx/types';
|
|||||||
import { getMDXComponent } from 'next-contentlayer/hooks';
|
import { getMDXComponent } from 'next-contentlayer/hooks';
|
||||||
|
|
||||||
import Components from './mdx-components';
|
import Components from './mdx-components';
|
||||||
|
// @ts-ignore
|
||||||
import styles from './mdx-renderer.module.css';
|
import styles from './mdx-renderer.module.css';
|
||||||
|
|
||||||
export function Mdx({
|
export function Mdx({
|
||||||
|
|||||||
9
pnpm-lock.yaml
generated
9
pnpm-lock.yaml
generated
@@ -319,6 +319,9 @@ importers:
|
|||||||
'@kit/prettier-config':
|
'@kit/prettier-config':
|
||||||
specifier: 0.1.0
|
specifier: 0.1.0
|
||||||
version: link:../../../tooling/prettier
|
version: link:../../../tooling/prettier
|
||||||
|
'@kit/supabase':
|
||||||
|
specifier: '*'
|
||||||
|
version: link:../../supabase
|
||||||
'@kit/tailwind-config':
|
'@kit/tailwind-config':
|
||||||
specifier: 0.1.0
|
specifier: 0.1.0
|
||||||
version: link:../../../tooling/tailwind
|
version: link:../../../tooling/tailwind
|
||||||
@@ -328,6 +331,12 @@ importers:
|
|||||||
'@kit/ui':
|
'@kit/ui':
|
||||||
specifier: '*'
|
specifier: '*'
|
||||||
version: link:../../ui
|
version: link:../../ui
|
||||||
|
'@supabase/supabase-js':
|
||||||
|
specifier: 2.40.0
|
||||||
|
version: 2.40.0
|
||||||
|
lucide-react:
|
||||||
|
specifier: ^0.363.0
|
||||||
|
version: 0.363.0(react@18.2.0)
|
||||||
|
|
||||||
packages/features/auth:
|
packages/features/auth:
|
||||||
devDependencies:
|
devDependencies:
|
||||||
|
|||||||
Reference in New Issue
Block a user