Skip to content

Commit

Permalink
feat(refactor): add user roles & admin panel with major code changes
Browse files Browse the repository at this point in the history
  • Loading branch information
mickasmt committed Jun 26, 2024
1 parent 15c719c commit e175f87
Show file tree
Hide file tree
Showing 37 changed files with 2,090 additions and 836 deletions.
9 changes: 5 additions & 4 deletions actions/generate-user-stripe.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,13 @@ export async function generateUserStripe(priceId: string): Promise<responseActio

try {
const session = await auth()
const user = session?.user;

if (!session?.user || !session?.user.email) {
if (!user || !user.email || !user.id) {
throw new Error("Unauthorized");
}

const subscriptionPlan = await getUserSubscriptionPlan(session.user.id)
const subscriptionPlan = await getUserSubscriptionPlan(user.id)

if (subscriptionPlan.isPaid && subscriptionPlan.stripeCustomerId) {
// User on Paid Plan - Create a portal session to manage subscription.
Expand All @@ -42,15 +43,15 @@ export async function generateUserStripe(priceId: string): Promise<responseActio
payment_method_types: ["card"],
mode: "subscription",
billing_address_collection: "auto",
customer_email: session.user.email,
customer_email: user.email,
line_items: [
{
price: priceId,
quantity: 1,
},
],
metadata: {
userId: session.user.id,
userId: user.id,
},
})

Expand Down
40 changes: 40 additions & 0 deletions actions/update-user-role.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
"use server";

import { revalidatePath } from "next/cache";
import { auth } from "@/auth";
import { UserRole } from "@prisma/client";

import { prisma } from "@/lib/db";
import { userRoleSchema } from "@/lib/validations/user";

export type FormData = {
role: UserRole;
};

export async function updateUserRole(userId: string, data: FormData) {
try {
const session = await auth();

if (!session?.user || session?.user.id !== userId) {
throw new Error("Unauthorized");
}

const { role } = userRoleSchema.parse(data);

// Update the user role.
await prisma.user.update({
where: {
id: userId,
},
data: {
role: role,
},
});

revalidatePath("/dashboard/settings");
return { status: "success" };
} catch (error) {
// console.log(error)
return { status: "error" };
}
}
17 changes: 14 additions & 3 deletions app/(auth)/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,18 @@
import { redirect } from "next/navigation";

import { getCurrentUser } from "@/lib/session";

interface AuthLayoutProps {
children: React.ReactNode
children: React.ReactNode;
}

export default function AuthLayout({ children }: AuthLayoutProps) {
return <div className="min-h-screen">{children}</div>
export default async function AuthLayout({ children }: AuthLayoutProps) {
const user = await getCurrentUser();

if (user) {
if (user.role === "ADMIN") redirect("/admin");
redirect("/dashboard");
}

return <div className="min-h-screen">{children}</div>;
}
20 changes: 10 additions & 10 deletions app/(auth)/login/page.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
import { Metadata } from "next"
import Link from "next/link"
import { Suspense } from "react";
import { Metadata } from "next";
import Link from "next/link";

import { cn } from "@/lib/utils"
import { buttonVariants } from "@/components/ui/button"
import { Icons } from "@/components/shared/icons"
import { UserAuthForm } from "@/components/forms/user-auth-form"
import { Suspense } from "react"
import { cn } from "@/lib/utils";
import { buttonVariants } from "@/components/ui/button";
import { UserAuthForm } from "@/components/forms/user-auth-form";
import { Icons } from "@/components/shared/icons";

export const metadata: Metadata = {
title: "Login",
description: "Login to your account",
}
};

export default function LoginPage() {
return (
Expand All @@ -19,7 +19,7 @@ export default function LoginPage() {
href="/"
className={cn(
buttonVariants({ variant: "outline", size: "sm" }),
"absolute left-4 top-4 md:left-8 md:top-8"
"absolute left-4 top-4 md:left-8 md:top-8",
)}
>
<>
Expand Down Expand Up @@ -50,5 +50,5 @@ export default function LoginPage() {
</p>
</div>
</div>
)
);
}
2 changes: 1 addition & 1 deletion app/(marketing)/pricing/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ export default async function PricingPage() {
const user = await getCurrentUser();
let subscriptionPlan;

if (user) {
if (user && user.id) {
subscriptionPlan = await getUserSubscriptionPlan(user.id);
}

Expand Down
32 changes: 32 additions & 0 deletions app/(protected)/admin/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { redirect } from "next/navigation";

import { getCurrentUser } from "@/lib/session";
import { constructMetadata } from "@/lib/utils";
import { DashboardHeader } from "@/components/dashboard/header";
import InfoCard from "@/components/dashboard/info-card";
import { DashboardShell } from "@/components/dashboard/shell";
import TransactionsList from "@/components/dashboard/transactions-list";

export const metadata = constructMetadata({
title: "Admin – SaaS Starter",
description: "Admin page for only admin management.",
});

export default async function AdminPage() {
const user = await getCurrentUser();
if (!user || user.role !== "ADMIN") redirect("/login");

return (
<DashboardShell>
<DashboardHeader heading="Admin" text="Access only for admin." />
<div className="flex flex-col gap-5">
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3">
<InfoCard />
<InfoCard />
<InfoCard />
</div>
<TransactionsList />
</div>
</DashboardShell>
);
}
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import { redirect } from "next/navigation";

import { BillingInfo } from "@/components/pricing/billing-info";
import { DashboardHeader } from "@/components/dashboard/header";
import { DashboardShell } from "@/components/dashboard/shell";
import { Icons } from "@/components/shared/icons";
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
import { getCurrentUser } from "@/lib/session";
import { getUserSubscriptionPlan } from "@/lib/subscription";
import { constructMetadata } from "@/lib/utils";
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
import { DashboardHeader } from "@/components/dashboard/header";
import { DashboardShell } from "@/components/dashboard/shell";
import { BillingInfo } from "@/components/pricing/billing-info";
import { Icons } from "@/components/shared/icons";

export const metadata = constructMetadata({
title: "Billing – SaaS Starter",
Expand All @@ -17,12 +17,13 @@ export const metadata = constructMetadata({
export default async function BillingPage() {
const user = await getCurrentUser();

if (!user) {
let userSubscriptionPlan;
if (user && user.id && user.role === "USER") {
userSubscriptionPlan = await getUserSubscriptionPlan(user.id);
} else {
redirect("/login");
}

const userSubscriptionPlan = await getUserSubscriptionPlan(user.id);

return (
<DashboardShell>
<DashboardHeader
Expand All @@ -33,7 +34,7 @@ export default async function BillingPage() {
<Alert className="!pl-14">
<Icons.warning />
<AlertTitle>This is a demo app.</AlertTitle>
<AlertDescription>
<AlertDescription className="text-balance">
SaaS Starter app is a demo app using a Stripe test environment. You
can find a list of test card numbers on the{" "}
<a
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,10 @@ import { DashboardShell } from "@/components/dashboard/shell";
export default function DashboardLoading() {
return (
<DashboardShell>
<DashboardHeader heading="Posts" text="Create and manage posts." />
<DashboardHeader
heading="Panel"
text="Current Role :"
/>
<div className="divide-border-200 divide-y rounded-md border">
<Skeleton className="h-[400px] w-full rounded-lg" />
</div>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,27 +1,24 @@
import { redirect } from "next/navigation";

import { getCurrentUser } from "@/lib/session";
import { constructMetadata } from "@/lib/utils";
import { Button } from "@/components/ui/button";
import { DashboardHeader } from "@/components/dashboard/header";
import { DashboardShell } from "@/components/dashboard/shell";
import { EmptyPlaceholder } from "@/components/shared/empty-placeholder";
import { Button } from "@/components/ui/button";
import { getCurrentUser } from "@/lib/session";
import { constructMetadata } from "@/lib/utils";

export const metadata = constructMetadata({
title: "Settings – SaaS Starter",
description: "Overview of your account and activities.",
title: "Panel – SaaS Starter",
description: "Create and manage content.",
});

export default async function DashboardPage() {
const user = await getCurrentUser();

if (!user) {
redirect("/login");
}

return (
<DashboardShell>
<DashboardHeader heading="Panel" text="Create and manage content." />
<DashboardHeader
heading="Panel"
text={`Current Role : ${user?.role} — Change your role in settings.`}
/>
<div>
<EmptyPlaceholder>
<EmptyPlaceholder.Icon name="post" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export default function DashboardSettingsLoading() {
<div className="grid gap-6">
<CardSkeleton />
<CardSkeleton />
<CardSkeleton />
</div>
</DashboardShell>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { DeleteAccountSection } from "@/components/dashboard/delete-account";
import { DashboardHeader } from "@/components/dashboard/header";
import { DashboardShell } from "@/components/dashboard/shell";
import { UserNameForm } from "@/components/forms/user-name-form";
import { UserRoleForm } from "@/components/forms/user-role-form";

export const metadata = constructMetadata({
title: "Settings – SaaS Starter",
Expand All @@ -15,9 +16,7 @@ export const metadata = constructMetadata({
export default async function SettingsPage() {
const user = await getCurrentUser();

if (!user) {
redirect("/login");
}
if (!user?.id) redirect("/login");

return (
<DashboardShell>
Expand All @@ -27,6 +26,7 @@ export default async function SettingsPage() {
/>
<div className="grid gap-6">
<UserNameForm user={{ id: user.id, name: user.name || "" }} />
<UserRoleForm user={{ id: user.id, role: user.role }} />
<DeleteAccountSection />
</div>
</DashboardShell>
Expand Down
26 changes: 21 additions & 5 deletions app/(dashboard)/layout.tsx → app/(protected)/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,22 +1,38 @@
import { redirect } from "next/navigation";

import { dashboardConfig } from "@/config/dashboard";
import { getCurrentUser } from "@/lib/session";
import { DashboardNav } from "@/components/layout/dashboard-sidenav";
import { NavBar } from "@/components/layout/navbar";
import { SiteFooter } from "@/components/layout/site-footer";
import MaxWidthWrapper from "@/components/shared/max-width-wrapper";
import { dashboardConfig } from "@/config/dashboard";

interface DashboardLayoutProps {
import { adminConfig } from "../../config/admin";

interface ProtectedLayoutProps {
children?: React.ReactNode;
}

export default function DashboardLayout({ children }: DashboardLayoutProps) {
export default async function ProtectedLayout({
children,
}: ProtectedLayoutProps) {
const user = await getCurrentUser();

if (!user) redirect("/login");

return (
<div className="flex min-h-screen flex-col space-y-6">
<NavBar />

<MaxWidthWrapper className="min-h-svh">
<div className="grid flex-1 gap-12 md:grid-cols-[200px_1fr]">
<aside className="hidden w-[200px] flex-col md:flex">
<DashboardNav items={dashboardConfig.sidebarNav} />
<DashboardNav
items={
user.role === "ADMIN"
? adminConfig.sidebarNav
: dashboardConfig.sidebarNav
}
/>
</aside>
<main className="flex w-full flex-1 flex-col overflow-hidden">
{children}
Expand Down
Loading

0 comments on commit e175f87

Please sign in to comment.