Skip to content

Commit

Permalink
feat: allow turning off pages from Payload
Browse files Browse the repository at this point in the history
  • Loading branch information
mvarendorff committed Jan 6, 2024
1 parent 3a78aaf commit 089ae21
Show file tree
Hide file tree
Showing 13 changed files with 172 additions and 15 deletions.
10 changes: 10 additions & 0 deletions packages/cms/src/access/hidden-unless-owner.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import {type User} from 'payload/auth';
import {type SessionUser} from '../collections/users';

export const hiddenUnlessOwner = ({user}: {user: User}) => {
const sessionUser = user as unknown as SessionUser;
const roles =
'roles' in sessionUser ? sessionUser.roles : sessionUser.user.roles;

return !roles.includes('owner');
};
39 changes: 39 additions & 0 deletions packages/cms/src/collections/features.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import {type CollectionConfig} from 'payload/types';
import {hiddenUnlessOwner} from '../access/hidden-unless-owner';
import {requireOneOf} from '../access/require-one-of';

export const Features: CollectionConfig = {
slug: 'feature',
access: {
create: requireOneOf(),
update: requireOneOf(),
delete: requireOneOf(),
},
admin: {
useAsTitle: 'name',
hidden: hiddenUnlessOwner,
},
fields: [
{
name: 'name',
type: 'text',
required: true,
},
{
name: 'pathPrefix',
type: 'text',
required: true,
},
{
name: 'navPath',
type: 'text',
hooks: {
beforeValidate: [({value, data}) => value ?? data.pathPrefix],
},
},
{
name: 'enabled',
type: 'checkbox',
},
],
};
10 changes: 4 additions & 6 deletions packages/cms/src/collections/users.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import {type GeneratedTypes} from 'payload';
import parseCookies from 'payload/dist/utilities/parseCookies';
import {type CollectionConfig} from 'payload/types';
import {hiddenUnlessOwner} from '../access/hidden-unless-owner';
import {requireOneOf} from '../access/require-one-of';
import {YtfAuthStrategy} from '../lib/auth-strategy';
import {getUserIdFromRequest} from '../lib/get-user-id-from-request';
Expand Down Expand Up @@ -49,22 +50,19 @@ export const Users: CollectionConfig = {
create: requireOneOf(),
update: requireOneOf(),
delete: requireOneOf(),
read: ({req, id}) => {
read: ({req}) => {
const {user} = req;
if (!user?.user) return false;

const {roles} = user.user;
if (roles.includes('owner')) return true;

// Setting it up this way, hides the users collection from the UI while still allowing the /me endpoint to
// function.
if (id) return user.user.id === id;

return false;
return true;
},
},
admin: {
useAsTitle: 'id',
hidden: hiddenUnlessOwner,
},
endpoints: [
{
Expand Down
10 changes: 10 additions & 0 deletions packages/cms/src/payload-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ export interface Config {
users: User;
groupchats: Groupchat;
'groupchat-keywords': GroupchatKeyword;
feature: Feature;
'payload-preferences': PayloadPreference;
'payload-migrations': PayloadMigration;
};
Expand Down Expand Up @@ -49,6 +50,15 @@ export interface GroupchatKeyword {
updatedAt: string;
createdAt: string;
}
export interface Feature {
id: number;
name: string;
pathPrefix: string;
navPath?: string | null;
enabled?: boolean | null;
updatedAt: string;
createdAt: string;
}
export interface PayloadPreference {
id: number;
user: {
Expand Down
3 changes: 2 additions & 1 deletion packages/cms/src/payload.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {viteBundler} from '@payloadcms/bundler-vite';
import {postgresAdapter} from '@payloadcms/db-postgres';
import {slateEditor} from '@payloadcms/richtext-slate';
import {buildConfig, type Config} from 'payload/config';
import {Features} from './collections/features';
import {GroupchatKeywords} from './collections/groupchat-keywords';
import {Groupchats} from './collections/groupchats';
import {Users} from './collections/users';
Expand All @@ -26,7 +27,7 @@ const config: Config = {
},
},
editor: slateEditor({}),
collections: [Users, Groupchats, GroupchatKeywords],
collections: [Users, Groupchats, GroupchatKeywords, Features],
db: postgresAdapter({
migrationDir: path.resolve(__dirname, 'migrations'),
pool: {connectionString: process.env.DATABASE_URI},
Expand Down
2 changes: 1 addition & 1 deletion packages/server/src/servers/public.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ import {type YtfApolloContext} from '../types';
const logger = createServerLogger('server', 'public');

const allowedPayloadOperations = {
Query: ['groupchatSearchToken'],
Query: ['groupchatSearchToken', 'Features'],
Mutation: <string[]>[],
Subscription: <string[]>[],
};
Expand Down
45 changes: 45 additions & 0 deletions packages/web/src/__generated__/graphql.ts

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions packages/web/src/app/buddyproject/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {Image} from 'ui';
import {InfoGrid} from 'ui/buddyproject';
import {ScrollToActionContainer} from 'ui/client';
import {buddyProjectSvg, yesbotBuddyProjectWebp} from '../../../assets';
import {ensureNavEnabled} from '../../lib/features/features';
import {graphqlWithHeaders} from '../../lib/graphql/client';
import {BuddyProjectButton} from './components/buddy-project-button';

Expand Down Expand Up @@ -44,6 +45,8 @@ const CTA = () => {
};

const Page = async () => {
await ensureNavEnabled('buddyproject');

const x = await graphqlWithHeaders((sdk) => sdk.ServerState());

const currentUser = x.me;
Expand Down
3 changes: 3 additions & 0 deletions packages/web/src/app/groupchats/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import {type Metadata} from 'next';
import {Heading} from 'ui';
import {TypesenseProvider} from '../../context/typesense';
import {getIsLoggedIn} from '../../context/user/user';
import {ensureNavEnabled} from '../../lib/features/features';
import {graphqlWithHeaders} from '../../lib/graphql/client';
import {GroupChatSearch} from './components/group-chat-search';

Expand All @@ -10,6 +11,8 @@ export const metadata: Metadata = {
};

const GroupChats = async () => {
await ensureNavEnabled('groupchats');

const isLoggedIn = await getIsLoggedIn();
const apiKey = await graphqlWithHeaders((sdk) => sdk.TypesenseApiKey());

Expand Down
4 changes: 3 additions & 1 deletion packages/web/src/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {type PropsWithChildren, Suspense} from 'react';
import {Footer} from 'ui';
import {CookieConsent} from '../components/cookie-consent/cookie-consent';
import {getCurrentUser} from '../context/user/user';
import {getNavRoutes} from '../lib/features/features';
import {Nav} from './nav';
import {Providers} from './providers';

Expand Down Expand Up @@ -36,6 +37,7 @@ export const metadata: Metadata = {

const RootLayout = async ({children}: PropsWithChildren) => {
const user = await getCurrentUser();
const routes = await getNavRoutes();

return (
<html lang='en'>
Expand All @@ -50,7 +52,7 @@ const RootLayout = async ({children}: PropsWithChildren) => {
<Providers user={user}>
<CookieConsent />
<div className='flex min-h-screen flex-col justify-between'>
<Nav user={user} />
<Nav user={user} routes={routes} />

<main className='mx-auto w-full max-w-7xl px-4 md:px-8'>
{children}
Expand Down
10 changes: 4 additions & 6 deletions packages/web/src/app/nav.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,24 +2,22 @@

import {useRouter} from 'next/navigation';
import {type FC} from 'react';
import {Navigation} from 'ui';
import {Navigation, type NavLinkDefinition} from 'ui';
import {type CurrentUserQuery} from '../__generated__/graphql';
import {logout} from '../context/user/logout-server-action';
import {navigateToLogin} from '../context/user/navigate-to-login';

export type NavProps = {
user: Exclude<CurrentUserQuery['me'], null>;
routes: NavLinkDefinition[];
};

export const Nav: FC<NavProps> = ({user}) => {
export const Nav: FC<NavProps> = ({user, routes}) => {
const router = useRouter();

return (
<Navigation
links={[
{text: 'Buddy Project', href: '/buddyproject'},
{text: 'Groupchats', href: '/groupchats'},
]}
links={routes}
onLoginButtonClick={() => navigateToLogin()}
menuItems={[
{
Expand Down
11 changes: 11 additions & 0 deletions packages/web/src/lib/features/features.graphql
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
query Features {
Features {
docs {
id
name
enabled
navPath
pathPrefix
}
}
}
37 changes: 37 additions & 0 deletions packages/web/src/lib/features/features.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import {notFound} from 'next/navigation';
import {type NavLinkDefinition} from '../../ui';
import {type FeaturesQuery, graphqlWithHeaders} from '../graphql/client';

type Defined<T> = Exclude<T, null | undefined>;
type Features = Defined<Defined<FeaturesQuery['Features']>['docs']>;
type Feature = Defined<Features[number]>;

const getFeatures = async (): Promise<Feature[]> => {
const data = await graphqlWithHeaders((sdk) => sdk.Features());

return (data.Features?.docs ?? []).filter((f): f is Feature => !!f);
};

const isNavEnabled = async (path: string): Promise<boolean> => {
const features = await getFeatures();
const feature = features.find((f) => path.startsWith(f.pathPrefix));

// If not defined otherwise, we consider a page accessible (think legal stuff that isn't setup in payload)
return feature?.enabled ?? true;
};

export const getNavRoutes = async (): Promise<NavLinkDefinition[]> => {
const features = await getFeatures();

return features
.filter((f) => f.enabled)
.map((f) => ({text: f.name, href: f.navPath ?? '#'}));
};

export const ensureNavEnabled = async (path: string) => {
const enabled = await isNavEnabled(path);

if (!enabled) {
notFound();
}
};

0 comments on commit 089ae21

Please sign in to comment.