diff --git a/.gitignore b/.gitignore
index 5ef6a52..d86875d 100644
--- a/.gitignore
+++ b/.gitignore
@@ -39,3 +39,6 @@ yarn-error.log*
# typescript
*.tsbuildinfo
next-env.d.ts
+
+# idea
+.idea
diff --git a/package.json b/package.json
index 0b3c1fc..d312693 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,7 @@
{
"name": "multitenant-ecommerce",
"version": "0.1.0",
+ "type": "module",
"private": true,
"scripts": {
"dev": "next dev",
diff --git a/public/auth-bg.png b/public/auth-bg.png
new file mode 100644
index 0000000..09e7467
Binary files /dev/null and b/public/auth-bg.png differ
diff --git a/src/app/(app)/(auth)/sign-in/page.tsx b/src/app/(app)/(auth)/sign-in/page.tsx
new file mode 100644
index 0000000..3d9de2f
--- /dev/null
+++ b/src/app/(app)/(auth)/sign-in/page.tsx
@@ -0,0 +1,7 @@
+import { SignInView } from "@/modules/auth/ui/views/sign-in-view";
+
+const Page = () => {
+ return
+}
+
+export default Page;
diff --git a/src/app/(app)/(auth)/sign-up/page.tsx b/src/app/(app)/(auth)/sign-up/page.tsx
new file mode 100644
index 0000000..08a2016
--- /dev/null
+++ b/src/app/(app)/(auth)/sign-up/page.tsx
@@ -0,0 +1,7 @@
+import { SignUpView } from "@/modules/auth/ui/views/sign-up-view";
+
+const Page = () => {
+ return
+}
+
+export default Page;
diff --git a/src/app/(app)/(home)/navbar.tsx b/src/app/(app)/(home)/navbar.tsx
index 5635cce..77e34e5 100644
--- a/src/app/(app)/(home)/navbar.tsx
+++ b/src/app/(app)/(home)/navbar.tsx
@@ -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 (
-
- );
+ return (
+
+ );
};
const Navbar = () => {
- const pathname = usePathname();
- const [isSidebarOpen, setIsSidebarOpen] = useState(false);
+ const pathname = usePathname();
+ const [isSidebarOpen, setIsSidebarOpen] = useState(false);
- return (
-
+ );
};
export default Navbar;
diff --git a/src/app/(app)/(home)/page.tsx b/src/app/(app)/(home)/page.tsx
index d2fdc12..c68f1c1 100644
--- a/src/app/(app)/(home)/page.tsx
+++ b/src/app/(app)/(home)/page.tsx
@@ -1,3 +1,12 @@
+'use client';
+
+import { useQuery } from "@tanstack/react-query";
+
+import { useTRPC } from "@/trpc/client";
+
export default function Home() {
- return
Home page
;
+ const trpc = useTRPC();
+ const { data } = useQuery(trpc.auth.session.queryOptions());
+
+ return {JSON.stringify(data?.user, null, 2)}
;
}
diff --git a/src/app/(app)/layout.tsx b/src/app/(app)/layout.tsx
index c9e2b49..53406cd 100644
--- a/src/app/(app)/layout.tsx
+++ b/src/app/(app)/layout.tsx
@@ -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 (
-
-
- {children}
-
-
- );
+ return (
+
+
+
+ {children}
+
+
+
+
+ );
}
diff --git a/src/collections/Users.ts b/src/collections/Users.ts
index c683d0e..3d9a6cb 100644
--- a/src/collections/Users.ts
+++ b/src/collections/Users.ts
@@ -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'
+ }
],
}
diff --git a/src/modules/auth/constants.ts b/src/modules/auth/constants.ts
new file mode 100644
index 0000000..32e1c5f
--- /dev/null
+++ b/src/modules/auth/constants.ts
@@ -0,0 +1 @@
+export const AUTH_COOKIE = 'payload-token';
\ No newline at end of file
diff --git a/src/modules/auth/schemas.ts b/src/modules/auth/schemas.ts
new file mode 100644
index 0000000..68ef5dd
--- /dev/null
+++ b/src/modules/auth/schemas.ts
@@ -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;
+
+export const loginSchema = z.object({
+ email: z.string().email(),
+ password: z.string(),
+});
+
+export type LoginSchema = z.infer
\ No newline at end of file
diff --git a/src/modules/auth/server/procedures.ts b/src/modules/auth/server/procedures.ts
new file mode 100644
index 0000000..284d8c8
--- /dev/null
+++ b/src/modules/auth/server/procedures.ts
@@ -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;
+ })
+})
\ No newline at end of file
diff --git a/src/modules/auth/ui/views/sign-in-view.tsx b/src/modules/auth/ui/views/sign-in-view.tsx
new file mode 100644
index 0000000..2a411e7
--- /dev/null
+++ b/src/modules/auth/ui/views/sign-in-view.tsx
@@ -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({
+ mode: 'all',
+ resolver: zodResolver(loginSchema),
+ defaultValues: {
+ email: '',
+ password: '',
+ }
+ })
+
+ const onSubmit = (values: LoginSchema) => {
+ login.mutate(values);
+ }
+
+ return (
+
+ )
+}
\ No newline at end of file
diff --git a/src/modules/auth/ui/views/sign-up-view.tsx b/src/modules/auth/ui/views/sign-up-view.tsx
new file mode 100644
index 0000000..d56075c
--- /dev/null
+++ b/src/modules/auth/ui/views/sign-up-view.tsx
@@ -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({
+ 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 (
+
+ )
+}
\ No newline at end of file
diff --git a/src/payload-types.ts b/src/payload-types.ts
index 2af02a9..7885ecb 100644
--- a/src/payload-types.ts
+++ b/src/payload-types.ts
@@ -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 {
+ username?: T;
updatedAt?: T;
createdAt?: T;
email?: T;
diff --git a/src/payload.config.ts b/src/payload.config.ts
index f49fdf3..1487641 100644
--- a/src/payload.config.ts
+++ b/src/payload.config.ts
@@ -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'
diff --git a/src/trpc/routers/_app.ts b/src/trpc/routers/_app.ts
index 2e0ed93..f7ea56b 100644
--- a/src/trpc/routers/_app.ts
+++ b/src/trpc/routers/_app.ts
@@ -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