mirror of
https://github.com/Nezumi-2711/multitenant-ecommerce.git
synced 2026-09-22 13:38:34 +00:00
Merge pull request #13 from Nezumi-2711/feat/multi-tenancy
Featuer/ Implement multi tenancy
This commit is contained in:
@@ -8,6 +8,7 @@
|
||||
"@payloadcms/db-mongodb": "^3.36.0",
|
||||
"@payloadcms/next": "^3.36.0",
|
||||
"@payloadcms/payload-cloud": "^3.36.0",
|
||||
"@payloadcms/plugin-multi-tenant": "3.36.0",
|
||||
"@payloadcms/richtext-lexical": "^3.36.0",
|
||||
"@radix-ui/react-accordion": "^1.2.8",
|
||||
"@radix-ui/react-alert-dialog": "^1.1.11",
|
||||
@@ -462,6 +463,8 @@
|
||||
|
||||
"@payloadcms/payload-cloud": ["@payloadcms/payload-cloud@3.36.0", "", { "dependencies": { "@aws-sdk/client-cognito-identity": "^3.614.0", "@aws-sdk/client-s3": "^3.614.0", "@aws-sdk/credential-providers": "^3.614.0", "@aws-sdk/lib-storage": "^3.614.0", "@payloadcms/email-nodemailer": "3.36.0", "amazon-cognito-identity-js": "^6.1.2", "nodemailer": "6.9.16" }, "peerDependencies": { "payload": "3.36.0" } }, "sha512-3S8PVBosNdPxk5FnlphfeqCS9ECDkMK4KTaZgXdoNI184psDH9Mu+QKBmMX2sH4WXxNVqQ5r7BRLNtQQqDvHkw=="],
|
||||
|
||||
"@payloadcms/plugin-multi-tenant": ["@payloadcms/plugin-multi-tenant@3.36.0", "", { "peerDependencies": { "@payloadcms/ui": "3.36.0", "next": "^15.2.3", "payload": "3.36.0" } }, "sha512-geX5jHLG5GUF/NVhyySNg0vG9sWVfeEPSSFh1L+YL48YFxRusITKSGXIzBdgNYUcQntQFeyTrGNWhazH8H4zZQ=="],
|
||||
|
||||
"@payloadcms/richtext-lexical": ["@payloadcms/richtext-lexical@3.36.0", "", { "dependencies": { "@lexical/headless": "0.28.0", "@lexical/html": "0.28.0", "@lexical/link": "0.28.0", "@lexical/list": "0.28.0", "@lexical/mark": "0.28.0", "@lexical/react": "0.28.0", "@lexical/rich-text": "0.28.0", "@lexical/selection": "0.28.0", "@lexical/table": "0.28.0", "@lexical/utils": "0.28.0", "@payloadcms/translations": "3.36.0", "@payloadcms/ui": "3.36.0", "@types/uuid": "10.0.0", "acorn": "8.12.1", "bson-objectid": "2.0.4", "dequal": "2.0.3", "escape-html": "1.0.3", "jsox": "1.2.121", "lexical": "0.28.0", "mdast-util-from-markdown": "2.0.2", "mdast-util-mdx-jsx": "3.1.3", "micromark-extension-mdx-jsx": "3.0.1", "qs-esm": "7.0.2", "react-error-boundary": "4.1.2", "ts-essentials": "10.0.3", "uuid": "10.0.0" }, "peerDependencies": { "@faceless-ui/modal": "3.0.0-beta.2", "@faceless-ui/scroll-info": "2.0.0", "@payloadcms/next": "3.36.0", "payload": "3.36.0", "react": "^19.0.0 || ^19.0.0-rc-65a56d0e-20241020", "react-dom": "^19.0.0 || ^19.0.0-rc-65a56d0e-20241020" } }, "sha512-HiT4fE7vENAXeWt8jup8wEYxtvFzx4TjnPjyCssxwbX/96ROlLQheVAq35zPAEYQnFZZMCfmaobpW/lUYgewXQ=="],
|
||||
|
||||
"@payloadcms/translations": ["@payloadcms/translations@3.36.0", "", { "dependencies": { "date-fns": "4.1.0" } }, "sha512-+TNesB9N85OAm6Cwt2wnQcrwhMv9z8S7vQYSWMyB/nS3V9cWHpsKShCTh9bLoIN2MjGTZ5QLw2Wx6p5kuvNX0w=="],
|
||||
|
||||
+3
-1
@@ -10,13 +10,15 @@
|
||||
"lint": "next lint",
|
||||
"generate:types": "payload generate:types",
|
||||
"db:fresh": "payload migrate:fresh",
|
||||
"db:seed": "bun run src/seed.ts"
|
||||
"db:seed": "bun run src/seed.ts",
|
||||
"db:reset": "bun run db:fresh && bun run db:seed"
|
||||
},
|
||||
"dependencies": {
|
||||
"@hookform/resolvers": "^5.0.1",
|
||||
"@payloadcms/db-mongodb": "^3.36.0",
|
||||
"@payloadcms/next": "^3.36.0",
|
||||
"@payloadcms/payload-cloud": "^3.36.0",
|
||||
"@payloadcms/plugin-multi-tenant": "3.36.0",
|
||||
"@payloadcms/richtext-lexical": "^3.36.0",
|
||||
"@radix-ui/react-accordion": "^1.2.8",
|
||||
"@radix-ui/react-alert-dialog": "^1.1.11",
|
||||
|
||||
@@ -4,6 +4,7 @@ import { dehydrate, HydrationBoundary } from '@tanstack/react-query';
|
||||
import { getQueryClient, trpc } from '@/trpc/server';
|
||||
import { loadProductFilters } from '@/modules/products/search-params';
|
||||
import { ProductListView } from '@/modules/products/ui/views/product-list-view';
|
||||
import { DEFAULT_LIMIT } from '@/constants';
|
||||
|
||||
interface Props {
|
||||
params: Promise<{
|
||||
@@ -17,10 +18,11 @@ const Page = async ({ params, searchParams }: Props) => {
|
||||
const queryClient = getQueryClient();
|
||||
const filters = await loadProductFilters(searchParams);
|
||||
|
||||
void queryClient.prefetchQuery(
|
||||
trpc.products.getMany.queryOptions({
|
||||
category: subcategory,
|
||||
void queryClient.prefetchInfiniteQuery(
|
||||
trpc.products.getMany.infiniteQueryOptions({
|
||||
...filters,
|
||||
category: subcategory,
|
||||
limit: DEFAULT_LIMIT,
|
||||
})
|
||||
);
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import { dehydrate, HydrationBoundary } from '@tanstack/react-query';
|
||||
import { getQueryClient, trpc } from '@/trpc/server';
|
||||
import { loadProductFilters } from '@/modules/products/search-params';
|
||||
import { ProductListView } from '@/modules/products/ui/views/product-list-view';
|
||||
import { DEFAULT_LIMIT } from '@/constants';
|
||||
|
||||
interface Props {
|
||||
params: Promise<{
|
||||
@@ -17,10 +18,11 @@ const Page = async ({ params, searchParams }: Props) => {
|
||||
const queryClient = getQueryClient();
|
||||
const filters = await loadProductFilters(searchParams);
|
||||
|
||||
void queryClient.prefetchQuery(
|
||||
trpc.products.getMany.queryOptions({
|
||||
category,
|
||||
void queryClient.prefetchInfiniteQuery(
|
||||
trpc.products.getMany.infiniteQueryOptions({
|
||||
...filters,
|
||||
category,
|
||||
limit: DEFAULT_LIMIT,
|
||||
})
|
||||
);
|
||||
|
||||
|
||||
@@ -1,12 +1,31 @@
|
||||
'use client';
|
||||
import type { SearchParams } from 'nuqs/server';
|
||||
import { dehydrate, HydrationBoundary } from '@tanstack/react-query';
|
||||
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { getQueryClient, trpc } from '@/trpc/server';
|
||||
import { loadProductFilters } from '@/modules/products/search-params';
|
||||
import { ProductListView } from '@/modules/products/ui/views/product-list-view';
|
||||
import { DEFAULT_LIMIT } from '@/constants';
|
||||
|
||||
import { useTRPC } from "@/trpc/client";
|
||||
|
||||
export default function Home() {
|
||||
const trpc = useTRPC();
|
||||
const { data } = useQuery(trpc.auth.session.queryOptions());
|
||||
|
||||
return <div>{JSON.stringify(data?.user, null, 2)}</div>;
|
||||
interface Props {
|
||||
searchParams: Promise<SearchParams>;
|
||||
}
|
||||
|
||||
const Page = async ({ searchParams }: Props) => {
|
||||
const filters = await loadProductFilters(searchParams);
|
||||
const queryClient = getQueryClient();
|
||||
|
||||
void queryClient.prefetchInfiniteQuery(
|
||||
trpc.products.getMany.infiniteQueryOptions({
|
||||
...filters,
|
||||
limit: DEFAULT_LIMIT,
|
||||
})
|
||||
);
|
||||
|
||||
return (
|
||||
<HydrationBoundary state={dehydrate(queryClient)}>
|
||||
<ProductListView />
|
||||
</HydrationBoundary>
|
||||
);
|
||||
};
|
||||
|
||||
export default Page;
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
|
||||
import { TenantField as TenantField_1d0591e3cf4f332c83a86da13a0de59a } from '@payloadcms/plugin-multi-tenant/client'
|
||||
import { TenantSelector as TenantSelector_1d0591e3cf4f332c83a86da13a0de59a } from '@payloadcms/plugin-multi-tenant/client'
|
||||
import { TenantSelectionProvider as TenantSelectionProvider_d6d5f193a167989e2ee7d14202901e62 } from '@payloadcms/plugin-multi-tenant/rsc'
|
||||
|
||||
export const importMap = {
|
||||
|
||||
"@payloadcms/plugin-multi-tenant/client#TenantField": TenantField_1d0591e3cf4f332c83a86da13a0de59a,
|
||||
"@payloadcms/plugin-multi-tenant/client#TenantSelector": TenantSelector_1d0591e3cf4f332c83a86da13a0de59a,
|
||||
"@payloadcms/plugin-multi-tenant/rsc#TenantSelectionProvider": TenantSelectionProvider_d6d5f193a167989e2ee7d14202901e62
|
||||
}
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
import { CollectionConfig } from "payload";
|
||||
|
||||
export const Tenants: CollectionConfig = {
|
||||
slug: 'tenants',
|
||||
admin: {
|
||||
useAsTitle: 'slug',
|
||||
},
|
||||
fields: [
|
||||
{
|
||||
name: 'name',
|
||||
type: 'text',
|
||||
required: true,
|
||||
label: 'Store Name',
|
||||
admin: {
|
||||
description: "This is the name of the store (e.g. Nezumi's Store)",
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'slug',
|
||||
type: 'text',
|
||||
index: true,
|
||||
required: true,
|
||||
unique: true,
|
||||
admin: {
|
||||
description: "This is the subdomain for the store (e.g. [slug].funroad.com)",
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'image',
|
||||
type: 'upload',
|
||||
relationTo: 'media',
|
||||
},
|
||||
{
|
||||
name: 'stripeAccountId',
|
||||
type: 'text',
|
||||
required: true,
|
||||
admin: {
|
||||
readOnly: true,
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'stripeDetailsSubmitted',
|
||||
type: 'checkbox',
|
||||
admin: {
|
||||
readOnly: true,
|
||||
description: 'You cannot create products until you submit your Stripe details',
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,4 +1,21 @@
|
||||
import type { CollectionConfig } from 'payload'
|
||||
import type { CollectionConfig } from 'payload';
|
||||
import { tenantsArrayField } from '@payloadcms/plugin-multi-tenant/fields';
|
||||
|
||||
const defaultTenantArrayField = tenantsArrayField({
|
||||
tenantsArrayFieldName: 'tenants',
|
||||
tenantsCollectionSlug: 'tenants',
|
||||
tenantsArrayTenantFieldName: 'tenant',
|
||||
arrayFieldAccess: {
|
||||
read: () => true,
|
||||
create: () => true,
|
||||
update: () => true,
|
||||
},
|
||||
tenantFieldAccess: {
|
||||
read: () => true,
|
||||
create: () => true,
|
||||
update: () => true,
|
||||
}
|
||||
})
|
||||
|
||||
export const Users: CollectionConfig = {
|
||||
slug: 'users',
|
||||
@@ -12,6 +29,23 @@ export const Users: CollectionConfig = {
|
||||
required: true,
|
||||
unique: true,
|
||||
type: 'text'
|
||||
},
|
||||
{
|
||||
admin: {
|
||||
position: 'sidebar',
|
||||
},
|
||||
name: 'roles',
|
||||
type: 'select',
|
||||
defaultValue: ['user'],
|
||||
hasMany: true,
|
||||
options: ['super-admin', 'user'],
|
||||
},
|
||||
{
|
||||
...defaultTenantArrayField,
|
||||
admin: {
|
||||
...(defaultTenantArrayField?.admin || {}),
|
||||
position: 'sidebar',
|
||||
}
|
||||
}
|
||||
],
|
||||
}
|
||||
|
||||
@@ -34,13 +34,27 @@ export const authRouter = createTRPCRouter({
|
||||
})
|
||||
}
|
||||
|
||||
const tenant = await ctx.db.create({
|
||||
collection: 'tenants',
|
||||
data: {
|
||||
name: input.username,
|
||||
slug: input.username,
|
||||
stripeAccountId: 'test',
|
||||
}
|
||||
})
|
||||
|
||||
await ctx.db.create({
|
||||
collection: 'users',
|
||||
data: {
|
||||
email: input.email,
|
||||
username: input.username,
|
||||
password: input.password,
|
||||
}
|
||||
tenants: [
|
||||
{
|
||||
tenant: tenant.id,
|
||||
},
|
||||
],
|
||||
},
|
||||
})
|
||||
|
||||
const data = await ctx.db.login({
|
||||
|
||||
@@ -7,7 +7,7 @@ import { cn } from "@/lib/utils";
|
||||
import { registerSchema, RegisterSchema } from "@/modules/auth/schemas";
|
||||
import { useTRPC } from "@/trpc/client";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useMutation } from "@tanstack/react-query";
|
||||
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
import { Poppins } from "next/font/google";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/navigation";
|
||||
@@ -22,11 +22,13 @@ const poppins = Poppins({
|
||||
export const SignUpView = () => {
|
||||
const router = useRouter();
|
||||
const trpc = useTRPC();
|
||||
const queryClient = useQueryClient();
|
||||
const register = useMutation(trpc.auth.register.mutationOptions({
|
||||
onError: (error) => {
|
||||
toast.error(error.message)
|
||||
},
|
||||
onSuccess: () => {
|
||||
onSuccess: async () => {
|
||||
await queryClient.invalidateQueries(trpc.auth.session.queryFilter())
|
||||
router.push('/');
|
||||
}
|
||||
}));
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
'use client';
|
||||
|
||||
import { useRef, useState } from 'react';
|
||||
import Link from 'next/link';
|
||||
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { cn } from '@/lib/utils';
|
||||
import useDropdownPosition from './use-dropdown-position';
|
||||
import { cn } from '@/lib/utils'
|
||||
import { CategoriesGetManyOutput } from '@/modules/categories/type';
|
||||
;
|
||||
import SubCategoryMenu from './subcategory-menu';
|
||||
|
||||
import Link from 'next/link';
|
||||
import { CategoriesGetManyOutput } from '@/modules/categories/type';
|
||||
|
||||
interface CategoryDropdownProps {
|
||||
category: CategoriesGetManyOutput[1];
|
||||
@@ -23,8 +23,6 @@ const CategoryDropdown = ({
|
||||
}: CategoryDropdownProps) => {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const dropdownRef = useRef<HTMLDivElement>(null);
|
||||
const { getDropdownPosition } = useDropdownPosition(dropdownRef);
|
||||
const dropdownPosition = getDropdownPosition();
|
||||
|
||||
// TODO: Will improve in mobile screen
|
||||
// const toggleDropdown = () => {
|
||||
@@ -77,7 +75,6 @@ const CategoryDropdown = ({
|
||||
<SubCategoryMenu
|
||||
category={category}
|
||||
isOpen={isOpen}
|
||||
position={dropdownPosition}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -6,17 +6,9 @@ import { CategoriesGetManyOutput } from '@/modules/categories/type';
|
||||
interface SubCategoryMenuProps {
|
||||
category: CategoriesGetManyOutput[1];
|
||||
isOpen: boolean;
|
||||
position: {
|
||||
top: number;
|
||||
left: number;
|
||||
};
|
||||
}
|
||||
|
||||
const SubCategoryMenu = ({
|
||||
category,
|
||||
isOpen,
|
||||
position,
|
||||
}: SubCategoryMenuProps) => {
|
||||
const SubCategoryMenu = ({ category, isOpen }: SubCategoryMenuProps) => {
|
||||
if (
|
||||
!isOpen ||
|
||||
!category.subcategories ||
|
||||
@@ -29,10 +21,10 @@ const SubCategoryMenu = ({
|
||||
|
||||
return (
|
||||
<div
|
||||
className="fixed z-100"
|
||||
className="absolute z-100"
|
||||
style={{
|
||||
top: position?.top,
|
||||
left: position?.left,
|
||||
top: '100%',
|
||||
left: 0,
|
||||
}}
|
||||
>
|
||||
{/* Invisible bridge to maintain hover */}
|
||||
|
||||
@@ -72,6 +72,7 @@ export interface Config {
|
||||
categories: Category;
|
||||
products: Product;
|
||||
tags: Tag;
|
||||
tenants: Tenant;
|
||||
'payload-locked-documents': PayloadLockedDocument;
|
||||
'payload-preferences': PayloadPreference;
|
||||
'payload-migrations': PayloadMigration;
|
||||
@@ -87,6 +88,7 @@ export interface Config {
|
||||
categories: CategoriesSelect<false> | CategoriesSelect<true>;
|
||||
products: ProductsSelect<false> | ProductsSelect<true>;
|
||||
tags: TagsSelect<false> | TagsSelect<true>;
|
||||
tenants: TenantsSelect<false> | TenantsSelect<true>;
|
||||
'payload-locked-documents': PayloadLockedDocumentsSelect<false> | PayloadLockedDocumentsSelect<true>;
|
||||
'payload-preferences': PayloadPreferencesSelect<false> | PayloadPreferencesSelect<true>;
|
||||
'payload-migrations': PayloadMigrationsSelect<false> | PayloadMigrationsSelect<true>;
|
||||
@@ -130,6 +132,13 @@ export interface UserAuthOperations {
|
||||
export interface User {
|
||||
id: string;
|
||||
username: string;
|
||||
roles?: ('super-admin' | 'user')[] | null;
|
||||
tenants?:
|
||||
| {
|
||||
tenant: string | Tenant;
|
||||
id?: string | null;
|
||||
}[]
|
||||
| null;
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
email: string;
|
||||
@@ -141,6 +150,29 @@ export interface User {
|
||||
lockUntil?: string | null;
|
||||
password?: string | null;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "tenants".
|
||||
*/
|
||||
export interface Tenant {
|
||||
id: string;
|
||||
/**
|
||||
* This is the name of the store (e.g. Nezumi's Store)
|
||||
*/
|
||||
name: string;
|
||||
/**
|
||||
* This is the subdomain for the store (e.g. [slug].funroad.com)
|
||||
*/
|
||||
slug: string;
|
||||
image?: (string | null) | Media;
|
||||
stripeAccountId: string;
|
||||
/**
|
||||
* You cannot create products until you submit your Stripe details
|
||||
*/
|
||||
stripeDetailsSubmitted?: boolean | null;
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "media".
|
||||
@@ -184,6 +216,7 @@ export interface Category {
|
||||
*/
|
||||
export interface Product {
|
||||
id: string;
|
||||
tenant?: (string | null) | Tenant;
|
||||
name: string;
|
||||
description?: string | null;
|
||||
/**
|
||||
@@ -234,6 +267,10 @@ export interface PayloadLockedDocument {
|
||||
| ({
|
||||
relationTo: 'tags';
|
||||
value: string | Tag;
|
||||
} | null)
|
||||
| ({
|
||||
relationTo: 'tenants';
|
||||
value: string | Tenant;
|
||||
} | null);
|
||||
globalSlug?: string | null;
|
||||
user: {
|
||||
@@ -283,6 +320,13 @@ export interface PayloadMigration {
|
||||
*/
|
||||
export interface UsersSelect<T extends boolean = true> {
|
||||
username?: T;
|
||||
roles?: T;
|
||||
tenants?:
|
||||
| T
|
||||
| {
|
||||
tenant?: T;
|
||||
id?: T;
|
||||
};
|
||||
updatedAt?: T;
|
||||
createdAt?: T;
|
||||
email?: T;
|
||||
@@ -329,6 +373,7 @@ export interface CategoriesSelect<T extends boolean = true> {
|
||||
* via the `definition` "products_select".
|
||||
*/
|
||||
export interface ProductsSelect<T extends boolean = true> {
|
||||
tenant?: T;
|
||||
name?: T;
|
||||
description?: T;
|
||||
price?: T;
|
||||
@@ -349,6 +394,19 @@ export interface TagsSelect<T extends boolean = true> {
|
||||
updatedAt?: T;
|
||||
createdAt?: T;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "tenants_select".
|
||||
*/
|
||||
export interface TenantsSelect<T extends boolean = true> {
|
||||
name?: T;
|
||||
slug?: T;
|
||||
image?: T;
|
||||
stripeAccountId?: T;
|
||||
stripeDetailsSubmitted?: T;
|
||||
updatedAt?: T;
|
||||
createdAt?: T;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "payload-locked-documents_select".
|
||||
|
||||
+12
-1
@@ -6,12 +6,14 @@ import path from 'path'
|
||||
import { buildConfig } from 'payload'
|
||||
import { fileURLToPath } from 'url'
|
||||
import sharp from 'sharp'
|
||||
import { multiTenantPlugin } from '@payloadcms/plugin-multi-tenant'
|
||||
|
||||
import { Users } from './collections/Users';
|
||||
import { Media } from './collections/Media'
|
||||
import { Categories } from './collections/Categories'
|
||||
import { Products } from './collections/Products'
|
||||
import { Tags } from './collections/Tags'
|
||||
import { Tenants } from './collections/Tenants'
|
||||
|
||||
const filename = fileURLToPath(import.meta.url)
|
||||
const dirname = path.dirname(filename)
|
||||
@@ -24,7 +26,7 @@ export default buildConfig({
|
||||
},
|
||||
},
|
||||
cookiePrefix: 'funroad',
|
||||
collections: [Users, Media, Categories, Products, Tags],
|
||||
collections: [Users, Media, Categories, Products, Tags, Tenants],
|
||||
editor: lexicalEditor(),
|
||||
secret: process.env.PAYLOAD_SECRET || '',
|
||||
typescript: {
|
||||
@@ -36,6 +38,15 @@ export default buildConfig({
|
||||
sharp,
|
||||
plugins: [
|
||||
payloadCloudPlugin(),
|
||||
multiTenantPlugin({
|
||||
collections: {
|
||||
products: {},
|
||||
},
|
||||
tenantsArrayField: {
|
||||
includeDefaultField: false,
|
||||
},
|
||||
userHasAccessToAllTenants: (user) => Boolean(user?.roles?.includes('super-admin')),
|
||||
})
|
||||
// storage-adapter-placeholder
|
||||
],
|
||||
})
|
||||
|
||||
+24
@@ -140,6 +140,30 @@ const categories = [
|
||||
const seed = async () => {
|
||||
const payload = await getPayload({ config });
|
||||
|
||||
const adminTenant = await payload.create({
|
||||
collection: 'tenants',
|
||||
data: {
|
||||
name: 'admin',
|
||||
slug: 'admin',
|
||||
stripeAccountId: 'admin',
|
||||
}
|
||||
})
|
||||
|
||||
await payload.create({
|
||||
collection: 'users',
|
||||
data: {
|
||||
email: 'admin@nezumi.pw',
|
||||
password: 'demo',
|
||||
roles: ['super-admin'],
|
||||
username: 'admin',
|
||||
tenants: [
|
||||
{
|
||||
tenant: adminTenant.id,
|
||||
}
|
||||
]
|
||||
}
|
||||
})
|
||||
|
||||
for (const category of categories) {
|
||||
const { name, slug, color } = category;
|
||||
const categoryDoc = await payload.create({
|
||||
|
||||
Reference in New Issue
Block a user