mirror of
https://github.com/Nezumi-2711/multitenant-ecommerce.git
synced 2026-09-22 13:38:34 +00:00
Merge pull request #6 from Nezumi-2711/feat/impl-authentication-feature
Feature/ Add authentication for the web app
This commit is contained in:
@@ -39,3 +39,6 @@ yarn-error.log*
|
||||
# typescript
|
||||
*.tsbuildinfo
|
||||
next-env.d.ts
|
||||
|
||||
# idea
|
||||
.idea
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"name": "multitenant-ecommerce",
|
||||
"version": "0.1.0",
|
||||
"type": "module",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 736 KiB |
@@ -0,0 +1,7 @@
|
||||
import { SignInView } from "@/modules/auth/ui/views/sign-in-view";
|
||||
|
||||
const Page = () => {
|
||||
return <SignInView />
|
||||
}
|
||||
|
||||
export default Page;
|
||||
@@ -0,0 +1,7 @@
|
||||
import { SignUpView } from "@/modules/auth/ui/views/sign-up-view";
|
||||
|
||||
const Page = () => {
|
||||
return <SignUpView />
|
||||
}
|
||||
|
||||
export default Page;
|
||||
@@ -12,109 +12,109 @@ import { Button } from '@/components/ui/button';
|
||||
import NavbarSidebar from './navbar-sidebar';
|
||||
|
||||
const poppins = Poppins({
|
||||
subsets: ['latin'],
|
||||
weight: ['700'],
|
||||
subsets: ['latin'],
|
||||
weight: ['700'],
|
||||
});
|
||||
|
||||
interface NavbarItemProps {
|
||||
href: string;
|
||||
children: ReactNode;
|
||||
isActive?: boolean;
|
||||
href: string;
|
||||
children: ReactNode;
|
||||
isActive?: boolean;
|
||||
}
|
||||
|
||||
const navbarItems = [
|
||||
{
|
||||
href: '/',
|
||||
children: 'Home',
|
||||
},
|
||||
{
|
||||
href: '/about',
|
||||
children: 'About',
|
||||
},
|
||||
{
|
||||
href: '/features',
|
||||
children: 'Features',
|
||||
},
|
||||
{
|
||||
href: '/pricing',
|
||||
children: 'Pricing',
|
||||
},
|
||||
{
|
||||
href: '/contact',
|
||||
children: 'Contact',
|
||||
},
|
||||
{
|
||||
href: '/',
|
||||
children: 'Home',
|
||||
},
|
||||
{
|
||||
href: '/about',
|
||||
children: 'About',
|
||||
},
|
||||
{
|
||||
href: '/features',
|
||||
children: 'Features',
|
||||
},
|
||||
{
|
||||
href: '/pricing',
|
||||
children: 'Pricing',
|
||||
},
|
||||
{
|
||||
href: '/contact',
|
||||
children: 'Contact',
|
||||
},
|
||||
];
|
||||
|
||||
const NavbarItem = ({ href, children, isActive }: NavbarItemProps) => {
|
||||
return (
|
||||
<Button
|
||||
asChild
|
||||
variant="outline"
|
||||
className={cn(
|
||||
'bg-transparent hover:bg-transparent rounded-full hover:border-primary border-transparent px-3.5 text-lg',
|
||||
isActive && 'bg-black text-white hover:bg-black hover:text-white'
|
||||
)}
|
||||
>
|
||||
<Link href={href}>{children}</Link>
|
||||
</Button>
|
||||
);
|
||||
return (
|
||||
<Button
|
||||
asChild
|
||||
variant="outline"
|
||||
className={cn(
|
||||
'bg-transparent hover:bg-transparent rounded-full hover:border-primary border-transparent px-3.5 text-lg',
|
||||
isActive && 'bg-black text-white hover:bg-black hover:text-white'
|
||||
)}
|
||||
>
|
||||
<Link href={href}>{children}</Link>
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
|
||||
const Navbar = () => {
|
||||
const pathname = usePathname();
|
||||
const [isSidebarOpen, setIsSidebarOpen] = useState(false);
|
||||
const pathname = usePathname();
|
||||
const [isSidebarOpen, setIsSidebarOpen] = useState(false);
|
||||
|
||||
return (
|
||||
<nav className="h-20 flex border-b justify-between font-medium bg-white">
|
||||
<Link href="/" className="pl-6 flex items-center">
|
||||
return (
|
||||
<nav className="h-20 flex border-b justify-between font-medium bg-white">
|
||||
<Link href="/" className="pl-6 flex items-center">
|
||||
<span className={cn('text-5xl font-semibold', poppins.className)}>
|
||||
funroad
|
||||
</span>
|
||||
</Link>
|
||||
</Link>
|
||||
|
||||
<NavbarSidebar
|
||||
items={navbarItems}
|
||||
open={isSidebarOpen}
|
||||
onOpenChange={setIsSidebarOpen}
|
||||
/>
|
||||
<NavbarSidebar
|
||||
items={navbarItems}
|
||||
open={isSidebarOpen}
|
||||
onOpenChange={setIsSidebarOpen}
|
||||
/>
|
||||
|
||||
<div className="items-center gap-4 hidden lg:flex">
|
||||
{navbarItems.map((item) => (
|
||||
<NavbarItem
|
||||
key={item.href}
|
||||
href={item.href}
|
||||
isActive={pathname === item.href}
|
||||
>
|
||||
{item.children}
|
||||
</NavbarItem>
|
||||
))}
|
||||
</div>
|
||||
<div className="items-center gap-4 hidden lg:flex">
|
||||
{navbarItems.map((item) => (
|
||||
<NavbarItem
|
||||
key={item.href}
|
||||
href={item.href}
|
||||
isActive={pathname === item.href}
|
||||
>
|
||||
{item.children}
|
||||
</NavbarItem>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="hidden lg:flex">
|
||||
<Button
|
||||
asChild
|
||||
variant="secondary"
|
||||
className="border-l border-t-0 border-b-0 border-r-0 px-12 h-full rounded-none bg-white hover:bg-pink-400 transition-colors text-lg"
|
||||
>
|
||||
<Link href="/sign-in">Log in</Link>
|
||||
</Button>
|
||||
<div className="hidden lg:flex">
|
||||
<Button
|
||||
asChild
|
||||
variant="secondary"
|
||||
className="border-l border-t-0 border-b-0 border-r-0 px-12 h-full rounded-none bg-white hover:bg-pink-400 transition-colors text-lg"
|
||||
>
|
||||
<Link prefetch href="/sign-in">Log in</Link>
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
asChild
|
||||
variant="secondary"
|
||||
className="border-l border-t-0 border-b-0 border-r-0 px-12 h-full rounded-none bg-black text-white hover:bg-pink-400 hover:text-black transition-colors text-lg"
|
||||
>
|
||||
<Link href="/sign-up">Start Selling</Link>
|
||||
</Button>
|
||||
</div>
|
||||
<Button
|
||||
asChild
|
||||
variant="secondary"
|
||||
className="border-l border-t-0 border-b-0 border-r-0 px-12 h-full rounded-none bg-black text-white hover:bg-pink-400 hover:text-black transition-colors text-lg"
|
||||
>
|
||||
<Link prefetch href="/sign-up">Start Selling</Link>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className='flex lg:hidden items-center justify-center'>
|
||||
<Button variant="ghost" className="size-12 border-transparent bg-white" onClick={() => setIsSidebarOpen(true)}>
|
||||
<MenuIcon />
|
||||
<MenuIcon/>
|
||||
</Button>
|
||||
</div>
|
||||
</nav>
|
||||
);
|
||||
</nav>
|
||||
);
|
||||
};
|
||||
|
||||
export default Navbar;
|
||||
|
||||
@@ -1,3 +1,12 @@
|
||||
'use client';
|
||||
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
|
||||
import { useTRPC } from "@/trpc/client";
|
||||
|
||||
export default function Home() {
|
||||
return <div>Home page</div>;
|
||||
const trpc = useTRPC();
|
||||
const { data } = useQuery(trpc.auth.session.queryOptions());
|
||||
|
||||
return <div>{JSON.stringify(data?.user, null, 2)}</div>;
|
||||
}
|
||||
|
||||
+19
-12
@@ -1,29 +1,36 @@
|
||||
import type { Metadata } from 'next';
|
||||
import { DM_Sans } from 'next/font/google';
|
||||
import { ReactNode } from "react";
|
||||
|
||||
import { TRPCReactProvider } from '@/trpc/client';
|
||||
|
||||
// Components
|
||||
import { Toaster } from "@/components/ui/sonner";
|
||||
|
||||
import './globals.css';
|
||||
|
||||
const dmSans = DM_Sans({
|
||||
subsets: ['latin'],
|
||||
subsets: ['latin'],
|
||||
});
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'Create Next App',
|
||||
description: 'Generated by create next app',
|
||||
title: 'Create Next App',
|
||||
description: 'Generated by create next app',
|
||||
};
|
||||
|
||||
export default function RootLayout({
|
||||
children,
|
||||
children,
|
||||
}: Readonly<{
|
||||
children: React.ReactNode;
|
||||
children: ReactNode;
|
||||
}>) {
|
||||
return (
|
||||
<html lang="en">
|
||||
<body className={`${dmSans.className} antialiased`}>
|
||||
<TRPCReactProvider>{children}</TRPCReactProvider>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
return (
|
||||
<html lang='en'>
|
||||
<body className={`${dmSans.className} antialiased`}>
|
||||
<TRPCReactProvider>
|
||||
{children}
|
||||
<Toaster />
|
||||
</TRPCReactProvider>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -7,7 +7,11 @@ export const Users: CollectionConfig = {
|
||||
},
|
||||
auth: true,
|
||||
fields: [
|
||||
// Email added by default
|
||||
// Add more fields as needed
|
||||
{
|
||||
name: 'username',
|
||||
required: true,
|
||||
unique: true,
|
||||
type: 'text'
|
||||
}
|
||||
],
|
||||
}
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
export const AUTH_COOKIE = 'payload-token';
|
||||
@@ -0,0 +1,25 @@
|
||||
import { z } from "zod";
|
||||
|
||||
export const registerSchema = z.object({
|
||||
email: z.string().email(),
|
||||
password: z.string().min(3),
|
||||
username: z.string()
|
||||
.min(3, 'Username must be at lease 3 characters')
|
||||
.max(63, 'Username must be less than 63 characters')
|
||||
.regex(/^[a-z0-9][a-z0-9-]*[a-z0-9]$/,
|
||||
'Username can only contain lowercase letters, numbers and hyphens. It must start and end a letter or number'
|
||||
).refine(
|
||||
(val) => !val.includes('--'),
|
||||
'Username cannot contain consecutive hyphens'
|
||||
)
|
||||
.transform((val) => val.toLowerCase()),
|
||||
});
|
||||
|
||||
export type RegisterSchema = z.infer<typeof registerSchema>;
|
||||
|
||||
export const loginSchema = z.object({
|
||||
email: z.string().email(),
|
||||
password: z.string(),
|
||||
});
|
||||
|
||||
export type LoginSchema = z.infer<typeof loginSchema>
|
||||
@@ -0,0 +1,101 @@
|
||||
import { headers as getHeaders, cookies as getCookies } from "next/headers";
|
||||
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { AUTH_COOKIE } from "@/modules/auth/constants";
|
||||
import { loginSchema, registerSchema } from "@/modules/auth/schemas";
|
||||
import { baseProcedure, createTRPCRouter } from "@/trpc/init";
|
||||
|
||||
export const authRouter = createTRPCRouter({
|
||||
session: baseProcedure.query(async ({ ctx }) => {
|
||||
const headers = await getHeaders();
|
||||
|
||||
return await ctx.db.auth({ headers });
|
||||
}),
|
||||
logout: baseProcedure.mutation((async () => {
|
||||
const cookies = await getCookies();
|
||||
|
||||
cookies.delete(AUTH_COOKIE);
|
||||
})),
|
||||
register: baseProcedure
|
||||
.input(
|
||||
registerSchema
|
||||
)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const existingData = await ctx.db.find({
|
||||
collection: 'users',
|
||||
limit: 1,
|
||||
where: {
|
||||
username: { equals: input.username }
|
||||
}
|
||||
})
|
||||
|
||||
const existingUser = existingData.docs[0];
|
||||
|
||||
if (existingUser) {
|
||||
throw new TRPCError({
|
||||
code: 'BAD_REQUEST',
|
||||
message: 'Username already taken',
|
||||
})
|
||||
}
|
||||
|
||||
await ctx.db.create({
|
||||
collection: 'users',
|
||||
data: {
|
||||
email: input.email,
|
||||
username: input.username,
|
||||
password: input.password,
|
||||
}
|
||||
})
|
||||
|
||||
const data = await ctx.db.login({
|
||||
collection: 'users',
|
||||
data: {
|
||||
email: input.email,
|
||||
password: input.password,
|
||||
}
|
||||
})
|
||||
|
||||
if (!data.token) {
|
||||
throw new TRPCError({
|
||||
code: 'UNAUTHORIZED',
|
||||
message: 'Failed to login',
|
||||
})
|
||||
}
|
||||
|
||||
const cookies = await getCookies();
|
||||
cookies.set({
|
||||
name: AUTH_COOKIE,
|
||||
value: data.token,
|
||||
httpOnly: true,
|
||||
path: '/',
|
||||
});
|
||||
}),
|
||||
login: baseProcedure
|
||||
.input(loginSchema)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const data = await ctx.db.login({
|
||||
collection: 'users',
|
||||
data: {
|
||||
email: input.email,
|
||||
password: input.password,
|
||||
}
|
||||
})
|
||||
|
||||
if (!data.token) {
|
||||
throw new TRPCError({
|
||||
code: 'UNAUTHORIZED',
|
||||
message: 'Failed to login',
|
||||
})
|
||||
}
|
||||
|
||||
const cookies = await getCookies();
|
||||
cookies.set({
|
||||
name: AUTH_COOKIE,
|
||||
value: data.token,
|
||||
httpOnly: true,
|
||||
path: '/',
|
||||
});
|
||||
|
||||
return data;
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,106 @@
|
||||
'use client';
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { LoginSchema, loginSchema } from "@/modules/auth/schemas";
|
||||
import { useTRPC } from "@/trpc/client";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useMutation } from "@tanstack/react-query";
|
||||
import { Poppins } from "next/font/google";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
|
||||
const poppins = Poppins({
|
||||
subsets: ['latin'],
|
||||
weight: ['700']
|
||||
})
|
||||
|
||||
export const SignInView = () => {
|
||||
const router = useRouter();
|
||||
const trpc = useTRPC();
|
||||
const login = useMutation(trpc.auth.login.mutationOptions({
|
||||
onError: (error) => {
|
||||
toast.error(error.message)
|
||||
},
|
||||
onSuccess: () => {
|
||||
router.push('/');
|
||||
}
|
||||
}));
|
||||
const form = useForm<LoginSchema>({
|
||||
mode: 'all',
|
||||
resolver: zodResolver(loginSchema),
|
||||
defaultValues: {
|
||||
email: '',
|
||||
password: '',
|
||||
}
|
||||
})
|
||||
|
||||
const onSubmit = (values: LoginSchema) => {
|
||||
login.mutate(values);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='grid grid-cols-1 lg:grid-cols-5'>
|
||||
<div className='bg-[#F4F4F0] h-screen w-full lg:col-span-3 overflow-y-auto'>
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(onSubmit)} className='flex flex-col gap-8 p-4 lg:p-16'>
|
||||
<div className='flex items-center justify-between mb-8'>
|
||||
<Link href='/'>
|
||||
<span className={cn('text-2xl font-semibold', poppins.className)}>funroad</span>
|
||||
</Link>
|
||||
<Button asChild variant='ghost' size='sm' className='text-base border-none underline'>
|
||||
<Link prefetch href='/sign-up'>
|
||||
Sign up
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<h1 className='text-4xl font-medium'>
|
||||
Welcome back to Funroad.
|
||||
</h1>
|
||||
|
||||
<FormField
|
||||
name='email' render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel className='text-base'>Email</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
name='password' render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel className='text-base'>Password</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} type='password' />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<Button
|
||||
type='submit' size='lg' variant='elevated'
|
||||
className='bg-black text-white hover:bg-pink-400 hover:text-primary'
|
||||
disabled={login.isPending}
|
||||
>Log in</Button>
|
||||
</form>
|
||||
|
||||
</Form>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className='h-screen w-full lg:col-span-2 hidden lg:block'
|
||||
style={{ backgroundImage: "url('/auth-bg.png')", backgroundSize: 'cover', backgroundPosition: 'center' }}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
'use client';
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form";
|
||||
import { Input } from "@/components/ui/input";
|
||||
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 { Poppins } from "next/font/google";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
|
||||
const poppins = Poppins({
|
||||
subsets: ['latin'],
|
||||
weight: ['700']
|
||||
})
|
||||
|
||||
export const SignUpView = () => {
|
||||
const router = useRouter();
|
||||
const trpc = useTRPC();
|
||||
const register = useMutation(trpc.auth.register.mutationOptions({
|
||||
onError: (error) => {
|
||||
toast.error(error.message)
|
||||
},
|
||||
onSuccess: () => {
|
||||
router.push('/');
|
||||
}
|
||||
}));
|
||||
const form = useForm<RegisterSchema>({
|
||||
mode: 'all',
|
||||
resolver: zodResolver(registerSchema),
|
||||
defaultValues: {
|
||||
email: '',
|
||||
password: '',
|
||||
username: '',
|
||||
}
|
||||
})
|
||||
|
||||
const onSubmit = (values: RegisterSchema) => {
|
||||
register.mutate(values);
|
||||
}
|
||||
|
||||
const username = form.watch('username');
|
||||
const usernameErrors = form.formState.errors.username;
|
||||
const showPreview = username && !usernameErrors;
|
||||
|
||||
return (
|
||||
<div className='grid grid-cols-1 lg:grid-cols-5'>
|
||||
<div className='bg-[#F4F4F0] h-screen w-full lg:col-span-3 overflow-y-auto'>
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(onSubmit)} className='flex flex-col gap-8 p-4 lg:p-16'>
|
||||
<div className='flex items-center justify-between mb-8'>
|
||||
<Link href='/'>
|
||||
<span className={cn('text-2xl font-semibold', poppins.className)}>funroad</span>
|
||||
</Link>
|
||||
<Button asChild variant='ghost' size='sm' className='text-base border-none underline'>
|
||||
<Link prefetch href='/sign-in'>
|
||||
Sign in
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<h1 className='text-4xl font-medium'>
|
||||
Join over 1,580 creators earning money on Funroad.
|
||||
</h1>
|
||||
|
||||
<FormField
|
||||
name='username' render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel className='text-base'>Username</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
|
||||
<FormDescription className={cn('hidden', showPreview && 'block')}>
|
||||
Your store will be available at
|
||||
{/* TODO: Use proper method to generate the preview url */}
|
||||
<strong>{username}</strong>.shop.com
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
name='email' render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel className='text-base'>Email</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
name='password' render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel className='text-base'>Password</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} type='password' />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<Button
|
||||
type='submit' size='lg' variant='elevated'
|
||||
className='bg-black text-white hover:bg-pink-400 hover:text-primary'
|
||||
disabled={register.isPending}
|
||||
>Create account</Button>
|
||||
</form>
|
||||
|
||||
</Form>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className='h-screen w-full lg:col-span-2 hidden lg:block'
|
||||
style={{ backgroundImage: "url('/auth-bg.png')", backgroundSize: 'cover', backgroundPosition: 'center' }}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -125,6 +125,7 @@ export interface UserAuthOperations {
|
||||
*/
|
||||
export interface User {
|
||||
id: string;
|
||||
username: string;
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
email: string;
|
||||
@@ -239,6 +240,7 @@ export interface PayloadMigration {
|
||||
* via the `definition` "users_select".
|
||||
*/
|
||||
export interface UsersSelect<T extends boolean = true> {
|
||||
username?: T;
|
||||
updatedAt?: T;
|
||||
createdAt?: T;
|
||||
email?: T;
|
||||
|
||||
@@ -7,7 +7,7 @@ import { buildConfig } from 'payload'
|
||||
import { fileURLToPath } from 'url'
|
||||
import sharp from 'sharp'
|
||||
|
||||
import { Users } from './collections/Users'
|
||||
import { Users } from './collections/Users';
|
||||
import { Media } from './collections/Media'
|
||||
import { Categories } from './collections/Categories'
|
||||
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
import { authRouter } from "@/modules/auth/server/procedures";
|
||||
import { categoriesRouter } from '@/modules/categories/server/procedures';
|
||||
|
||||
|
||||
import { createTRPCRouter } from '../init';
|
||||
|
||||
export const appRouter = createTRPCRouter({
|
||||
auth: authRouter,
|
||||
categories: categoriesRouter,
|
||||
});
|
||||
// export type definition of API
|
||||
|
||||
Reference in New Issue
Block a user