@@ -251,10 +270,6 @@ export default function LoginForm({ redirectTo }: { redirectTo?: string }) {
theme={theme.theme}
/>
- {/*
*/}
- {/*
- {DASHBOARD_PAGE_HEADER}, {profile.name ? profile.name.split(" ")[0] : ""}
-
-
*/}
-
- {/*
-
-
-
-
-
*/}
);
diff --git a/apps/web/app/(with-contexts)/dashboard/(sidebar)/pages/page.tsx b/apps/web/app/(with-contexts)/dashboard/(sidebar)/pages/page.tsx
index 4685b0d20..5ab09fe3a 100644
--- a/apps/web/app/(with-contexts)/dashboard/(sidebar)/pages/page.tsx
+++ b/apps/web/app/(with-contexts)/dashboard/(sidebar)/pages/page.tsx
@@ -4,7 +4,6 @@ import DashboardContent from "@components/admin/dashboard-content";
import LoadingScreen from "@components/admin/loading-screen";
import { ProfileContext } from "@components/contexts";
import { UIConstants } from "@courselit/common-models";
-import { checkPermission } from "@courselit/utils";
import { MANAGE_PAGES_PAGE_HEADING } from "@ui-config/strings";
import dynamic from "next/dynamic";
import { useContext } from "react";
@@ -17,15 +16,15 @@ const breadcrumbs = [{ label: MANAGE_PAGES_PAGE_HEADING, href: "#" }];
export default function Page() {
const { profile } = useContext(ProfileContext);
- if (
- !profile ||
- !checkPermission(profile.permissions!, [permissions.manageSite])
- ) {
+ if (!profile) {
return
;
}
return (
-
+
);
diff --git a/apps/web/app/(with-contexts)/dashboard/(sidebar)/settings/page.tsx b/apps/web/app/(with-contexts)/dashboard/(sidebar)/settings/page.tsx
index ad9772c38..1afc38e1e 100644
--- a/apps/web/app/(with-contexts)/dashboard/(sidebar)/settings/page.tsx
+++ b/apps/web/app/(with-contexts)/dashboard/(sidebar)/settings/page.tsx
@@ -5,7 +5,6 @@ import LoadingScreen from "@components/admin/loading-screen";
import Settings from "@components/admin/settings";
import { ProfileContext, SiteInfoContext } from "@components/contexts";
import { Profile, UIConstants } from "@courselit/common-models";
-import { checkPermission } from "@courselit/utils";
import { SITE_SETTINGS_PAGE_HEADING } from "@ui-config/strings";
import { useSearchParams } from "next/navigation";
import { useContext } from "react";
@@ -20,15 +19,15 @@ export default function Page() {
const tab = searchParams?.get("tab") || "Branding";
- if (
- !profile ||
- !checkPermission(profile.permissions!, [permissions.manageSettings])
- ) {
+ if (!profile) {
return ;
}
return (
-
+
+
{HEADER_HELP}
diff --git a/apps/web/app/(with-contexts)/dashboard/(sidebar)/users/users-hub.tsx b/apps/web/app/(with-contexts)/dashboard/(sidebar)/users/users-hub.tsx
index d44ecebf0..18d387671 100644
--- a/apps/web/app/(with-contexts)/dashboard/(sidebar)/users/users-hub.tsx
+++ b/apps/web/app/(with-contexts)/dashboard/(sidebar)/users/users-hub.tsx
@@ -29,7 +29,7 @@ import {
useToast,
Skeleton,
} from "@courselit/components-library";
-import { checkPermission, FetchBuilder } from "@courselit/utils";
+import { FetchBuilder } from "@courselit/utils";
import {
TOAST_TITLE_ERROR,
USER_TABLE_HEADER_COMMUNITIES,
@@ -141,12 +141,15 @@ export default function UsersHub() {
setPage(1);
}, []);
- if (!checkPermission(profile.permissions!, [permissions.manageUsers])) {
+ if (!profile) {
return ;
}
return (
-
+
{USERS_MANAGER_PAGE_HEADING}
diff --git a/apps/web/app/(with-contexts)/dashboard/layout.tsx b/apps/web/app/(with-contexts)/dashboard/layout.tsx
deleted file mode 100644
index 39439e010..000000000
--- a/apps/web/app/(with-contexts)/dashboard/layout.tsx
+++ /dev/null
@@ -1,16 +0,0 @@
-import React from "react";
-import { auth } from "@/auth";
-import { redirect } from "next/navigation";
-
-export default async function Layout({
- children,
-}: {
- children: React.ReactNode;
-}) {
- const session = await auth();
- if (!session) {
- redirect("/login?redirect=/dashboard");
- }
-
- return children;
-}
diff --git a/apps/web/app/(with-contexts)/dashboard/page.tsx b/apps/web/app/(with-contexts)/dashboard/page.tsx
new file mode 100644
index 000000000..92d70d583
--- /dev/null
+++ b/apps/web/app/(with-contexts)/dashboard/page.tsx
@@ -0,0 +1,16 @@
+import { redirect } from "next/navigation";
+import { getProfile } from "../action";
+import { Profile } from "@courselit/common-models";
+import { checkPermission } from "@courselit/utils";
+import { ADMIN_PERMISSIONS } from "@ui-config/constants";
+
+export default async function Page() {
+ const profile = (await getProfile()) as Profile;
+ if (checkPermission(profile.permissions, ADMIN_PERMISSIONS)) {
+ redirect("/dashboard/overview");
+ } else {
+ redirect("/dashboard/my-content");
+ }
+
+ return null;
+}
diff --git a/apps/web/app/(with-contexts)/layout-with-context.tsx b/apps/web/app/(with-contexts)/layout-with-context.tsx
index 9feeb8a07..7233ef713 100644
--- a/apps/web/app/(with-contexts)/layout-with-context.tsx
+++ b/apps/web/app/(with-contexts)/layout-with-context.tsx
@@ -94,7 +94,6 @@ export default function Layout(props: {
theme: Theme;
config: ServerConfig;
session: Session | null;
- // profile: Partial | null;
}) {
return (
@@ -102,7 +101,3 @@ export default function Layout(props: {
);
}
-
-// function formatHSL(hsl: HSL): string {
-// return `${hsl[0]} ${hsl[1]}% ${hsl[2]}%`;
-// }
diff --git a/apps/web/app/actions.ts b/apps/web/app/actions.ts
index 412b8ac05..6f89f4c0e 100644
--- a/apps/web/app/actions.ts
+++ b/apps/web/app/actions.ts
@@ -1,13 +1,13 @@
import { headers as headersType } from "next/headers";
-export const getBackendAddress = (
+export async function getBackendAddress(
headers: Headers,
-): `${string}://${string}` => {
+): Promise<`${string}://${string}`> {
return `${headers.get("x-forwarded-proto")}://${headers.get("host")}`;
-};
+}
export async function getAddressFromHeaders(headers: typeof headersType) {
const headersList = await headers();
- const address = getBackendAddress(headersList);
+ const address = await getBackendAddress(headersList);
return address;
}
diff --git a/apps/web/app/api/auth/code/generate/route.ts b/apps/web/app/api/auth/code/generate/route.ts
index 1c1443bcc..738565346 100644
--- a/apps/web/app/api/auth/code/generate/route.ts
+++ b/apps/web/app/api/auth/code/generate/route.ts
@@ -1,6 +1,6 @@
import { NextRequest } from "next/server";
import { responses } from "@/config/strings";
-import { generateUniquePasscode, hashCode } from "@/ui-lib/utils";
+import { generateUniquePasscode, hashCode } from "@/lib/utils";
import VerificationToken from "@/models/VerificationToken";
import pug from "pug";
import MagicCodeEmailTemplate from "@/templates/magic-code-email";
diff --git a/apps/web/app/verify-domain/route.ts b/apps/web/app/verify-domain/route.ts
index 816fb7e3b..1f170f778 100644
--- a/apps/web/app/verify-domain/route.ts
+++ b/apps/web/app/verify-domain/route.ts
@@ -1,11 +1,12 @@
import DomainModel, { Domain } from "../../models/Domain";
import { responses } from "../../config/strings";
-import constants from "../../config/constants";
+import constants from "@/config/constants";
import { isDateInFuture } from "../../lib/utils";
import { createUser } from "../../graphql/users/logic";
import { headers } from "next/headers";
import connectToDatabase from "../../services/db";
import { warn } from "@/services/logger";
+import SubscriberModel, { Subscriber } from "@models/Subscriber";
const { domainNameForSingleTenancy, schoolNameForSingleTenancy } = constants;
@@ -41,7 +42,7 @@ export async function GET(req: Request) {
await connectToDatabase();
- if (process.env.MULTITENANT === "true") {
+ if (constants.multitenant) {
const host = headerList.get("host");
if (!host) {
@@ -160,6 +161,9 @@ export async function GET(req: Request) {
domain: domain!,
email: domain!.email,
superAdmin: true,
+ name: constants.multitenant
+ ? await getSubscriberName(domain!.email)
+ : "",
});
await DomainModel.findOneAndUpdate(
{ _id: domain!._id },
@@ -181,3 +185,12 @@ export async function GET(req: Request) {
logo: domain!.settings?.logo?.file,
});
}
+
+async function getSubscriberName(email: string): Promise {
+ const subscriber = (await SubscriberModel.findOne(
+ { email },
+ { name: 1, _id: 0 },
+ ).lean()) as unknown as Subscriber;
+
+ return subscriber ? subscriber.name : "";
+}
diff --git a/apps/web/auth.ts b/apps/web/auth.ts
index 250bd732e..fb8ce5ceb 100644
--- a/apps/web/auth.ts
+++ b/apps/web/auth.ts
@@ -1,13 +1,15 @@
-import NextAuth from "next-auth";
+import NextAuth, { Session } from "next-auth";
import { z } from "zod";
import { authConfig } from "./auth.config";
import CredentialsProvider from "next-auth/providers/credentials";
import VerificationToken from "@models/VerificationToken";
-import User from "@models/User";
+import UserModel from "@models/User";
import { createUser } from "./graphql/users/logic";
-import { hashCode } from "@ui-lib/utils";
+import { hashCode } from "@/lib/utils";
import DomainModel, { Domain } from "@models/Domain";
import { error } from "./services/logger";
+import { User } from "next-auth";
+import { User as AppUser } from "@courselit/common-models";
export const { auth, signIn, signOut, handlers } = NextAuth({
...authConfig,
@@ -49,7 +51,7 @@ export const { auth, signIn, signOut, handlers } = NextAuth({
return null;
}
- let user = await User.findOne({
+ let user = await UserModel.findOne({
domain: domain._id,
email: sanitizedEmail,
});
@@ -66,12 +68,28 @@ export const { auth, signIn, signOut, handlers } = NextAuth({
if (!user.active) {
return null;
}
- return {
- id: user.userId,
- email: sanitizedEmail,
- name: user.name,
- };
+ return user;
},
}),
],
+ callbacks: {
+ jwt({ token, user }: { token: any; user?: User }) {
+ if (user) {
+ token.userId = (user as unknown as AppUser).userId;
+ token.domain = (user as any).domain.toString();
+ }
+ return token;
+ },
+ session({ session, token }: { session: Session; token: any }) {
+ if (session.user && token.userId) {
+ if (token.userId) {
+ (session.user as any).userId = token.userId;
+ }
+ if (token.domain) {
+ (session.user as any).domain = token.domain;
+ }
+ }
+ return session;
+ },
+ },
});
diff --git a/apps/web/components/admin/dashboard-content.tsx b/apps/web/components/admin/dashboard-content.tsx
index 96a7101d3..18b4c76cc 100644
--- a/apps/web/components/admin/dashboard-content.tsx
+++ b/apps/web/components/admin/dashboard-content.tsx
@@ -32,7 +32,7 @@ export default function DashboardContent({
}) {
const { profile } = useContext(ProfileContext);
- if (!profile.userId) {
+ if (!profile || !profile.userId) {
return ;
}
diff --git a/apps/web/components/admin/dashboard-skeleton/app-sidebar.tsx b/apps/web/components/admin/dashboard-skeleton/app-sidebar.tsx
index 04b5f80ec..bd2f832d1 100644
--- a/apps/web/components/admin/dashboard-skeleton/app-sidebar.tsx
+++ b/apps/web/components/admin/dashboard-skeleton/app-sidebar.tsx
@@ -32,6 +32,7 @@ import { ProfileContext, SiteInfoContext } from "@components/contexts";
import { checkPermission } from "@courselit/utils";
import { Profile, UIConstants } from "@courselit/common-models";
import {
+ GET_SET_UP,
MY_CONTENT_HEADER,
SIDEBAR_MENU_BLOGS,
SIDEBAR_MENU_MAILS,
@@ -41,7 +42,11 @@ import {
} from "@ui-config/strings";
import { NavSecondary } from "./nav-secondary";
import { usePathname, useSearchParams } from "next/navigation";
-import { ComponentProps, useContext } from "react";
+import { ComponentProps, useContext, useEffect, useState } from "react";
+import { CircularProgress } from "@components/circular-progress";
+import { hasPermissionToAccessSetupChecklist } from "@/lib/utils";
+import { ADMIN_PERMISSIONS } from "@ui-config/constants";
+import { getSetupChecklist } from "@/app/(with-contexts)/dashboard/(sidebar)/action";
const { permissions } = UIConstants;
export function AppSidebar({ ...props }: ComponentProps) {
@@ -50,9 +55,36 @@ export function AppSidebar({ ...props }: ComponentProps) {
const path = usePathname();
const searchParams = useSearchParams();
const tab = searchParams?.get("tab");
+ const [checklist, setChecklist] = useState([]);
+ const [totalChecklistItems, setTotalChecklistItems] = useState(0);
+
+ useEffect(() => {
+ const loadChecklist = async () => {
+ try {
+ const setupChecklist = await getSetupChecklist();
+ if (!setupChecklist) {
+ return;
+ }
+ setChecklist(setupChecklist.checklist);
+ setTotalChecklistItems(setupChecklist.total);
+ } catch (error) {}
+ };
+
+ if (
+ profile &&
+ profile.userId &&
+ hasPermissionToAccessSetupChecklist(profile.permissions!)
+ ) {
+ loadChecklist();
+ }
+ }, [profile]);
+
+ if (!profile) {
+ return null;
+ }
const { navMainItems, navProjectItems, navSecondaryItems } =
- getSidebarItems(profile, path, tab);
+ getSidebarItems({ profile, path, tab, checklist, totalChecklistItems });
return (
@@ -70,9 +102,6 @@ export function AppSidebar({ ...props }: ComponentProps) {
alt="logo"
/>
- {/*
-
-
*/}
{siteInfo.title}
@@ -96,7 +125,19 @@ export function AppSidebar({ ...props }: ComponentProps) {
);
}
-function getSidebarItems(profile: Partial, path, tab) {
+function getSidebarItems({
+ profile,
+ checklist = [],
+ totalChecklistItems = 0,
+ path,
+ tab,
+}: {
+ profile: Partial;
+ checklist: string[];
+ totalChecklistItems: number;
+ path?: string | null;
+ tab?: string | null;
+}) {
const navMainItems: any[] = [];
if (
@@ -110,7 +151,6 @@ function getSidebarItems(profile: Partial, path, tab) {
url: "/dashboard/overview",
icon: Target,
isActive: path === "/dashboard/overview",
- // items: [],
});
navMainItems.push({
title: "Products",
@@ -118,7 +158,7 @@ function getSidebarItems(profile: Partial, path, tab) {
icon: Box,
isActive:
path === "/dashboard/products" ||
- path.startsWith("/dashboard/product"),
+ path?.startsWith("/dashboard/product"),
items: [],
});
}
@@ -139,7 +179,7 @@ function getSidebarItems(profile: Partial, path, tab) {
icon: Text,
isActive:
path === "/dashboard/blogs" ||
- path.startsWith("/dashboard/blog"),
+ path?.startsWith("/dashboard/blog"),
items: [],
});
}
@@ -150,7 +190,7 @@ function getSidebarItems(profile: Partial, path, tab) {
icon: Globe,
isActive:
path === "/dashboard/pages" ||
- path.startsWith("/dashboard/page"),
+ path?.startsWith("/dashboard/page"),
items: [],
});
}
@@ -179,8 +219,8 @@ function getSidebarItems(profile: Partial, path, tab) {
url: "#",
icon: Mail,
isActive:
- path.startsWith("/dashboard/mails") ||
- path.startsWith("/dashboard/mail"),
+ path?.startsWith("/dashboard/mails") ||
+ path?.startsWith("/dashboard/mail"),
items: [
{
title: "Broadcasts",
@@ -242,14 +282,36 @@ function getSidebarItems(profile: Partial, path, tab) {
});
}
- const navSecondaryItems = [
- {
+ const navSecondaryItems: any[] = [];
+ if (
+ profile &&
+ profile.permissions &&
+ checkPermission(profile.permissions, ADMIN_PERMISSIONS)
+ ) {
+ if (totalChecklistItems && checklist.length) {
+ navSecondaryItems.push({
+ title: GET_SET_UP,
+ url: "/dashboard/get-set-up",
+ icon: (
+
+ ),
+ isActive: path === "/dashboard/get-set-up",
+ });
+ }
+ navSecondaryItems.push({
title: "Support",
url: "/dashboard/support",
- icon: LifeBuoy,
+ icon: ,
isActive: path === "/dashboard/support",
- },
- ];
+ });
+ }
const navProjectItems = [
{
name: MY_CONTENT_HEADER,
diff --git a/apps/web/components/admin/dashboard-skeleton/nav-secondary.tsx b/apps/web/components/admin/dashboard-skeleton/nav-secondary.tsx
index 4b446d1f1..29d857aad 100644
--- a/apps/web/components/admin/dashboard-skeleton/nav-secondary.tsx
+++ b/apps/web/components/admin/dashboard-skeleton/nav-secondary.tsx
@@ -1,5 +1,4 @@
import * as React from "react";
-import { type LucideIcon } from "lucide-react";
import {
SidebarGroup,
@@ -17,7 +16,7 @@ export function NavSecondary({
items: {
title: string;
url: string;
- icon: LucideIcon;
+ icon: any;
isActive?: boolean;
}[];
} & React.ComponentPropsWithoutRef) {
@@ -34,7 +33,7 @@ export function NavSecondary({
tooltip={item.title}
>
-
+ {item.icon}
{item.title}
diff --git a/apps/web/components/admin/dashboard/to-do.tsx b/apps/web/components/admin/dashboard/to-do.tsx
deleted file mode 100644
index b27f7671e..000000000
--- a/apps/web/components/admin/dashboard/to-do.tsx
+++ /dev/null
@@ -1,54 +0,0 @@
-"use client";
-
-import { SiteInfoContext } from "@components/contexts";
-import { Link } from "@courselit/components-library";
-import {
- SITE_SETTINGS_SECTION_GENERAL,
- SITE_SETTINGS_SECTION_PAYMENT,
-} from "@ui-config/strings";
-import { useContext } from "react";
-
-export default function Todo() {
- const siteinfo = useContext(SiteInfoContext);
-
- return (
-
- {(!siteinfo.title || (siteinfo.logo && !siteinfo.logo.file)) && (
-
-
- Basic details missing 💁♀️
-
-
- Give your school a proper name, description and a logo.
-
-
-
-
- Update now
-
-
-
-
- )}
- {(!siteinfo.currencyISOCode || !siteinfo.paymentMethod) && (
-
-
Start earning 💸
-
- Update your payment details to sell paid products.
-
-
-
-
- Update now
-
-
-
-
- )}
-
- );
-}
diff --git a/apps/web/components/admin/settings/index.tsx b/apps/web/components/admin/settings/index.tsx
index 04c839535..cd9eaf6f3 100644
--- a/apps/web/components/admin/settings/index.tsx
+++ b/apps/web/components/admin/settings/index.tsx
@@ -81,7 +81,6 @@ import {
CardTitle,
} from "@components/ui/card";
import { Copy, Info } from "lucide-react";
-import { Label } from "@components/ui/label";
import { Input } from "@components/ui/input";
import Resources from "@components/resources";
import { AddressContext } from "@components/contexts";
@@ -1068,7 +1067,6 @@ const Settings = (props: SettingsProps) => {
-
-
-
-
-
- copyToClipboard(
- `${address.backend}/api/payment/webhook-old`,
- )
- }
- >
-
-
-
-
{
+ value: number;
+ className?: string;
+ /**
+ * Matches lucide/shadcn pattern: viewBox stays 0 0 24 24 but rendered size can be
+ * controlled via `className` (e.g. "h-6 w-6") or overridden via this prop.
+ */
+ size?: number;
+ strokeWidth?: number;
+}
+
+// https://github.com/shadcn-ui/ui/issues/697
+// https://github.com/shadcn-ui/ui/issues/697#issuecomment-2621653578 CircularProgress
+
+function clamp(input: number, a: number, b: number): number {
+ return Math.max(Math.min(input, Math.max(a, b)), Math.min(a, b));
+}
+
+// fix to percentage values
+const total = 100;
+
+/**
+ * @see https://developer.mozilla.org/en-US/docs/Web/HTML/Element/progress
+ * @see https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/progressbar_role
+ */
+export const CircularProgress = ({
+ value,
+ className,
+ size = 24,
+ strokeWidth = 2,
+ ...restSvgProps
+}: ProgressCircleProps): any => {
+ const normalizedValue = clamp(value, 0, total);
+
+ // geometry is based on the viewBox size (default 24) to match lucide icons.
+ const radius = (size - strokeWidth) / 2;
+ const circumference = 2 * Math.PI * radius;
+ const progress = (normalizedValue / total) * circumference;
+ const halfSize = size / 2;
+
+ const commonParams = {
+ cx: halfSize,
+ cy: halfSize,
+ r: radius,
+ fill: "none",
+ strokeWidth,
+ };
+
+ return (
+ // biome-ignore lint/a11y/useFocusableInteractive: false positive (progress + progressbar are not focusable interactives)
+ // biome-ignore lint/nursery/useAriaPropsSupportedByRole: biome rule at odds with mdn docs (presumed nursary bug with rule)
+
+ );
+};
diff --git a/apps/web/config/constants.ts b/apps/web/config/constants.ts
index 9a843ff06..47c5e9f63 100644
--- a/apps/web/config/constants.ts
+++ b/apps/web/config/constants.ts
@@ -5,6 +5,7 @@ import { UIConstants } from "@courselit/common-models";
const { permissions } = UIConstants;
export default {
+ multitenant: process.env.MULTITENANT === "true",
domainNameForSingleTenancy: "main",
schoolNameForSingleTenancy: "My school",
dbConnectionString:
diff --git a/apps/web/graphql/courses/helpers.ts b/apps/web/graphql/courses/helpers.ts
index 107f79a0d..e5be7eae6 100644
--- a/apps/web/graphql/courses/helpers.ts
+++ b/apps/web/graphql/courses/helpers.ts
@@ -1,27 +1,13 @@
-import { getPaymentMethod } from "../../payments";
import { internal, responses } from "../../config/strings";
import GQLContext from "../../models/GQLContext";
-import CourseModel, { InternalCourse } from "../../models/Course";
+import CourseModel from "../../models/Course";
import constants from "../../config/constants";
-import { Progress } from "../../models/Progress";
-import { User } from "../../models/User";
import Page from "../../models/Page";
import slugify from "slugify";
import { addGroup } from "./logic";
-import { Constants, Course } from "@courselit/common-models";
+import { Constants, Course, Progress, User } from "@courselit/common-models";
import { getPlans } from "../paymentplans/logic";
-
-const validatePaymentMethod = async (domain: string) => {
- try {
- await getPaymentMethod(domain);
- } catch (err: any) {
- if (err.message === responses.update_payment_method) {
- throw err;
- } else {
- throw new Error(responses.internal_error);
- }
- }
-};
+import { InternalCourse } from "@courselit/common-logic";
export const validateCourse = async (
courseData: InternalCourse,
diff --git a/apps/web/graphql/courses/logic.ts b/apps/web/graphql/courses/logic.ts
index c731d1f26..39bb1a1db 100644
--- a/apps/web/graphql/courses/logic.ts
+++ b/apps/web/graphql/courses/logic.ts
@@ -162,7 +162,12 @@ export const createCourse = async (
ctx: GQLContext,
) => {
checkIfAuthenticated(ctx);
- if (!checkPermission(ctx.user.permissions, [permissions.manageCourse])) {
+ if (
+ !checkPermission(ctx.user.permissions, [
+ permissions.manageAnyCourse,
+ permissions.manageCourse,
+ ])
+ ) {
throw new Error(responses.action_not_allowed);
}
diff --git a/apps/web/graphql/paymentplans/logic.ts b/apps/web/graphql/paymentplans/logic.ts
index 9bd6c05a3..6a333f2cb 100644
--- a/apps/web/graphql/paymentplans/logic.ts
+++ b/apps/web/graphql/paymentplans/logic.ts
@@ -52,7 +52,10 @@ function checkEntityManagementPermission(
) {
if (entityType === membershipEntityType.COURSE) {
if (
- !checkPermission(ctx.user.permissions, [permissions.manageCourse])
+ !checkPermission(ctx.user.permissions, [
+ permissions.manageAnyCourse,
+ permissions.manageCourse,
+ ])
) {
throw new Error(responses.action_not_allowed);
}
diff --git a/apps/web/lib/utils.ts b/apps/web/lib/utils.ts
index 7142d7a2d..f488bbcb3 100644
--- a/apps/web/lib/utils.ts
+++ b/apps/web/lib/utils.ts
@@ -1,3 +1,6 @@
+import { UIConstants } from "@courselit/common-models";
+import { createHash, randomInt } from "crypto";
+
export const capitalize = (s: string) => {
if (typeof s !== "string") return "";
return s.charAt(0).toUpperCase() + s.slice(1);
@@ -43,3 +46,30 @@ export const generateEmailFrom = ({
}) => {
return `${name} <${email}>`;
};
+
+export const hasPermissionToAccessSetupChecklist = (
+ userPermissions: string[],
+) => {
+ const { permissions } = UIConstants;
+ const REQUIRED_PERMISSIONS_FOR_SETUP_CHECKLIST = [
+ permissions.manageAnyCourse,
+ permissions.manageSettings,
+ permissions.manageSite,
+ permissions.publishCourse,
+ ] as const;
+
+ return REQUIRED_PERMISSIONS_FOR_SETUP_CHECKLIST.every((perm) =>
+ userPermissions.includes(perm),
+ );
+};
+
+export function generateUniquePasscode() {
+ return randomInt(100000, 999999);
+}
+
+// Inspired from: https://github.com/nextauthjs/next-auth/blob/c4ad77b86762b7fd2e6362d8bf26c5953846774a/packages/next-auth/src/core/lib/utils.ts#L16
+export function hashCode(code: number) {
+ return createHash("sha256")
+ .update(`${code}${process.env.AUTH_SECRET}`)
+ .digest("hex");
+}
diff --git a/apps/web/middleware.ts b/apps/web/middleware.ts
index 1ed6a18d9..89f20c4cf 100644
--- a/apps/web/middleware.ts
+++ b/apps/web/middleware.ts
@@ -7,7 +7,7 @@ const { auth } = NextAuth(authConfig);
export default auth(async (request: NextRequest) => {
const requestHeaders = request.headers;
- const backend = getBackendAddress(requestHeaders);
+ const backend = await getBackendAddress(requestHeaders);
if (request.nextUrl.pathname === "/healthy") {
return Response.json({ success: true });
@@ -52,6 +52,20 @@ export default auth(async (request: NextRequest) => {
}
}
+ if (request.nextUrl.pathname.startsWith("/dashboard")) {
+ const session = await auth();
+ if (!session) {
+ return NextResponse.redirect(
+ new URL(
+ `/login?redirect=${encodeURIComponent(
+ request.nextUrl.pathname,
+ )}`,
+ request.url,
+ ),
+ );
+ }
+ }
+
return NextResponse.next({
request: {
headers: requestHeaders,
@@ -66,6 +80,12 @@ export default auth(async (request: NextRequest) => {
});
export const config = {
- matcher: ["/", "/favicon.ico", "/api/:path*", "/healthy"],
+ matcher: [
+ "/",
+ "/favicon.ico",
+ "/api/:path*",
+ "/healthy",
+ "/dashboard/:path*",
+ ],
unstable_allowDynamic: ["/node_modules/next-auth/**"],
};
diff --git a/apps/web/models/Subscriber.ts b/apps/web/models/Subscriber.ts
new file mode 100644
index 000000000..2cb11a11b
--- /dev/null
+++ b/apps/web/models/Subscriber.ts
@@ -0,0 +1,16 @@
+import mongoose from "mongoose";
+
+export interface Subscriber {
+ subscriberId: string;
+ name?: string;
+ email: string;
+}
+
+const SubscriberSchema = new mongoose.Schema({
+ subscriberId: { type: String, required: true, unique: true },
+ name: { type: String },
+ email: { type: String, required: true, unique: true },
+});
+
+export default mongoose.models.Subscriber ||
+ mongoose.model("Subscriber", SubscriberSchema);
diff --git a/apps/web/next-env.d.ts b/apps/web/next-env.d.ts
index 830fb594c..36a4fe488 100644
--- a/apps/web/next-env.d.ts
+++ b/apps/web/next-env.d.ts
@@ -1,5 +1,6 @@
///
///
+///
///
// NOTE: This file should not be edited
diff --git a/apps/web/package.json b/apps/web/package.json
index d2b8c9925..7c7607c45 100644
--- a/apps/web/package.json
+++ b/apps/web/package.json
@@ -20,7 +20,7 @@
"@courselit/page-primitives": "workspace:^",
"@courselit/utils": "workspace:^",
"@hookform/resolvers": "^3.9.1",
- "@radix-ui/react-alert-dialog": "^1.1.2",
+ "@radix-ui/react-alert-dialog": "^1.1.11",
"@radix-ui/react-avatar": "^1.1.3",
"@radix-ui/react-checkbox": "^1.1.4",
"@radix-ui/react-collapsible": "^1.1.3",
@@ -33,7 +33,7 @@
"@radix-ui/react-scroll-area": "^1.2.3",
"@radix-ui/react-select": "^2.1.6",
"@radix-ui/react-separator": "^1.1.2",
- "@radix-ui/react-slot": "^1.1.2",
+ "@radix-ui/react-slot": "^1.2.3",
"@radix-ui/react-switch": "^1.1.3",
"@radix-ui/react-tabs": "^1.1.3",
"@radix-ui/react-toast": "^1.2.6",
diff --git a/apps/web/ui-config/constants.ts b/apps/web/ui-config/constants.ts
index 750f56fd9..615dae446 100644
--- a/apps/web/ui-config/constants.ts
+++ b/apps/web/ui-config/constants.ts
@@ -1,6 +1,7 @@
/**
* This file provides application wide constants.
*/
+import { UIConstants } from "@courselit/common-models";
// Constants that represent types from the server
export const LESSON_TYPE_TEXT = "text";
@@ -47,3 +48,19 @@ export const TIME_RANGES = [
{ value: "1y", label: "1 year" },
{ value: "lifetime", label: "Lifetime" },
];
+
+const { permissions } = UIConstants;
+export const REQUIRED_PERMISSIONS_FOR_SETUP_CHECKLIST = [
+ permissions.manageAnyCourse,
+ permissions.manageSettings,
+ permissions.manageSite,
+] as const;
+
+export const ADMIN_PERMISSIONS = [
+ UIConstants.permissions.manageAnyCourse,
+ UIConstants.permissions.manageCourse,
+ UIConstants.permissions.manageUsers,
+ UIConstants.permissions.manageSite,
+ UIConstants.permissions.manageCommunity,
+ UIConstants.permissions.manageSettings,
+];
diff --git a/apps/web/ui-config/strings.ts b/apps/web/ui-config/strings.ts
index 98048dbea..477b98cad 100644
--- a/apps/web/ui-config/strings.ts
+++ b/apps/web/ui-config/strings.ts
@@ -596,7 +596,7 @@ export const APP_MESSAGE_MAIL_DELETED = "Mail deleted";
export const NEW_PAGE_FORM_WARNING =
"These settings cannot be changed later on, so proceed with caution.";
export const DASHBOARD_PAGE_HEADER = "Welcome";
-export const UNNAMED_USER = "Unnamed";
+export const UNNAMED_USER = "Stranger";
export const MAIL_REQUEST_FORM_REASON_FIELD = "Reason";
export const MAIL_REQUEST_FORM_REASON_PLACEHOLDER =
"Please be as detailed as possible. This will help us review your application better.";
@@ -646,3 +646,4 @@ export const HEADER_HELP = "Help";
export const CHECKOUT_PAGE_ORDER_SUMMARY = "Order summary";
export const TEXT_EDITOR_PLACEHOLDER = "Type here...";
export const BTN_VIEW_CERTIFICATE = "View certificate";
+export const GET_SET_UP = "Get set up";
diff --git a/apps/web/ui-lib/utils.ts b/apps/web/ui-lib/utils.ts
index bd642a35c..b1ae80e08 100644
--- a/apps/web/ui-lib/utils.ts
+++ b/apps/web/ui-lib/utils.ts
@@ -13,8 +13,6 @@ import type {
} from "@courselit/common-models";
import { checkPermission, FetchBuilder } from "@courselit/utils";
import { Constants, UIConstants } from "@courselit/common-models";
-import { createHash, randomInt } from "crypto";
-// import { headers as headersType } from "next/headers";
import { Theme } from "@courselit/page-models";
export { getPlanPrice } from "@courselit/utils";
const { permissions } = UIConstants;
@@ -294,17 +292,6 @@ export const moveMemberUp = (arr: any[], index: number) =>
export const moveMemberDown = (arr: any[], index: number) =>
swapMembers(arr, index, index + 1);
-export function generateUniquePasscode() {
- return randomInt(100000, 999999);
-}
-
-// Inspired from: https://github.com/nextauthjs/next-auth/blob/c4ad77b86762b7fd2e6362d8bf26c5953846774a/packages/next-auth/src/core/lib/utils.ts#L16
-export function hashCode(code: number) {
- return createHash("sha256")
- .update(`${code}${process.env.AUTH_SECRET}`)
- .digest("hex");
-}
-
export const sortCourseGroups = (course: Course) => {
return course.groups.sort((a: Group, b: Group) => a.rank - b.rank);
};
diff --git a/packages/common-models/src/profile.ts b/packages/common-models/src/profile.ts
index 25332a8c6..a85a9aa93 100644
--- a/packages/common-models/src/profile.ts
+++ b/packages/common-models/src/profile.ts
@@ -2,14 +2,14 @@ import { Media } from "./media";
import { Progress } from "./progress";
export default interface Profile {
- name: string;
+ name?: string;
id: string;
fetched: boolean;
purchases: Progress[];
email: string;
- bio: string;
+ bio?: string;
permissions: string[];
userId: string;
- subscribedToUpdates: string;
+ subscribedToUpdates: boolean;
avatar: Partial;
}
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index d227828be..5e622b3b1 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -219,7 +219,7 @@ importers:
specifier: ^3.9.1
version: 3.10.0(react-hook-form@7.56.1(react@18.3.1))
'@radix-ui/react-alert-dialog':
- specifier: ^1.1.2
+ specifier: ^1.1.11
version: 1.1.11(@types/react-dom@18.3.6(@types/react@18.3.20))(@types/react@18.3.20)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@radix-ui/react-avatar':
specifier: ^1.1.3
@@ -258,7 +258,7 @@ importers:
specifier: ^1.1.2
version: 1.1.4(@types/react-dom@18.3.6(@types/react@18.3.20))(@types/react@18.3.20)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@radix-ui/react-slot':
- specifier: ^1.1.2
+ specifier: ^1.2.3
version: 1.2.3(@types/react@18.3.20)(react@18.3.1)
'@radix-ui/react-switch':
specifier: ^1.1.3