First Commit
This commit is contained in:
Executable
+21
@@ -0,0 +1,21 @@
|
||||
import React from "react";
|
||||
import type { AppContextObject } from "@/src/types";
|
||||
import { AppContext } from "../__root";
|
||||
import Button from "@/src/components/twui/layout/Button";
|
||||
import Row from "@/src/components/twui/layout/Row";
|
||||
|
||||
export default function CTAButtons() {
|
||||
const appContext = React.useContext<AppContextObject>(AppContext);
|
||||
const { appState, user } = appContext;
|
||||
|
||||
/**
|
||||
* Render component
|
||||
*/
|
||||
return (
|
||||
<Row className="w-full flex-col md:flex-row items-stretch">
|
||||
<Button title="Login" className="w-full" href="/auth/login">
|
||||
Login
|
||||
</Button>
|
||||
</Row>
|
||||
);
|
||||
}
|
||||
Executable
+30
@@ -0,0 +1,30 @@
|
||||
import React from "react";
|
||||
import type { AppContextObject } from "@/src/types";
|
||||
import { PartyPopper } from "lucide-react";
|
||||
import { AppContext } from "../__root";
|
||||
import Span from "@/src/components/twui/layout/Span";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
import Row from "@/src/components/twui/layout/Row";
|
||||
|
||||
export default function HeroBanner() {
|
||||
const appContext = React.useContext<AppContextObject>(AppContext);
|
||||
|
||||
/**
|
||||
* Render component
|
||||
*/
|
||||
return (
|
||||
<a
|
||||
href="/about"
|
||||
className={twMerge(
|
||||
"flex items-center gap-4 bg-accent text-dark/70! px-4 py-2",
|
||||
"font-bold uppercase tracking-widest text-sm sm:text-base",
|
||||
"rounded-default w-full md:w-auto",
|
||||
)}
|
||||
>
|
||||
<Row className="justify-center md:justify-start w-full">
|
||||
<PartyPopper size={30} />
|
||||
<Span>Celebrating 10 Years of Charity</Span>
|
||||
</Row>
|
||||
</a>
|
||||
);
|
||||
}
|
||||
Executable
+44
@@ -0,0 +1,44 @@
|
||||
import React from "react";
|
||||
import CTAButtons from "../(partials)/cta-buttons";
|
||||
import MainHeroSection from "@/src/components/general/main-hero-section";
|
||||
import Img from "@/src/components/twui/layout/Img";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
export default function Hero() {
|
||||
return (
|
||||
<React.Fragment>
|
||||
<MainHeroSection
|
||||
title="Wireguard UI"
|
||||
sub_title="The ultimate Wireguard Dashboard"
|
||||
cta_buttons={
|
||||
<>
|
||||
<CTAButtons />
|
||||
</>
|
||||
}
|
||||
img_props={{
|
||||
id: "hero-image",
|
||||
}}
|
||||
img_component={
|
||||
<>
|
||||
<Img
|
||||
alt="bg image"
|
||||
src={"/images/susan_brown_and_yanya_1-10.webp"}
|
||||
className={twMerge(
|
||||
`hidden lg:flex absolute top-0 left-0 w-full h-full object-cover`,
|
||||
"z-0 object-top",
|
||||
)}
|
||||
/>
|
||||
<Img
|
||||
alt="bg image"
|
||||
src={"/images/susan_brown_and_yanya_1-6.webp"}
|
||||
className={twMerge(
|
||||
`flex lg:hidden absolute top-0 left-0 w-full h-full object-cover`,
|
||||
"z-0",
|
||||
)}
|
||||
/>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
import type { BunextPageModuleMeta } from "@moduletrace/bunext/types";
|
||||
import Section from "@/src/components/twui/layout/Section";
|
||||
import Container from "@/src/components/twui/layout/Container";
|
||||
import Stack from "@/src/components/twui/layout/Stack";
|
||||
import H1 from "@/src/components/twui/layout/H1";
|
||||
import Span from "@/src/components/twui/layout/Span";
|
||||
import Button from "@/src/components/twui/layout/Button";
|
||||
import { Home } from "lucide-react";
|
||||
import Divider from "../components/twui/layout/Divider";
|
||||
import { SiteData } from "../data/site-data";
|
||||
|
||||
export default function NotFoundPage() {
|
||||
return (
|
||||
<>
|
||||
<Section className="h-[130px] flex items-center bg-dark text-white p-0!"></Section>
|
||||
<Section className="min-h-[60vh] flex items-center">
|
||||
<Container>
|
||||
<Stack className="w-full items-center text-center gap-6">
|
||||
<Span className="text-8xl font-bold text-primary">
|
||||
404
|
||||
</Span>
|
||||
<H1 className="text-center">Page Not Found</H1>
|
||||
<Span className="text-xl max-w-md text-center opacity-70">
|
||||
The page you are looking for does not exist or has
|
||||
been moved.
|
||||
</Span>
|
||||
<Button
|
||||
title="Go Home"
|
||||
href="/"
|
||||
beforeIcon={<Home size={17} />}
|
||||
>
|
||||
Back to Home
|
||||
</Button>
|
||||
</Stack>
|
||||
</Container>
|
||||
</Section>
|
||||
<Divider />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export const meta: BunextPageModuleMeta = {
|
||||
title: `404 | ${SiteData["SiteName"]}`,
|
||||
description: "Page not found",
|
||||
};
|
||||
@@ -0,0 +1,44 @@
|
||||
import type { BunextPageServerFn } from "@moduletrace/bunext/types";
|
||||
import type { PagePropsType } from "../types";
|
||||
import userAuth from "../functions/backend/auth/user-auth";
|
||||
|
||||
const server: BunextPageServerFn<PagePropsType> = async ({ req, url }) => {
|
||||
const { user, user_types } = await userAuth({ req });
|
||||
|
||||
if (url.pathname.startsWith("/admin") && !user?.logged_in_status) {
|
||||
return {
|
||||
redirect: {
|
||||
destination: "/auth/login",
|
||||
permanent: false,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
if (
|
||||
url.pathname.startsWith("/auth") &&
|
||||
!url.pathname.match(/logout/) &&
|
||||
user?.logged_in_status
|
||||
) {
|
||||
return {
|
||||
redirect: {
|
||||
destination: "/admin",
|
||||
permanent: false,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
props: {
|
||||
environment: process.env.NODE_ENV,
|
||||
envs: {
|
||||
GOOGLE_CLIENT_ID: process.env.GOOGLE_CLIENT_ID || "",
|
||||
R2_PUBLIC_DOMAIN: process.env.R2_PUBLIC_DOMAIN || "",
|
||||
PAYSTACK_PUBLIC_KEY: process.env.PAYSTACK_PUBLIC_KEY || "",
|
||||
},
|
||||
user,
|
||||
user_types,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export default server;
|
||||
Executable
+89
@@ -0,0 +1,89 @@
|
||||
// @ts-ignore
|
||||
import "../styles/main.css";
|
||||
|
||||
import React from "react";
|
||||
import type {
|
||||
BunextPageHeadFCProps,
|
||||
BunextRootComponentProps,
|
||||
} from "@moduletrace/bunext/types";
|
||||
|
||||
import type { AppContextObject, AppStateObject } from "@/src/types";
|
||||
import Layout from "../layouts/main";
|
||||
import AdminLayout from "../layouts/admin";
|
||||
import LoginShell from "../components/general/login-shell";
|
||||
|
||||
export const AppContext = React.createContext<AppContextObject>(
|
||||
{} as AppContextObject,
|
||||
);
|
||||
|
||||
/**
|
||||
* # NextApp App Root
|
||||
* @description Root component for NextApp App (Next.js)
|
||||
*/
|
||||
export default function SBFApp({
|
||||
children,
|
||||
props,
|
||||
url,
|
||||
query,
|
||||
}: BunextRootComponentProps) {
|
||||
const [refresh, setRefresh] = React.useState(0);
|
||||
const [appState, setAppState] = React.useState<AppStateObject>({
|
||||
likes: null,
|
||||
});
|
||||
|
||||
const appContextObject: AppContextObject = {
|
||||
user: null,
|
||||
appState,
|
||||
setAppState,
|
||||
refresh,
|
||||
setRefresh,
|
||||
pageProps: { ...props, url },
|
||||
query,
|
||||
};
|
||||
|
||||
if (url?.pathname.startsWith("/admin")) {
|
||||
return (
|
||||
<AppContext.Provider value={appContextObject}>
|
||||
<AdminLayout>{children}</AdminLayout>
|
||||
</AppContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
if (url?.pathname.startsWith("/auth")) {
|
||||
return (
|
||||
<AppContext.Provider value={appContextObject}>
|
||||
<LoginShell>{children}</LoginShell>
|
||||
</AppContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<AppContext.Provider value={appContextObject}>
|
||||
<Layout
|
||||
// no_header_divider={
|
||||
// url?.pathname ? ["/"].includes(url.pathname) : undefined
|
||||
// }
|
||||
blank={
|
||||
url?.pathname
|
||||
? ["/auth/logout"].includes(url.pathname)
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
{children}
|
||||
</Layout>
|
||||
</AppContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export function Head({ serverRes, ctx }: BunextPageHeadFCProps) {
|
||||
return (
|
||||
<>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link
|
||||
rel="preconnect"
|
||||
href="https://fonts.gstatic.com"
|
||||
crossOrigin=""
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
import type { BunextPageModuleMeta } from "@moduletrace/bunext/types";
|
||||
import InnerPageHeroSection from "@/src/components/general/inner-page-hero-section";
|
||||
import { SiteData } from "@/src/data/site-data";
|
||||
|
||||
export default function AboutPage() {
|
||||
return (
|
||||
<>
|
||||
<InnerPageHeroSection
|
||||
title="Our mission is to give orphans a proper home"
|
||||
sub_title="About Us"
|
||||
description="We raise a generation of God-fearing children who would compete very favourably with the elite in all ramifications."
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export const meta: BunextPageModuleMeta = {
|
||||
title: `About Us | ${SiteData["SiteName"]}`,
|
||||
description: `${SiteData["SiteDescription"]}`,
|
||||
};
|
||||
@@ -0,0 +1,16 @@
|
||||
import type { BunextPageModuleMeta } from "@moduletrace/bunext/types";
|
||||
import AdminHero from "@/src/components/general/admin-hero";
|
||||
import { SiteData } from "@/src/data/site-data";
|
||||
|
||||
export default function AdminDashboardPage() {
|
||||
return (
|
||||
<>
|
||||
<AdminHero title="Dashboard" />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export const meta: BunextPageModuleMeta = {
|
||||
title: `Admin Dashboard | ${SiteData["SiteName"]}`,
|
||||
description: `Admin dashboard`,
|
||||
};
|
||||
@@ -0,0 +1,16 @@
|
||||
import type { BunextPageModuleMeta } from "@moduletrace/bunext/types";
|
||||
import AdminHero from "@/src/components/general/admin-hero";
|
||||
import { SiteData } from "@/src/data/site-data";
|
||||
|
||||
export default function AdminSettingsPage() {
|
||||
return (
|
||||
<>
|
||||
<AdminHero title="Settings" />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export const meta: BunextPageModuleMeta = {
|
||||
title: `Admin Settings | ${SiteData["SiteName"]}`,
|
||||
description: `WgUI settings.`,
|
||||
};
|
||||
@@ -0,0 +1,28 @@
|
||||
import checkUserAccess from "@/src/functions/backend/auth/check-user-access";
|
||||
import type { AdminCrudAPIParams } from "@/src/types";
|
||||
import type { APIResponseObject } from "@moduletrace/bunext/types";
|
||||
|
||||
export default async function ({
|
||||
user,
|
||||
user_types,
|
||||
}: AdminCrudAPIParams): Promise<APIResponseObject> {
|
||||
if (!user?.id) {
|
||||
return {
|
||||
success: false,
|
||||
msg: `No user passed`,
|
||||
};
|
||||
}
|
||||
|
||||
const is_super_admin = checkUserAccess({ user_types });
|
||||
|
||||
if (!is_super_admin.success) {
|
||||
return {
|
||||
success: false,
|
||||
msg: `Unauthorized`,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import _ from "lodash";
|
||||
import defaultChecks from "../default-checks";
|
||||
import type { AdminCrudAPIParams } from "@/src/types";
|
||||
import type { APIResponseObject } from "@moduletrace/bunext/types";
|
||||
import checkUserAccess from "@/src/functions/backend/auth/check-user-access";
|
||||
|
||||
export default async function (
|
||||
params: AdminCrudAPIParams,
|
||||
): Promise<APIResponseObject> {
|
||||
const { table, user, user_types, body, id, query } = params;
|
||||
|
||||
const default_check = await defaultChecks(params);
|
||||
const is_super_admin = checkUserAccess({ user_types }).success;
|
||||
|
||||
if (!default_check.success) {
|
||||
return default_check;
|
||||
}
|
||||
|
||||
if (!is_super_admin) {
|
||||
return {
|
||||
success: false,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import _ from "lodash";
|
||||
import checks from "./checks";
|
||||
import type { AdminCrudAPIParams } from "@/src/types";
|
||||
import type { APIResponseObject } from "@moduletrace/bunext/types";
|
||||
import BunSQLite from "@moduletrace/bun-sqlite";
|
||||
|
||||
export default async function (
|
||||
params: AdminCrudAPIParams,
|
||||
): Promise<APIResponseObject> {
|
||||
const { table, body, id, query } = params;
|
||||
|
||||
const check = await checks(params);
|
||||
|
||||
if (!check?.success) {
|
||||
return {
|
||||
success: false,
|
||||
msg: `Delete Unauthorized`,
|
||||
};
|
||||
}
|
||||
|
||||
const res = await BunSQLite.delete({
|
||||
table,
|
||||
query: _.merge(query?.sql_query, body?.sql_query),
|
||||
targetId: id,
|
||||
});
|
||||
|
||||
return res;
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
import _ from "lodash";
|
||||
import defaultChecks from "../default-checks";
|
||||
import type { AdminCrudAPIParams } from "@/src/types";
|
||||
import type { APIResponseObject } from "@moduletrace/bunext/types";
|
||||
|
||||
export default async function (
|
||||
params: AdminCrudAPIParams,
|
||||
): Promise<APIResponseObject> {
|
||||
const { table, user, user_types, body, id, query } = params;
|
||||
|
||||
const default_check = await defaultChecks(params);
|
||||
|
||||
if (!default_check.success) {
|
||||
return default_check;
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
import _ from "lodash";
|
||||
import checks from "./checks";
|
||||
import type { AdminCrudAPIParams } from "@/src/types";
|
||||
import type { APIResponseObject } from "@moduletrace/bunext/types";
|
||||
import BunSQLite from "@moduletrace/bun-sqlite";
|
||||
|
||||
export default async function (
|
||||
params: AdminCrudAPIParams,
|
||||
): Promise<APIResponseObject> {
|
||||
const { table, body, id, query } = params;
|
||||
|
||||
const check = await checks(params);
|
||||
|
||||
if (!check?.success) {
|
||||
return {
|
||||
success: false,
|
||||
msg: `Select Unauthorized`,
|
||||
};
|
||||
}
|
||||
|
||||
let final_sql_query = _.merge(query?.sql_query, body?.sql_query);
|
||||
|
||||
const res = await BunSQLite.select({
|
||||
table,
|
||||
query: final_sql_query,
|
||||
targetId: id,
|
||||
count: query?.count || body?.count,
|
||||
});
|
||||
|
||||
return res;
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import type { AdminCrudAPIParams } from "@/src/types";
|
||||
import type { APIResponseObject } from "@moduletrace/bunext/types";
|
||||
import _ from "lodash";
|
||||
import defaultChecks from "../default-checks";
|
||||
|
||||
export default async function (
|
||||
params: AdminCrudAPIParams,
|
||||
): Promise<APIResponseObject> {
|
||||
const default_check = await defaultChecks(params);
|
||||
|
||||
if (!default_check.success) {
|
||||
return default_check;
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
import _ from "lodash";
|
||||
import checks from "./checks";
|
||||
import type { AdminCrudAPIParams } from "@/src/types";
|
||||
import type { APIResponseObject } from "@moduletrace/bunext/types";
|
||||
import BunSQLite from "@moduletrace/bun-sqlite";
|
||||
|
||||
export default async function (
|
||||
params: AdminCrudAPIParams,
|
||||
): Promise<APIResponseObject> {
|
||||
const { table, body } = params;
|
||||
|
||||
const check = await checks(params);
|
||||
|
||||
if (!check?.success) {
|
||||
return {
|
||||
success: false,
|
||||
msg: `Insert Unauthorized`,
|
||||
};
|
||||
}
|
||||
|
||||
if (!body?.insert_data) {
|
||||
return {
|
||||
success: false,
|
||||
msg: `No data to insert`,
|
||||
};
|
||||
}
|
||||
|
||||
let final_insert_data = [...body.insert_data];
|
||||
|
||||
const res = await BunSQLite.insert({
|
||||
table,
|
||||
data: final_insert_data,
|
||||
update_on_duplicate: body.update_on_duplicate,
|
||||
});
|
||||
|
||||
return res;
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
import _ from "lodash";
|
||||
import defaultChecks from "../default-checks";
|
||||
import type { AdminCrudAPIParams } from "@/src/types";
|
||||
import type { APIResponseObject } from "@moduletrace/bunext/types";
|
||||
import checkUserAccess from "@/src/functions/backend/auth/check-user-access";
|
||||
|
||||
export default async function (
|
||||
params: AdminCrudAPIParams,
|
||||
): Promise<APIResponseObject> {
|
||||
const { user_types } = params;
|
||||
|
||||
const default_check = await defaultChecks(params);
|
||||
|
||||
if (!default_check.success) {
|
||||
return default_check;
|
||||
}
|
||||
|
||||
const is_admin = checkUserAccess({
|
||||
user_types,
|
||||
includes: ["admin"],
|
||||
});
|
||||
|
||||
if (!is_admin.success) {
|
||||
return is_admin;
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
import _ from "lodash";
|
||||
import checks from "./checks";
|
||||
import type { AdminCrudAPIParams } from "@/src/types";
|
||||
import type { APIResponseObject } from "@moduletrace/bunext/types";
|
||||
import BunSQLite from "@moduletrace/bun-sqlite";
|
||||
|
||||
export default async function (
|
||||
params: AdminCrudAPIParams,
|
||||
): Promise<APIResponseObject> {
|
||||
const { table, body, id, query } = params;
|
||||
|
||||
const check = await checks(params);
|
||||
|
||||
if (!check?.success) {
|
||||
return {
|
||||
success: false,
|
||||
msg: `Update Unauthorized`,
|
||||
};
|
||||
}
|
||||
|
||||
if (!body?.update_data) {
|
||||
return {
|
||||
success: false,
|
||||
msg: `No data to update`,
|
||||
};
|
||||
}
|
||||
|
||||
const res = await BunSQLite.update({
|
||||
table,
|
||||
data: body.update_data,
|
||||
query: _.merge(query?.sql_query, body?.sql_query),
|
||||
targetId: id,
|
||||
});
|
||||
|
||||
return res;
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
import _ from "lodash";
|
||||
import type { AdminCrudAPIParams, TableType } from "@/src/types";
|
||||
import type { APIResponseObject } from "@moduletrace/bunext/types";
|
||||
import checkUserAccess from "@/src/functions/backend/auth/check-user-access";
|
||||
import type {
|
||||
BUN_SQLITE_WGUI_ALL_TYPEDEFS,
|
||||
BUN_SQLITE_WGUI_MEDIA,
|
||||
} from "@/db/types/db";
|
||||
import BunSQLite from "@moduletrace/bun-sqlite";
|
||||
import { rmSync } from "fs";
|
||||
import type { ServerQueryParam } from "@moduletrace/bun-sqlite/dist/types";
|
||||
|
||||
export default async function (
|
||||
params: AdminCrudAPIParams,
|
||||
): Promise<APIResponseObject> {
|
||||
const { user, user_types, body, id } = params;
|
||||
|
||||
const can_delete_media = checkUserAccess({
|
||||
user_types,
|
||||
includes: ["admin"],
|
||||
});
|
||||
|
||||
if (!can_delete_media.success) {
|
||||
return {
|
||||
success: false,
|
||||
msg: `Can't delete media`,
|
||||
};
|
||||
}
|
||||
|
||||
let query = _.merge<
|
||||
ServerQueryParam<BUN_SQLITE_WGUI_ALL_TYPEDEFS>,
|
||||
ServerQueryParam<BUN_SQLITE_WGUI_ALL_TYPEDEFS>
|
||||
>(body?.sql_query || {}, {});
|
||||
|
||||
const media_to_delete = await BunSQLite.select<
|
||||
BUN_SQLITE_WGUI_MEDIA,
|
||||
TableType
|
||||
>({
|
||||
table: "media",
|
||||
query,
|
||||
targetId: id,
|
||||
});
|
||||
|
||||
if (!media_to_delete.payload) {
|
||||
return {
|
||||
success: false,
|
||||
msg: `No Media to Delete.`,
|
||||
};
|
||||
}
|
||||
|
||||
for (let i = 0; i < media_to_delete.payload.length; i++) {
|
||||
const media = media_to_delete.payload[i];
|
||||
if (!media?.id) continue;
|
||||
|
||||
if (media.media_write_path) {
|
||||
rmSync(media.media_write_path);
|
||||
}
|
||||
|
||||
if (media.media_thumbnail_write_path) {
|
||||
rmSync(media.media_thumbnail_write_path);
|
||||
}
|
||||
|
||||
await BunSQLite.delete<BUN_SQLITE_WGUI_MEDIA, TableType>({
|
||||
table: "media",
|
||||
targetId: media.id,
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
import checkUserAccess from "@/src/functions/backend/auth/check-user-access";
|
||||
import grabMedia from "@/src/functions/backend/db/media/grab-media";
|
||||
import type { AdminCrudAPIParams } from "@/src/types";
|
||||
import BunSQLite from "@moduletrace/bun-sqlite";
|
||||
import type { APIResponseObject } from "@moduletrace/bunext/types";
|
||||
import _ from "lodash";
|
||||
|
||||
export default async function (
|
||||
params: AdminCrudAPIParams,
|
||||
): Promise<APIResponseObject> {
|
||||
const { table, user_types, body, id, query, user } = params;
|
||||
|
||||
let final_sql_query = _.merge(query?.sql_query, body?.sql_query) || {};
|
||||
|
||||
delete final_sql_query.join;
|
||||
|
||||
const can_user_see_all_media = checkUserAccess({
|
||||
user_types,
|
||||
includes: ["admin"],
|
||||
});
|
||||
|
||||
if (!can_user_see_all_media.success) {
|
||||
final_sql_query.query = {
|
||||
...final_sql_query.query,
|
||||
user_id: {
|
||||
value: user.id,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const GET = await grabMedia({ query, user });
|
||||
|
||||
return GET;
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
import type { AdminCrudAPIParams } from "@/src/types";
|
||||
import type { APIResponseObject } from "@moduletrace/bunext/types";
|
||||
import _ from "lodash";
|
||||
import get from "./get";
|
||||
import post from "./post";
|
||||
import put from "./put";
|
||||
import del from "./del";
|
||||
|
||||
export default async function (
|
||||
params: AdminCrudAPIParams,
|
||||
): Promise<APIResponseObject> {
|
||||
const { req, query } = params;
|
||||
|
||||
switch (req.method) {
|
||||
case "GET":
|
||||
return await get(params);
|
||||
case "POST":
|
||||
return await post(params);
|
||||
case "PUT":
|
||||
return await put(params);
|
||||
case "DELETE":
|
||||
return await del(params);
|
||||
|
||||
default:
|
||||
return {
|
||||
success: false,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
import type { BUN_SQLITE_WGUI_MEDIA } from "@/db/types/db";
|
||||
import checkUserAccess from "@/src/functions/backend/auth/check-user-access";
|
||||
import type { AdminCrudAPIParams } from "@/src/types";
|
||||
import BunSQLite from "@moduletrace/bun-sqlite";
|
||||
import type { APIResponseObject } from "@moduletrace/bunext/types";
|
||||
import _ from "lodash";
|
||||
|
||||
export default async function (
|
||||
params: AdminCrudAPIParams,
|
||||
): Promise<APIResponseObject> {
|
||||
const { table, body, user_types, user } = params;
|
||||
|
||||
let final_insert_data: BUN_SQLITE_WGUI_MEDIA[] = [
|
||||
...(body?.insert_data || []),
|
||||
].map((d) => ({ ...d, user_id: user.id }));
|
||||
|
||||
const is_user_allowed_to_post_any = checkUserAccess({
|
||||
includes: ["admin"],
|
||||
user_types,
|
||||
});
|
||||
|
||||
if (!is_user_allowed_to_post_any.success) {
|
||||
final_insert_data = final_insert_data.map((fid) => ({
|
||||
...fid,
|
||||
user_id: user.id,
|
||||
}));
|
||||
}
|
||||
|
||||
const POST = await BunSQLite.insert({
|
||||
table,
|
||||
data: final_insert_data,
|
||||
update_on_duplicate: body?.update_on_duplicate,
|
||||
});
|
||||
|
||||
return POST;
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
import checkUserAccess from "@/src/functions/backend/auth/check-user-access";
|
||||
import type { AdminCrudAPIParams } from "@/src/types";
|
||||
import BunSQLite from "@moduletrace/bun-sqlite";
|
||||
import type { APIResponseObject } from "@moduletrace/bunext/types";
|
||||
import _ from "lodash";
|
||||
|
||||
export default async function (
|
||||
params: AdminCrudAPIParams,
|
||||
): Promise<APIResponseObject> {
|
||||
const { table, body, user_types, id, query, user } = params;
|
||||
|
||||
if (!body?.update_data) {
|
||||
return {
|
||||
success: false,
|
||||
msg: `No Update Data`,
|
||||
};
|
||||
}
|
||||
|
||||
const is_user_allowed_to_put = checkUserAccess({
|
||||
includes: ["admin"],
|
||||
user_types,
|
||||
});
|
||||
|
||||
let final_sql_query = _.merge(query?.sql_query, body?.sql_query) || {};
|
||||
let targetId = id;
|
||||
|
||||
if (!is_user_allowed_to_put.success) {
|
||||
final_sql_query = {};
|
||||
targetId = user.id;
|
||||
}
|
||||
|
||||
const PUT = await BunSQLite.update({
|
||||
table,
|
||||
data: body?.update_data,
|
||||
targetId,
|
||||
query: final_sql_query,
|
||||
});
|
||||
|
||||
return PUT;
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
import type { BUN_SQLITE_WGUI_ALL_TYPEDEFS } from "@/db/types/db";
|
||||
import {
|
||||
RolesAdminCantUpdate,
|
||||
SuperAdminOnlyEditableUserTypes,
|
||||
UneditableUserTypes,
|
||||
} from "@/src/dict/user-types-dict";
|
||||
import checkUserAccess from "@/src/functions/backend/auth/check-user-access";
|
||||
import type { AdminCrudAPIParams, UserType } from "@/src/types";
|
||||
import BunSQLite from "@moduletrace/bun-sqlite";
|
||||
import type { APIResponseObject } from "@moduletrace/bunext/types";
|
||||
import _ from "lodash";
|
||||
|
||||
export default async function (
|
||||
params: AdminCrudAPIParams,
|
||||
): Promise<APIResponseObject> {
|
||||
const { table, user_types, body, id, query } = params;
|
||||
|
||||
let final_sql_query = _.merge(query?.sql_query, body?.sql_query) || {};
|
||||
|
||||
const records_to_delete =
|
||||
await BunSQLite.select<BUN_SQLITE_WGUI_ALL_TYPEDEFS>({
|
||||
table,
|
||||
query: final_sql_query,
|
||||
targetId: id,
|
||||
});
|
||||
|
||||
if (!records_to_delete.payload?.[0]) {
|
||||
return {
|
||||
success: false,
|
||||
msg: `No Records to Delete`,
|
||||
};
|
||||
}
|
||||
|
||||
const is_user_super_admin = checkUserAccess({
|
||||
user_types,
|
||||
});
|
||||
|
||||
const admin_includes: UserType[] = ["admin"];
|
||||
|
||||
const can_delete_user_type = checkUserAccess({
|
||||
user_types,
|
||||
includes: admin_includes,
|
||||
});
|
||||
|
||||
const is_user_admin = Boolean(
|
||||
user_types.find(
|
||||
(ut) => ut.user_type && admin_includes.includes(ut.user_type),
|
||||
),
|
||||
);
|
||||
|
||||
if (!can_delete_user_type.success) {
|
||||
return can_delete_user_type;
|
||||
}
|
||||
|
||||
const ignored_user_types = [...UneditableUserTypes] as UserType[];
|
||||
|
||||
if (!is_user_super_admin.success) {
|
||||
ignored_user_types.push(...SuperAdminOnlyEditableUserTypes);
|
||||
}
|
||||
|
||||
const user_types_to_delete = records_to_delete.payload.filter(
|
||||
(rec) => !ignored_user_types.includes(rec.user_type as any),
|
||||
);
|
||||
|
||||
for (let i = 0; i < user_types_to_delete.length; i++) {
|
||||
const user_type_to_delete = user_types_to_delete[i];
|
||||
|
||||
if (!user_type_to_delete?.id) continue;
|
||||
|
||||
if (
|
||||
is_user_admin &&
|
||||
user_type_to_delete.user_type &&
|
||||
RolesAdminCantUpdate.includes(user_type_to_delete.user_type)
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
await BunSQLite.delete({
|
||||
table,
|
||||
targetId: user_type_to_delete.id,
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
import checkUserAccess from "@/src/functions/backend/auth/check-user-access";
|
||||
import type { AdminCrudAPIParams } from "@/src/types";
|
||||
import BunSQLite from "@moduletrace/bun-sqlite";
|
||||
import type { APIResponseObject } from "@moduletrace/bunext/types";
|
||||
import _ from "lodash";
|
||||
|
||||
export default async function (
|
||||
params: AdminCrudAPIParams,
|
||||
): Promise<APIResponseObject> {
|
||||
const { table, user_types, body, id, query, user } = params;
|
||||
|
||||
let final_sql_query = _.merge(query?.sql_query, body?.sql_query) || {};
|
||||
|
||||
delete final_sql_query.join;
|
||||
|
||||
const can_user_see_all_user_types = checkUserAccess({
|
||||
user_types,
|
||||
includes: ["admin"],
|
||||
});
|
||||
|
||||
if (!can_user_see_all_user_types.success) {
|
||||
final_sql_query.query = {
|
||||
...final_sql_query.query,
|
||||
user_id: {
|
||||
value: user.id,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const GET = await BunSQLite.select({
|
||||
table,
|
||||
query: final_sql_query,
|
||||
targetId: id,
|
||||
count: query?.count || body?.count,
|
||||
});
|
||||
|
||||
return GET;
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
import type { AdminCrudAPIParams } from "@/src/types";
|
||||
import type { APIResponseObject } from "@moduletrace/bunext/types";
|
||||
import _ from "lodash";
|
||||
import get from "./get";
|
||||
import post from "./post";
|
||||
import put from "./put";
|
||||
import del from "./del";
|
||||
|
||||
export default async function (
|
||||
params: AdminCrudAPIParams,
|
||||
): Promise<APIResponseObject> {
|
||||
const { req, query } = params;
|
||||
|
||||
switch (req.method) {
|
||||
case "GET":
|
||||
return await get(params);
|
||||
case "POST":
|
||||
return await post(params);
|
||||
case "PUT":
|
||||
return await put(params);
|
||||
case "DELETE":
|
||||
return await del(params);
|
||||
|
||||
default:
|
||||
return {
|
||||
success: false,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
import {
|
||||
RolesAdminCantUpdate,
|
||||
SuperAdminOnlyEditableUserTypes,
|
||||
UneditableUserTypes,
|
||||
} from "@/src/dict/user-types-dict";
|
||||
import checkUserAccess from "@/src/functions/backend/auth/check-user-access";
|
||||
import type { AdminCrudAPIParams, UserType } from "@/src/types";
|
||||
import BunSQLite from "@moduletrace/bun-sqlite";
|
||||
import type { APIResponseObject } from "@moduletrace/bunext/types";
|
||||
import _ from "lodash";
|
||||
|
||||
export default async function (
|
||||
params: AdminCrudAPIParams,
|
||||
): Promise<APIResponseObject> {
|
||||
const { table, body, user_types } = params;
|
||||
|
||||
const is_user_super_admin = checkUserAccess({
|
||||
user_types,
|
||||
});
|
||||
|
||||
const ignored_user_types = [...UneditableUserTypes] as UserType[];
|
||||
|
||||
if (!is_user_super_admin.success) {
|
||||
ignored_user_types.push(...SuperAdminOnlyEditableUserTypes);
|
||||
}
|
||||
|
||||
let final_insert_data = [...(body?.insert_data || [])].filter(
|
||||
(i) => !ignored_user_types.includes(i.user_type as any),
|
||||
);
|
||||
|
||||
const admin_includes: UserType[] = ["admin"];
|
||||
|
||||
const is_user_allowed_to_post = checkUserAccess({
|
||||
includes: admin_includes,
|
||||
user_types,
|
||||
});
|
||||
|
||||
if (!is_user_allowed_to_post.success) {
|
||||
return is_user_allowed_to_post;
|
||||
}
|
||||
|
||||
const is_user_admin = Boolean(
|
||||
user_types.find(
|
||||
(ut) => ut.user_type && admin_includes.includes(ut.user_type),
|
||||
),
|
||||
);
|
||||
|
||||
if (is_user_admin) {
|
||||
final_insert_data = final_insert_data.filter(
|
||||
(d) => d.user_type && !RolesAdminCantUpdate.includes(d.user_type),
|
||||
);
|
||||
}
|
||||
|
||||
const POST = await BunSQLite.insert({
|
||||
table,
|
||||
data: final_insert_data,
|
||||
update_on_duplicate: body?.update_on_duplicate,
|
||||
});
|
||||
|
||||
return POST;
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import type { AdminCrudAPIParams } from "@/src/types";
|
||||
import type { APIResponseObject } from "@moduletrace/bunext/types";
|
||||
import _ from "lodash";
|
||||
|
||||
export default async function (
|
||||
params: AdminCrudAPIParams,
|
||||
): Promise<APIResponseObject> {
|
||||
const { table, body, user_types, id, query } = params;
|
||||
|
||||
return {
|
||||
success: false,
|
||||
msg: `Can't update user_type. Can only delete and insert.`,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
import type { BUN_SQLITE_WGUI_ALL_TYPEDEFS } from "@/db/types/db";
|
||||
import checkUserAccess from "@/src/functions/backend/auth/check-user-access";
|
||||
import checkAllowedJoins from "@/src/functions/backend/db/check-allowed-joins";
|
||||
import grabUsers from "@/src/functions/backend/db/users/grab-users";
|
||||
import type { AdminCrudAPIParams, ApiReqParams, TableType } from "@/src/types";
|
||||
import BunSQLite from "@moduletrace/bun-sqlite";
|
||||
import type { APIResponseObject } from "@moduletrace/bunext/types";
|
||||
import _ from "lodash";
|
||||
|
||||
export default async function (
|
||||
params: AdminCrudAPIParams,
|
||||
): Promise<APIResponseObject> {
|
||||
const { req, table, user_types, body, id, query, user } = params;
|
||||
|
||||
let final_sql_query = _.merge(query?.sql_query, body?.sql_query) || {};
|
||||
let final_query =
|
||||
_.merge<
|
||||
ApiReqParams<BUN_SQLITE_WGUI_ALL_TYPEDEFS>,
|
||||
ApiReqParams<BUN_SQLITE_WGUI_ALL_TYPEDEFS>
|
||||
>(query || {}, { sql_query: final_sql_query }) || {};
|
||||
|
||||
const allowed_joins: TableType[] = ["user_types", "media"];
|
||||
|
||||
const check_allowed_joins = checkAllowedJoins({
|
||||
joins: allowed_joins,
|
||||
query: final_sql_query,
|
||||
});
|
||||
|
||||
if (!check_allowed_joins.success) {
|
||||
return check_allowed_joins;
|
||||
}
|
||||
|
||||
const can_user_see_all_users = checkUserAccess({
|
||||
user_types,
|
||||
includes: ["admin"],
|
||||
});
|
||||
|
||||
if (!can_user_see_all_users.success) {
|
||||
final_sql_query.query = {
|
||||
...final_sql_query.query,
|
||||
user_id: {
|
||||
value: user.id,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// const GET = await BunSQLite.select({
|
||||
// table,
|
||||
// query: final_sql_query,
|
||||
// targetId: id,
|
||||
// count: query?.count || body?.count,
|
||||
// });
|
||||
|
||||
const GET = await grabUsers({
|
||||
query: final_query,
|
||||
user_id: id,
|
||||
});
|
||||
|
||||
return GET;
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
import type { AdminCrudAPIParams } from "@/src/types";
|
||||
import type { APIResponseObject } from "@moduletrace/bunext/types";
|
||||
import _ from "lodash";
|
||||
import get from "./get";
|
||||
import post from "./post";
|
||||
import put from "./put";
|
||||
|
||||
export default async function (
|
||||
params: AdminCrudAPIParams,
|
||||
): Promise<APIResponseObject> {
|
||||
const { req } = params;
|
||||
|
||||
switch (req.method) {
|
||||
case "GET":
|
||||
return await get(params);
|
||||
case "POST":
|
||||
return await post(params);
|
||||
case "PUT":
|
||||
return await put(params);
|
||||
case "DELETE":
|
||||
return {
|
||||
success: false,
|
||||
msg: `Can't delete any user`,
|
||||
};
|
||||
|
||||
default:
|
||||
return {
|
||||
success: false,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
import checkUserAccess from "@/src/functions/backend/auth/check-user-access";
|
||||
import grabUsers from "@/src/functions/backend/db/users/grab-users";
|
||||
import type { AdminCrudAPIParams } from "@/src/types";
|
||||
import BunSQLite from "@moduletrace/bun-sqlite";
|
||||
import type { APIResponseObject } from "@moduletrace/bunext/types";
|
||||
import _ from "lodash";
|
||||
|
||||
export default async function (
|
||||
params: AdminCrudAPIParams,
|
||||
): Promise<APIResponseObject> {
|
||||
const { table, body, user_types } = params;
|
||||
|
||||
let final_insert_data = [...(body?.insert_data || [])];
|
||||
|
||||
const is_user_allowed_to_post = checkUserAccess({
|
||||
user_types,
|
||||
});
|
||||
|
||||
if (!is_user_allowed_to_post.success) {
|
||||
return is_user_allowed_to_post;
|
||||
}
|
||||
|
||||
const POST = await BunSQLite.insert({
|
||||
table,
|
||||
data: final_insert_data,
|
||||
update_on_duplicate: body?.update_on_duplicate,
|
||||
});
|
||||
|
||||
return POST;
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
import checkUserAccess from "@/src/functions/backend/auth/check-user-access";
|
||||
import type { AdminCrudAPIParams } from "@/src/types";
|
||||
import BunSQLite from "@moduletrace/bun-sqlite";
|
||||
import type { APIResponseObject } from "@moduletrace/bunext/types";
|
||||
import _ from "lodash";
|
||||
|
||||
export default async function (
|
||||
params: AdminCrudAPIParams,
|
||||
): Promise<APIResponseObject> {
|
||||
const { table, body, user_types, id, query, user } = params;
|
||||
|
||||
let final_sql_query = _.merge(query?.sql_query, body?.sql_query) || {};
|
||||
|
||||
if (!body?.update_data) {
|
||||
return {
|
||||
success: false,
|
||||
msg: `No Update Data`,
|
||||
};
|
||||
}
|
||||
|
||||
const is_user_allowed_to_update = checkUserAccess({
|
||||
includes: ["admin"],
|
||||
user_types,
|
||||
});
|
||||
|
||||
let targetId = id;
|
||||
|
||||
if (!is_user_allowed_to_update.success) {
|
||||
final_sql_query = {};
|
||||
targetId = user.id;
|
||||
}
|
||||
|
||||
const PUT = await BunSQLite.update({
|
||||
table,
|
||||
data: body.update_data,
|
||||
targetId,
|
||||
query: final_sql_query,
|
||||
});
|
||||
|
||||
return PUT;
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import type { TableType } from "@/src/types";
|
||||
import type { APIResponseObject } from "@moduletrace/bunext/types";
|
||||
|
||||
type Params = {
|
||||
allowed_joins: TableType[];
|
||||
};
|
||||
|
||||
export default function checkAllowedJoin({
|
||||
allowed_joins,
|
||||
}: Params): APIResponseObject {
|
||||
return {
|
||||
success: true,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
import _ from "lodash";
|
||||
import users from "./(tables)/users";
|
||||
import userTypes from "./(tables)/user_types";
|
||||
import media from "./(tables)/media";
|
||||
import get from "./(functions)/get";
|
||||
import put from "./(functions)/put";
|
||||
import del from "./(functions)/delete";
|
||||
import post from "./(functions)/post";
|
||||
import type {
|
||||
APIResponseObject,
|
||||
BunextAPIRouteHandler,
|
||||
} from "@moduletrace/bunext/types";
|
||||
import type { AdminCrudAPIParams, ApiReqParams, TableType } from "@/src/types";
|
||||
import userAuth from "@/src/functions/backend/auth/user-auth";
|
||||
import checkUserAccess from "@/src/functions/backend/auth/check-user-access";
|
||||
|
||||
export const handler: BunextAPIRouteHandler<APIResponseObject> = async (
|
||||
params,
|
||||
) => {
|
||||
const req = params.req;
|
||||
const body = params.body as ApiReqParams;
|
||||
const query = params.query as ApiReqParams;
|
||||
|
||||
const { user, user_types } = await userAuth({ req });
|
||||
|
||||
if (!user?.logged_in_status) {
|
||||
return {
|
||||
success: false,
|
||||
msg: `Unauthorized`,
|
||||
logoutUser: true,
|
||||
};
|
||||
}
|
||||
|
||||
if (!user_types?.[0]) {
|
||||
return {
|
||||
success: false,
|
||||
msg: `User types not found`,
|
||||
logoutUser: true,
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
if (
|
||||
user_types.find((ty) => ty.user_type == "read_only") &&
|
||||
!req.method.match(/get/)
|
||||
) {
|
||||
throw new Error(`This user can only read records.`);
|
||||
}
|
||||
|
||||
const [table, id] = params.query.paths.split("/") as [
|
||||
TableType,
|
||||
string | number,
|
||||
];
|
||||
|
||||
const crud_params: AdminCrudAPIParams = {
|
||||
table,
|
||||
id,
|
||||
user: user || undefined,
|
||||
user_types: user_types || [],
|
||||
body,
|
||||
query,
|
||||
req,
|
||||
};
|
||||
|
||||
switch (table) {
|
||||
case "users":
|
||||
return await users(crud_params);
|
||||
case "user_types":
|
||||
return await userTypes(crud_params);
|
||||
case "media":
|
||||
return await media(crud_params);
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
const is_super_admin = checkUserAccess({ user_types });
|
||||
|
||||
if (!is_super_admin.success) {
|
||||
return {
|
||||
success: false,
|
||||
msg: `Unauthorized`,
|
||||
};
|
||||
}
|
||||
|
||||
switch (req.method) {
|
||||
case "GET":
|
||||
return await get(crud_params);
|
||||
case "POST":
|
||||
return await post(crud_params);
|
||||
case "PUT":
|
||||
return await put(crud_params);
|
||||
case "DELETE":
|
||||
return await del(crud_params);
|
||||
|
||||
default:
|
||||
return {
|
||||
success: false,
|
||||
msg: `Unhandled`,
|
||||
};
|
||||
}
|
||||
} catch (error: any) {
|
||||
return {
|
||||
success: false,
|
||||
msg: error.message,
|
||||
};
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,58 @@
|
||||
import _ from "lodash";
|
||||
import userAuth from "@/src/functions/backend/auth/user-auth";
|
||||
import type { ApiReqParams } from "@/src/types";
|
||||
import type {
|
||||
APIResponseObject,
|
||||
BunextAPIRouteHandler,
|
||||
} from "@moduletrace/bunext/types";
|
||||
import deleteMedia from "@/src/functions/backend/db/media/delete-media";
|
||||
import checkUserAccess from "@/src/functions/backend/auth/check-user-access";
|
||||
|
||||
export const handler: BunextAPIRouteHandler<APIResponseObject> = async (
|
||||
params,
|
||||
) => {
|
||||
const req = params.req;
|
||||
const body = params.body as ApiReqParams;
|
||||
|
||||
if (req.method !== "DELETE") {
|
||||
return {
|
||||
success: false,
|
||||
};
|
||||
}
|
||||
|
||||
const { user, user_types } = await userAuth({ req });
|
||||
|
||||
if (!user?.logged_in_status) {
|
||||
return {
|
||||
success: false,
|
||||
msg: `Unauthorized`,
|
||||
logoutUser: true,
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
const { id, ids, media_paradigm, user_id } = body;
|
||||
|
||||
const can_delete_all_media = checkUserAccess({
|
||||
includes: ["admin", "board_member"],
|
||||
user_types,
|
||||
});
|
||||
|
||||
if (user_id && user.id !== user_id && !can_delete_all_media.success) {
|
||||
throw new Error(`Operation not allowed!`);
|
||||
}
|
||||
|
||||
return await deleteMedia({
|
||||
id,
|
||||
ids,
|
||||
media_paradigm,
|
||||
user_id,
|
||||
can_delete_all_media: can_delete_all_media.success,
|
||||
});
|
||||
} catch (error: any) {
|
||||
return {
|
||||
success: false,
|
||||
msg: `API Error: ${error.message}`,
|
||||
};
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,48 @@
|
||||
import checkUserAccess from "@/src/functions/backend/auth/check-user-access";
|
||||
import userAuth from "@/src/functions/backend/auth/user-auth";
|
||||
import grabUsers from "@/src/functions/backend/db/users/grab-users";
|
||||
import type {
|
||||
APIResponseObject,
|
||||
BunextAPIRouteHandler,
|
||||
} from "@moduletrace/bunext/types";
|
||||
import _ from "lodash";
|
||||
|
||||
export const handler: BunextAPIRouteHandler<APIResponseObject> = async ({
|
||||
req,
|
||||
query,
|
||||
}) => {
|
||||
const { user, user_types } = await userAuth({ req });
|
||||
|
||||
if (!user?.logged_in_status) {
|
||||
return {
|
||||
success: false,
|
||||
msg: `Unauthorized`,
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
const is_admin = checkUserAccess({
|
||||
user_types,
|
||||
includes: ["admin"],
|
||||
});
|
||||
|
||||
if (
|
||||
!is_admin.success &&
|
||||
(!query?.user_id || query.user_id !== user.id)
|
||||
) {
|
||||
return {
|
||||
success: false,
|
||||
msg: `Unauthorized`,
|
||||
};
|
||||
}
|
||||
|
||||
return await grabUsers({
|
||||
query,
|
||||
});
|
||||
} catch (error: any) {
|
||||
return {
|
||||
success: false,
|
||||
msg: error.message,
|
||||
};
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,87 @@
|
||||
import twuiNumberfy from "@/src/components/twui/utils/numberfy";
|
||||
import loginUser from "@/src/functions/backend/auth/login-user";
|
||||
import userAuth from "@/src/functions/backend/auth/user-auth";
|
||||
import uploadAndRecordMedia from "@/src/functions/backend/db/users/media/upload-and-record-media";
|
||||
import type { ApiReqParams } from "@/src/types";
|
||||
import bufferFromBase64 from "@/src/utils/buffer-from-base-64";
|
||||
import type {
|
||||
APIResponseObject,
|
||||
BunextAPIRouteHandler,
|
||||
} from "@moduletrace/bunext/types";
|
||||
import _ from "lodash";
|
||||
|
||||
export const handler: BunextAPIRouteHandler<APIResponseObject> = async (
|
||||
params,
|
||||
) => {
|
||||
const req = params.req;
|
||||
const body = params.body as ApiReqParams;
|
||||
|
||||
if (req.method !== "POST") {
|
||||
return {
|
||||
success: false,
|
||||
};
|
||||
}
|
||||
|
||||
const { user, user_types } = await userAuth({ req });
|
||||
|
||||
if (!user?.logged_in_status) {
|
||||
return {
|
||||
success: false,
|
||||
msg: `Unauthorized`,
|
||||
logoutUser: true,
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
const {
|
||||
user_id,
|
||||
media_base_64_data_url,
|
||||
media_paradigms,
|
||||
media_type,
|
||||
is_media_primary,
|
||||
update_on_duplicate,
|
||||
reauth,
|
||||
return_media_text_content,
|
||||
media,
|
||||
is_media_private,
|
||||
media_name,
|
||||
} = body;
|
||||
|
||||
if (!media_base_64_data_url) {
|
||||
throw new Error(
|
||||
`No image payload sent! Base64 data URL with mime type is required.`,
|
||||
);
|
||||
}
|
||||
|
||||
const { buffer, ext } = await bufferFromBase64(media_base_64_data_url);
|
||||
|
||||
const new_media = await uploadAndRecordMedia({
|
||||
data: buffer,
|
||||
media_paradigms,
|
||||
media_type,
|
||||
is_primary: is_media_primary,
|
||||
update_on_duplicate,
|
||||
extract_media_text_content: return_media_text_content,
|
||||
media,
|
||||
media_mime_type: ext,
|
||||
user: { id: twuiNumberfy(user_id) },
|
||||
is_private: is_media_private,
|
||||
media_name,
|
||||
});
|
||||
|
||||
if (reauth) {
|
||||
return await loginUser({
|
||||
user_id: Number(user_id),
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
...new_media,
|
||||
};
|
||||
} catch (error: any) {
|
||||
return {
|
||||
success: false,
|
||||
msg: error.message,
|
||||
};
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,133 @@
|
||||
import type {
|
||||
BUN_SQLITE_WGUI_SSO_LOGIN_CODES,
|
||||
BUN_SQLITE_WGUI_USERS,
|
||||
BunSQLiteTables,
|
||||
} from "@/db/types/db";
|
||||
import { AppData } from "@/src/data/app-data";
|
||||
import { SiteData } from "@/src/data/site-data";
|
||||
import sendEmail from "@/src/functions/backend/email/send-email";
|
||||
import type { ApiReqParams, SSOAuth } from "@/src/types";
|
||||
import { setCookies } from "@/src/utils/cookies";
|
||||
import { encrypt } from "@/src/utils/crypt";
|
||||
import EJSON from "@/src/utils/ejson";
|
||||
import BunSQLite from "@moduletrace/bun-sqlite";
|
||||
import BunSQLiteB from "@moduletrace/bun-sqlite";
|
||||
import type {
|
||||
APIResponseObject,
|
||||
BunextAPIRouteHandler,
|
||||
} from "@moduletrace/bunext/types";
|
||||
|
||||
export const handler: BunextAPIRouteHandler<APIResponseObject> = async ({
|
||||
req,
|
||||
body,
|
||||
}) => {
|
||||
try {
|
||||
const { login } = body as ApiReqParams;
|
||||
|
||||
if (!login?.email_or_username) {
|
||||
throw new Error(`Please pass an email or phone number`);
|
||||
}
|
||||
|
||||
const target_user = (
|
||||
await BunSQLiteB.select<
|
||||
BUN_SQLITE_WGUI_USERS,
|
||||
(typeof BunSQLiteTables)[number]
|
||||
>({
|
||||
table: "users",
|
||||
query: {
|
||||
query: {
|
||||
email: {
|
||||
value: login.email_or_username,
|
||||
},
|
||||
username: {
|
||||
value: login.email_or_username,
|
||||
},
|
||||
},
|
||||
searchOperator: "OR",
|
||||
},
|
||||
})
|
||||
)?.singleRes;
|
||||
|
||||
if (!target_user?.id || !target_user.email) {
|
||||
throw new Error(`User not Found!`);
|
||||
}
|
||||
|
||||
const sso_code = Math.random().toString().slice(2, 8);
|
||||
|
||||
const email_component = (
|
||||
<div>
|
||||
<div>Use this Code to Complete your login</div>
|
||||
<h1>{sso_code}</h1>
|
||||
<div>
|
||||
Code expires in {AppData["SSOCodeExpiryMinutes"]} minutes.
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
const send_mail = await sendEmail({
|
||||
content: email_component,
|
||||
to:
|
||||
process.env.NODE_ENV == "production"
|
||||
? target_user.email
|
||||
: "[email protected]",
|
||||
subject: `Use one-time-code to complete your login.`,
|
||||
text: `Complete your login to the SBF portal`,
|
||||
title: `${SiteData["SiteName"]} SSO`,
|
||||
options: {
|
||||
priority: "high",
|
||||
},
|
||||
});
|
||||
|
||||
if (!send_mail.success) {
|
||||
return send_mail;
|
||||
}
|
||||
|
||||
const record_sso = await BunSQLite.insert<
|
||||
BUN_SQLITE_WGUI_SSO_LOGIN_CODES,
|
||||
(typeof BunSQLiteTables)[number]
|
||||
>({
|
||||
data: [
|
||||
{
|
||||
code: sso_code,
|
||||
user_id: target_user.id,
|
||||
},
|
||||
],
|
||||
table: "sso_login_codes",
|
||||
update_on_duplicate: true,
|
||||
});
|
||||
|
||||
const sso_auth: SSOAuth = {
|
||||
user_id: target_user.id,
|
||||
sso_code,
|
||||
email: target_user.email,
|
||||
};
|
||||
|
||||
const sso_auth_string = EJSON.stringify(sso_auth);
|
||||
if (!sso_auth_string) {
|
||||
throw new Error(`Couldn't Stringify SSO Auth`);
|
||||
}
|
||||
|
||||
const encrypted_sso_auth_string = await encrypt(sso_auth_string);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
bunext_api_route_res_transform_fn(res) {
|
||||
const new_res = res.clone();
|
||||
setCookies(new_res, [
|
||||
{
|
||||
name: AppData["SSOAuthCookieName"],
|
||||
value: encrypted_sso_auth_string,
|
||||
},
|
||||
]);
|
||||
return new_res;
|
||||
},
|
||||
};
|
||||
} catch (error: any) {
|
||||
console.log("error", error.message);
|
||||
|
||||
return {
|
||||
success: false,
|
||||
msg: error.message,
|
||||
};
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,74 @@
|
||||
import loginUser from "@/src/functions/backend/auth/login-user";
|
||||
import verifyGoogleIdToken from "@/src/functions/backend/auth/verify-google-id-token";
|
||||
import grabUsers from "@/src/functions/backend/db/users/grab-users";
|
||||
import uploadAndRecordMedia from "@/src/functions/backend/db/users/media/upload-and-record-media";
|
||||
import type { ApiReqParams } from "@/src/types";
|
||||
import type {
|
||||
APIResponseObject,
|
||||
BunextAPIRouteHandler,
|
||||
} from "@moduletrace/bunext/types";
|
||||
|
||||
export const handler: BunextAPIRouteHandler<APIResponseObject> = async ({
|
||||
body,
|
||||
}) => {
|
||||
try {
|
||||
const { google_token } = body as ApiReqParams;
|
||||
|
||||
if (!google_token) {
|
||||
throw new Error(`No Google token was provided`);
|
||||
}
|
||||
|
||||
const googleClientId = process.env.GOOGLE_CLIENT_ID;
|
||||
|
||||
if (!googleClientId) {
|
||||
throw new Error(`Google login is not configured on this server`);
|
||||
}
|
||||
|
||||
const googleUser = await verifyGoogleIdToken({
|
||||
clientId: googleClientId,
|
||||
idToken: google_token,
|
||||
});
|
||||
|
||||
const target_user_res = await grabUsers({
|
||||
query: {
|
||||
search_term_email: googleUser.email,
|
||||
},
|
||||
exact_match: true,
|
||||
});
|
||||
|
||||
const target_user = target_user_res.singleRes;
|
||||
|
||||
if (!target_user?.id) {
|
||||
throw new Error(`No account exists for ${googleUser.email}`);
|
||||
}
|
||||
|
||||
if (googleUser.picture && !target_user.profile_media_id) {
|
||||
const fullResUrl = googleUser.picture.replace(/=s\d+-c$/, "=s0");
|
||||
|
||||
const res = await fetch(fullResUrl);
|
||||
|
||||
if (res.ok) {
|
||||
const bytes = new Uint8Array(await res.arrayBuffer());
|
||||
|
||||
await uploadAndRecordMedia({
|
||||
data: bytes,
|
||||
user: target_user,
|
||||
media_paradigms: ["user-profile-image"],
|
||||
media_type: "image",
|
||||
is_primary: true,
|
||||
media_name: `user-${target_user.id}-profile`,
|
||||
media_mime_type: "jpeg",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return await loginUser({
|
||||
user_id: target_user.id,
|
||||
});
|
||||
} catch (error: any) {
|
||||
return {
|
||||
success: false,
|
||||
msg: error.message,
|
||||
};
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,95 @@
|
||||
import type {
|
||||
BUN_SQLITE_WGUI_SSO_LOGIN_CODES,
|
||||
BUN_SQLITE_WGUI_USERS,
|
||||
BunSQLiteTables,
|
||||
} from "@/db/types/db";
|
||||
import { AppData } from "@/src/data/app-data";
|
||||
import loginUser from "@/src/functions/backend/auth/login-user";
|
||||
import type { ApiReqParams, SSOAuth } from "@/src/types";
|
||||
import { getCookie } from "@/src/utils/cookies";
|
||||
import { decrypt } from "@/src/utils/crypt";
|
||||
import EJSON from "@/src/utils/ejson";
|
||||
import BunSQLite from "@moduletrace/bun-sqlite";
|
||||
import type {
|
||||
APIResponseObject,
|
||||
BunextAPIRouteHandler,
|
||||
} from "@moduletrace/bunext/types";
|
||||
|
||||
export const handler: BunextAPIRouteHandler<APIResponseObject> = async ({
|
||||
req,
|
||||
body,
|
||||
}) => {
|
||||
try {
|
||||
const { sso_code } = body as ApiReqParams;
|
||||
|
||||
const sso_cookie = getCookie(req, AppData["SSOAuthCookieName"]);
|
||||
|
||||
if (!sso_cookie) {
|
||||
throw new Error(`No SSO session found!`);
|
||||
}
|
||||
|
||||
if (!sso_code) {
|
||||
throw new Error(`No SSO code sent!`);
|
||||
}
|
||||
|
||||
const decrypted_sso_json = await decrypt(sso_cookie);
|
||||
const decrypted_sso_object = EJSON.parse(decrypted_sso_json) as SSOAuth;
|
||||
|
||||
const target_sso = (
|
||||
await BunSQLite.select<
|
||||
BUN_SQLITE_WGUI_SSO_LOGIN_CODES,
|
||||
(typeof BunSQLiteTables)[number]
|
||||
>({
|
||||
table: "sso_login_codes",
|
||||
query: {
|
||||
query: {
|
||||
code: {
|
||||
value: sso_code,
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
).singleRes;
|
||||
|
||||
if (!target_sso?.code) {
|
||||
throw new Error(`Invalid Code!`);
|
||||
}
|
||||
|
||||
const now = Date.now();
|
||||
const time_elapsed = now - Number(target_sso.updated_at);
|
||||
const expirty_time = AppData["SSOCodeExpiryMinutes"] * 60 * 1000;
|
||||
|
||||
if (time_elapsed > expirty_time) {
|
||||
throw new Error(`Code Expired. Please Login again.`);
|
||||
}
|
||||
|
||||
const target_user = (
|
||||
await BunSQLite.select<
|
||||
BUN_SQLITE_WGUI_USERS,
|
||||
(typeof BunSQLiteTables)[number]
|
||||
>({
|
||||
table: "users",
|
||||
query: {
|
||||
query: {
|
||||
email: decrypted_sso_object.email
|
||||
? { value: decrypted_sso_object.email }
|
||||
: undefined,
|
||||
},
|
||||
},
|
||||
})
|
||||
).singleRes;
|
||||
|
||||
if (!target_user?.id) {
|
||||
throw new Error(`This device wasn't used to get this SSO code`);
|
||||
}
|
||||
|
||||
return await loginUser({
|
||||
user_id: target_user.id,
|
||||
});
|
||||
} catch (error: any) {
|
||||
return {
|
||||
success: false,
|
||||
msg: error.message,
|
||||
};
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,52 @@
|
||||
import type { BUN_SQLITE_WGUI_USERS } from "@/db/types/db";
|
||||
import grabUsers from "@/src/functions/backend/db/users/grab-users";
|
||||
import type { ApiReqParams, TableType } from "@/src/types";
|
||||
import BunSQLite from "@moduletrace/bun-sqlite";
|
||||
import type {
|
||||
APIResponseObject,
|
||||
BunextAPIRouteHandler,
|
||||
} from "@moduletrace/bunext/types";
|
||||
import { hash } from "bun";
|
||||
import _ from "lodash";
|
||||
|
||||
export const handler: BunextAPIRouteHandler<
|
||||
APIResponseObject<BUN_SQLITE_WGUI_USERS>
|
||||
> = async (ctx) => {
|
||||
try {
|
||||
const body = ctx.body as ApiReqParams;
|
||||
|
||||
const users_res = await grabUsers({
|
||||
query: {
|
||||
sql_query: {
|
||||
limit: 1,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
console.log("users_res", users_res);
|
||||
|
||||
if (users_res.singleRes?.id) {
|
||||
return {
|
||||
success: false,
|
||||
};
|
||||
}
|
||||
|
||||
let final_insert_data = [
|
||||
...(body?.insert_data?.map((data) => {
|
||||
return { ...data, password: hash(data.password || "") };
|
||||
}) || []),
|
||||
];
|
||||
|
||||
const POST = await BunSQLite.insert<BUN_SQLITE_WGUI_USERS, TableType>({
|
||||
table: "users",
|
||||
data: final_insert_data,
|
||||
});
|
||||
|
||||
return POST;
|
||||
} catch (error: any) {
|
||||
return {
|
||||
success: false,
|
||||
msg: error.message,
|
||||
};
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,10 @@
|
||||
import type { BunextPageServerFn } from "@moduletrace/bunext/types";
|
||||
|
||||
export const server: BunextPageServerFn = async () => {
|
||||
return {
|
||||
redirect: {
|
||||
destination: "/auth/login",
|
||||
permanent: false,
|
||||
},
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,31 @@
|
||||
import type { BunextPageModuleMeta } from "@moduletrace/bunext/types";
|
||||
import Section from "@/src/components/twui/layout/Section";
|
||||
import Container from "@/src/components/twui/layout/Container";
|
||||
import Stack from "@/src/components/twui/layout/Stack";
|
||||
import H1 from "@/src/components/twui/layout/H1";
|
||||
import Span from "@/src/components/twui/layout/Span";
|
||||
import BlurredImageBG from "@/src/components/general/blured-image-bg";
|
||||
import { SiteData } from "@/src/data/site-data";
|
||||
|
||||
export default function AuthPage() {
|
||||
return (
|
||||
<>
|
||||
<Section className="hero-section">
|
||||
<BlurredImageBG src="/images/children-school-1.webp" />
|
||||
<Container className="relative z-10">
|
||||
<Stack className="w-full items-center max-w-3xl mx-auto gap-7">
|
||||
<Span className="htag">Admin Section</Span>
|
||||
<H1 className="text-center font-bold">
|
||||
Redirecting you to login ...
|
||||
</H1>
|
||||
</Stack>
|
||||
</Container>
|
||||
</Section>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export const meta: BunextPageModuleMeta = {
|
||||
title: `Admin Login | ${SiteData["SiteName"]}`,
|
||||
description: `Admin section access. Redirecting to login.`,
|
||||
};
|
||||
@@ -0,0 +1,37 @@
|
||||
import fetchApi from "@/src/components/twui/utils/fetch/fetchApi";
|
||||
import type useFormInit from "@/src/hooks/use-form-init";
|
||||
import type { ApiReqParams, LoginFormObject } from "@/src/types";
|
||||
import type { APIResponseObject } from "@moduletrace/bunext/types";
|
||||
|
||||
export default async function submitLoginForm({
|
||||
setLoading,
|
||||
setStatus,
|
||||
form,
|
||||
}: ReturnType<typeof useFormInit<LoginFormObject>>) {
|
||||
setLoading(true);
|
||||
|
||||
const res = await fetchApi<ApiReqParams, APIResponseObject>(
|
||||
`/api/auth/get-login-code`,
|
||||
{
|
||||
method: "POST",
|
||||
body: {
|
||||
login: {
|
||||
email_or_username: form.username_or_email,
|
||||
},
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
if (res.success) {
|
||||
window.location.pathname = `/auth/sso`;
|
||||
} else {
|
||||
setStatus({
|
||||
error: true,
|
||||
msg: res.msg || "Login Failed",
|
||||
});
|
||||
console.log("res", res);
|
||||
console.log("form", form);
|
||||
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,200 @@
|
||||
import fetchApi from "@/src/components/twui/utils/fetch/fetchApi";
|
||||
import type useFormInit from "@/src/hooks/use-form-init";
|
||||
import { AppContext } from "@/src/pages/__root";
|
||||
import type { ApiReqParams, GoogleWindow, LoginFormObject } from "@/src/types";
|
||||
import type { APIResponseObject } from "@moduletrace/bunext/types";
|
||||
import { useContext, useEffect, useRef, useState } from "react";
|
||||
|
||||
const GoogleScriptSelector =
|
||||
'script[src="https://accounts.google.com/gsi/client"]';
|
||||
|
||||
export default function useGoogleLoginInit({
|
||||
loading,
|
||||
setStatus,
|
||||
status,
|
||||
setLoading,
|
||||
}: ReturnType<typeof useFormInit<LoginFormObject>>) {
|
||||
const { pageProps } = useContext(AppContext);
|
||||
|
||||
const googleButtonRef = useRef<HTMLDivElement>(null);
|
||||
const googleInitializedRef = useRef(false);
|
||||
const [scriptLoaded, setScriptLoaded] = useState(false);
|
||||
const [buttonWidth, setButtonWidth] = useState(0);
|
||||
const [googleReady, setGoogleReady] = useState(false);
|
||||
|
||||
const googleClientId = pageProps?.envs?.GOOGLE_CLIENT_ID;
|
||||
|
||||
const submitGoogleLogin = async (google_token: string) => {
|
||||
setLoading(true);
|
||||
|
||||
const res = await fetchApi<ApiReqParams, APIResponseObject>(
|
||||
`/api/auth/google-login`,
|
||||
{
|
||||
method: "POST",
|
||||
body: {
|
||||
google_token,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
if (res.success) {
|
||||
window.location.pathname = `/admin`;
|
||||
} else {
|
||||
setStatus({
|
||||
error: true,
|
||||
msg: res.msg || "Google Login Failed",
|
||||
});
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!googleClientId) {
|
||||
return;
|
||||
}
|
||||
|
||||
const clientWindow = window as GoogleWindow;
|
||||
|
||||
if (clientWindow.google?.accounts?.id) {
|
||||
setScriptLoaded(true);
|
||||
return;
|
||||
}
|
||||
|
||||
const existingScript = document.querySelector(
|
||||
GoogleScriptSelector,
|
||||
) as HTMLScriptElement | null;
|
||||
|
||||
const handleLoad = () => {
|
||||
setScriptLoaded(true);
|
||||
setStatus(undefined);
|
||||
};
|
||||
|
||||
const handleError = () => {
|
||||
setStatus({
|
||||
error: true,
|
||||
msg: "Couldn't load Google login.",
|
||||
});
|
||||
};
|
||||
|
||||
if (existingScript) {
|
||||
existingScript.addEventListener("load", handleLoad);
|
||||
existingScript.addEventListener("error", handleError);
|
||||
|
||||
return () => {
|
||||
existingScript.removeEventListener("load", handleLoad);
|
||||
existingScript.removeEventListener("error", handleError);
|
||||
};
|
||||
}
|
||||
|
||||
const script = document.createElement("script");
|
||||
script.src = "https://accounts.google.com/gsi/client";
|
||||
script.async = true;
|
||||
script.defer = true;
|
||||
script.addEventListener("load", handleLoad);
|
||||
script.addEventListener("error", handleError);
|
||||
|
||||
document.head.appendChild(script);
|
||||
|
||||
return () => {
|
||||
script.removeEventListener("load", handleLoad);
|
||||
script.removeEventListener("error", handleError);
|
||||
};
|
||||
}, [status]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!googleButtonRef.current || typeof ResizeObserver === "undefined") {
|
||||
return;
|
||||
}
|
||||
|
||||
const resizeObserver = new ResizeObserver((entries) => {
|
||||
const nextWidth = Math.round(entries[0]?.contentRect.width || 0);
|
||||
|
||||
if (!nextWidth) {
|
||||
return;
|
||||
}
|
||||
|
||||
setButtonWidth((prev) => (prev === nextWidth ? prev : nextWidth));
|
||||
});
|
||||
|
||||
resizeObserver.observe(googleButtonRef.current);
|
||||
|
||||
return () => {
|
||||
resizeObserver.disconnect();
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
!googleClientId ||
|
||||
!scriptLoaded ||
|
||||
!googleButtonRef.current ||
|
||||
!buttonWidth
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const googleId = (window as GoogleWindow).google?.accounts?.id;
|
||||
|
||||
if (!googleId) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!googleInitializedRef.current) {
|
||||
googleId.initialize({
|
||||
client_id: googleClientId,
|
||||
callback: async ({ credential }) => {
|
||||
if (!credential) {
|
||||
setStatus({
|
||||
error: true,
|
||||
msg: "Google login did not return a token.",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
await submitGoogleLogin(credential);
|
||||
},
|
||||
});
|
||||
|
||||
googleInitializedRef.current = true;
|
||||
}
|
||||
|
||||
googleButtonRef.current.innerHTML = "";
|
||||
googleId.renderButton(googleButtonRef.current, {
|
||||
theme: "outline",
|
||||
size: "large",
|
||||
text: "signin_with",
|
||||
shape: "rectangular",
|
||||
width: Math.min(buttonWidth, 400),
|
||||
});
|
||||
setGoogleReady(true);
|
||||
}, [buttonWidth, scriptLoaded, status, submitGoogleLogin]);
|
||||
|
||||
const handleButtonClick = () => {
|
||||
if (loading) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!googleClientId) {
|
||||
setStatus({
|
||||
error: true,
|
||||
msg: "Google login is not configured yet.",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (!googleInitializedRef.current) {
|
||||
setStatus({
|
||||
error: true,
|
||||
msg: "Google login is still loading. Please try again.",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
loading,
|
||||
handleButtonClick,
|
||||
googleClientId,
|
||||
googleButtonRef,
|
||||
googleReady,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
import Button from "@/src/components/twui/layout/Button";
|
||||
import Img from "@/src/components/twui/layout/Img";
|
||||
import Row from "@/src/components/twui/layout/Row";
|
||||
import type useFormInit from "@/src/hooks/use-form-init";
|
||||
import type { LoginFormObject } from "@/src/types";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
import useGoogleLoginInit from "../../(hooks)/use-google-login-init";
|
||||
|
||||
export default function GoogleLogin(
|
||||
init: ReturnType<typeof useFormInit<LoginFormObject>>,
|
||||
) {
|
||||
const {
|
||||
loading,
|
||||
handleButtonClick,
|
||||
googleClientId,
|
||||
googleButtonRef,
|
||||
googleReady,
|
||||
} = useGoogleLoginInit(init);
|
||||
|
||||
return (
|
||||
<Row className="w-full relative justify-center">
|
||||
<Button
|
||||
title="Login with Google"
|
||||
type="button"
|
||||
variant="outlined"
|
||||
color="gray"
|
||||
className="w-full py-3 relative"
|
||||
loading={loading}
|
||||
onClick={handleButtonClick}
|
||||
>
|
||||
<Img
|
||||
alt="Google Logo Icon"
|
||||
src={`/icons/google.png`}
|
||||
size={17}
|
||||
/>
|
||||
<Row>
|
||||
<span>Login with Google</span>
|
||||
</Row>
|
||||
</Button>
|
||||
{googleClientId ? (
|
||||
<div
|
||||
ref={googleButtonRef}
|
||||
aria-hidden
|
||||
className={twMerge(
|
||||
"absolute inset-0 z-10 opacity-0",
|
||||
googleReady && !loading
|
||||
? "pointer-events-auto"
|
||||
: "pointer-events-none",
|
||||
)}
|
||||
/>
|
||||
) : null}
|
||||
</Row>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
import Form from "@/src/components/twui/form/Form";
|
||||
import LoadingOverlay from "@/src/components/twui/elements/LoadingOverlay";
|
||||
import Row from "@/src/components/twui/layout/Row";
|
||||
import Divider from "@/src/components/twui/layout/Divider";
|
||||
import Span from "@/src/components/twui/layout/Span";
|
||||
import useFormInit from "@/src/hooks/use-form-init";
|
||||
import { type LoginFormObject } from "@/src/types";
|
||||
import LoginShell from "@/src/components/general/login-shell";
|
||||
import GoogleLogin from "./google-login";
|
||||
import LoginFormEmailUsername from "./login-form-email-username";
|
||||
import submitLoginForm from "../../(functions)/submit-login-form";
|
||||
import LoginFormAction from "./login-form-action";
|
||||
|
||||
export default function LoginForm() {
|
||||
const init = useFormInit<LoginFormObject>();
|
||||
const { loading, status } = init;
|
||||
|
||||
return (
|
||||
<>
|
||||
<GoogleLogin {...init} />
|
||||
<Row className="w-full flex-nowrap gap-4 -my-2">
|
||||
<Divider />
|
||||
<Span className="text-sm font-semibold opacity-50">OR</Span>
|
||||
<Divider />
|
||||
</Row>
|
||||
<Form
|
||||
className="w-full flex items-center justify-center relative gap-4"
|
||||
onSubmit={() => {
|
||||
submitLoginForm(init);
|
||||
}}
|
||||
>
|
||||
{loading && <LoadingOverlay />}
|
||||
<LoginFormEmailUsername {...init} />
|
||||
<LoginFormAction {...init} />
|
||||
</Form>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import Input from "@/src/components/twui/form/Input";
|
||||
import Button from "@/src/components/twui/layout/Button";
|
||||
import useFormInit from "@/src/hooks/use-form-init";
|
||||
import { type LoginFormObject } from "@/src/types";
|
||||
|
||||
export default function LoginFormAction({}: ReturnType<
|
||||
typeof useFormInit<LoginFormObject>
|
||||
>) {
|
||||
return (
|
||||
<>
|
||||
<Button title="Submit Login Form" type="submit" className="w-full">
|
||||
Get Login Code
|
||||
</Button>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
import Input from "@/src/components/twui/form/Input";
|
||||
import useFormInit from "@/src/hooks/use-form-init";
|
||||
import { type LoginFormObject } from "@/src/types";
|
||||
|
||||
export default function LoginFormEmailUsername({
|
||||
setForm,
|
||||
}: ReturnType<typeof useFormInit<LoginFormObject>>) {
|
||||
return (
|
||||
<Input
|
||||
type="email"
|
||||
placeholder="Email or Username"
|
||||
onChange={(e) => {
|
||||
setForm((prev) => ({
|
||||
...prev,
|
||||
username_or_email: e.target.value,
|
||||
}));
|
||||
}}
|
||||
showLabel
|
||||
required
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
import Section from "@/src/components/twui/layout/Section";
|
||||
import Container from "@/src/components/twui/layout/Container";
|
||||
import Stack from "@/src/components/twui/layout/Stack";
|
||||
import H1 from "@/src/components/twui/layout/H1";
|
||||
import Span from "@/src/components/twui/layout/Span";
|
||||
import LoginShell from "@/src/components/general/login-shell";
|
||||
|
||||
export default function Hero() {
|
||||
// return <LoginShell></LoginShell>;
|
||||
return (
|
||||
<Section className="">
|
||||
<Container className="relative z-10">
|
||||
<Stack className="w-full items-center max-w-3xl mx-auto gap-7">
|
||||
<Span className="htag">Welcome Back Admin</Span>
|
||||
<H1 className="text-center font-bold">
|
||||
Login to your account
|
||||
</H1>
|
||||
</Stack>
|
||||
</Container>
|
||||
</Section>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import Stack from "@/src/components/twui/layout/Stack";
|
||||
import LoginForm from "../(partials)/login-form";
|
||||
import Span from "@/src/components/twui/layout/Span";
|
||||
import ArrowedLink from "@/src/components/twui/layout/ArrowedLink";
|
||||
|
||||
export default function LoginFormSection() {
|
||||
return (
|
||||
<Stack className="w-full items-center max-w-3xl mx-auto gap-7">
|
||||
<LoginForm />
|
||||
<Stack className="gap-2 items-center">
|
||||
<Span variant="faded">Received an SSO code already?</Span>
|
||||
<ArrowedLink
|
||||
link={{
|
||||
title: "Complete Login",
|
||||
url: "/auth/sso",
|
||||
}}
|
||||
/>
|
||||
</Stack>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import grabUsers from "@/src/functions/backend/db/users/grab-users";
|
||||
import type { BunextPageServerFn } from "@moduletrace/bunext/types";
|
||||
|
||||
export const server: BunextPageServerFn = async (ctx) => {
|
||||
const users_res = await grabUsers();
|
||||
|
||||
if (!users_res.singleRes?.id) {
|
||||
return {
|
||||
redirect: {
|
||||
destination: `/auth/signup`,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return {};
|
||||
};
|
||||
@@ -0,0 +1,11 @@
|
||||
import Hero from "./(sections)/hero";
|
||||
import LoginFormSection from "./(sections)/login-form-section";
|
||||
|
||||
export default function LoginPage() {
|
||||
return (
|
||||
<>
|
||||
{/* <Hero /> */}
|
||||
<LoginFormSection />
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
import { AppData } from "@/src/data/app-data";
|
||||
import { deleteCookies } from "@/src/utils/cookies";
|
||||
import type { BunextPageServerFn } from "@moduletrace/bunext/types";
|
||||
|
||||
export const server: BunextPageServerFn = async ({ req }) => {
|
||||
return {
|
||||
res_transform(res) {
|
||||
const new_res = res.clone();
|
||||
deleteCookies(new_res, [
|
||||
{
|
||||
name: AppData["AuthKeyCookieName"],
|
||||
httpOnly: true,
|
||||
},
|
||||
{
|
||||
name: AppData["AuthCSRFCookieName"],
|
||||
httpOnly: true,
|
||||
},
|
||||
{
|
||||
name: AppData["SSOAuthCookieName"],
|
||||
httpOnly: true,
|
||||
},
|
||||
]);
|
||||
return new_res;
|
||||
},
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,47 @@
|
||||
import type { BunextPageModuleMeta } from "@moduletrace/bunext/types";
|
||||
import Section from "@/src/components/twui/layout/Section";
|
||||
import Loading from "@/src/components/twui/elements/Loading";
|
||||
import Row from "@/src/components/twui/layout/Row";
|
||||
import Span from "@/src/components/twui/layout/Span";
|
||||
import { useEffect } from "react";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
import Logo from "@/src/components/general/logo";
|
||||
import Paper from "@/src/components/twui/elements/Paper";
|
||||
import Container from "@/src/components/twui/layout/Container";
|
||||
import Center from "@/src/components/twui/layout/Center";
|
||||
import { SiteData } from "@/src/data/site-data";
|
||||
|
||||
export default function LogoutPage() {
|
||||
useEffect(() => {
|
||||
setTimeout(() => {
|
||||
window.location.pathname = "/";
|
||||
}, 1000);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Section
|
||||
className={twMerge(
|
||||
"h-screen w-screen px-0 items-center justify-center",
|
||||
)}
|
||||
>
|
||||
<Center>
|
||||
<Paper className="w-auto p-10 items-center">
|
||||
<Logo
|
||||
text_props={{
|
||||
className: "text-dark",
|
||||
}}
|
||||
/>
|
||||
<Row>
|
||||
<Loading />
|
||||
<Span>Logging out ...</Span>
|
||||
</Row>
|
||||
</Paper>
|
||||
</Center>
|
||||
</Section>
|
||||
);
|
||||
}
|
||||
|
||||
export const meta: BunextPageModuleMeta = {
|
||||
title: `Logging Out | ${SiteData["SiteName"]}`,
|
||||
description: `You are being logged out and redirected to the homepage.`,
|
||||
};
|
||||
@@ -0,0 +1,25 @@
|
||||
import grabUsers from "@/src/functions/backend/db/users/grab-users";
|
||||
import type { PagePropsType } from "@/src/types";
|
||||
import type { BunextPageServerFn } from "@moduletrace/bunext/types";
|
||||
|
||||
export const server: BunextPageServerFn<PagePropsType> = async (ctx) => {
|
||||
if (ctx.props.user?.id) {
|
||||
return {
|
||||
redirect: {
|
||||
destination: `/admin`,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const users_res = await grabUsers();
|
||||
|
||||
if (users_res.singleRes?.id) {
|
||||
return {
|
||||
redirect: {
|
||||
destination: `/auth/login`,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return {};
|
||||
};
|
||||
@@ -0,0 +1,19 @@
|
||||
import type { BunextPageModuleMeta } from "@moduletrace/bunext/types";
|
||||
import Stack from "@/src/components/twui/layout/Stack";
|
||||
import UserForm from "@/src/components/general/user-form";
|
||||
import { SiteData } from "@/src/data/site-data";
|
||||
import H1 from "@/src/components/twui/layout/H1";
|
||||
|
||||
export default function LogoutPage() {
|
||||
return (
|
||||
<Stack className="items-center gap-10">
|
||||
<H1 className="text-center text-xl!">Create Admin User</H1>
|
||||
<UserForm is_first_user />
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
export const meta: BunextPageModuleMeta = {
|
||||
title: `Create Super Admin Account | ${SiteData["SiteName"]}`,
|
||||
description: `Create Super Admin Account`,
|
||||
};
|
||||
@@ -0,0 +1,73 @@
|
||||
import Form from "@/src/components/twui/form/Form";
|
||||
import LoadingOverlay from "@/src/components/twui/elements/LoadingOverlay";
|
||||
import Input from "@/src/components/twui/form/Input";
|
||||
import Button from "@/src/components/twui/layout/Button";
|
||||
import useFormInit from "@/src/hooks/use-form-init";
|
||||
import fetchApi from "@/src/components/twui/utils/fetch/fetchApi";
|
||||
import { type ApiReqParams, type SSOFormObject } from "@/src/types";
|
||||
import type { APIResponseObject } from "@moduletrace/bunext/types";
|
||||
import LoginShell from "@/src/components/general/login-shell";
|
||||
|
||||
export default function SSOForm() {
|
||||
const init = useFormInit<SSOFormObject>();
|
||||
const { form, loading, setForm, setLoading, status, setStatus } = init;
|
||||
|
||||
const submitSSOLoginForm = async () => {
|
||||
setLoading(true);
|
||||
|
||||
const res = await fetchApi<ApiReqParams, APIResponseObject>(
|
||||
`/api/auth/login`,
|
||||
{
|
||||
method: "POST",
|
||||
body: {
|
||||
sso_code: form.code,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
if (res.success) {
|
||||
window.location.pathname = `/admin`;
|
||||
} else {
|
||||
setStatus({
|
||||
error: true,
|
||||
msg: res.msg || "Login Failed",
|
||||
});
|
||||
console.log("res", res);
|
||||
console.log("form", form);
|
||||
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<LoginShell status={status}>
|
||||
<Form
|
||||
className="w-full flex items-center justify-center relative gap-4"
|
||||
onSubmit={submitSSOLoginForm}
|
||||
>
|
||||
{loading && <LoadingOverlay />}
|
||||
|
||||
<Input
|
||||
placeholder="SSO Code"
|
||||
onChange={(e) => {
|
||||
setForm((prev) => ({
|
||||
...prev,
|
||||
code: e.target.value,
|
||||
}));
|
||||
}}
|
||||
showLabel
|
||||
required
|
||||
/>
|
||||
<Button
|
||||
title="Submit Login Form"
|
||||
type="submit"
|
||||
className="w-full"
|
||||
>
|
||||
Complete Login
|
||||
</Button>
|
||||
</Form>
|
||||
</LoginShell>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
import Section from "@/src/components/twui/layout/Section";
|
||||
import Container from "@/src/components/twui/layout/Container";
|
||||
import Stack from "@/src/components/twui/layout/Stack";
|
||||
import H1 from "@/src/components/twui/layout/H1";
|
||||
import Span from "@/src/components/twui/layout/Span";
|
||||
|
||||
export default function Hero() {
|
||||
return (
|
||||
<Section className="hero-section">
|
||||
<Container className="relative z-10">
|
||||
<Stack className="w-full items-center max-w-3xl mx-auto gap-7">
|
||||
<Span className="htag">SSO Login</Span>
|
||||
<H1 className="text-center font-bold">
|
||||
Complete your login
|
||||
</H1>
|
||||
</Stack>
|
||||
</Container>
|
||||
</Section>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
import Section from "@/src/components/twui/layout/Section";
|
||||
import Container from "@/src/components/twui/layout/Container";
|
||||
import Stack from "@/src/components/twui/layout/Stack";
|
||||
import SSOForm from "../(partials)/sso-form";
|
||||
import Span from "@/src/components/twui/layout/Span";
|
||||
import ArrowedLink from "@/src/components/twui/layout/ArrowedLink";
|
||||
|
||||
export default function SSOFormSection() {
|
||||
return (
|
||||
<Section className="py-0!">
|
||||
<Container className="relative z-10 py-20 -mt-28">
|
||||
<Stack className="w-full items-center max-w-3xl mx-auto gap-7">
|
||||
<SSOForm />
|
||||
<Stack className="gap-2 items-center">
|
||||
<Span variant="faded">
|
||||
Login Failed or Code Expired?
|
||||
</Span>
|
||||
<ArrowedLink
|
||||
link={{
|
||||
title: "Login Again",
|
||||
url: "/auth/login",
|
||||
}}
|
||||
/>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Container>
|
||||
</Section>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import type { BunextPageModuleMeta } from "@moduletrace/bunext/types";
|
||||
import Hero from "./(sections)/hero";
|
||||
import SSOFormSection from "./(sections)/sso-form-section";
|
||||
import { SiteData } from "@/src/data/site-data";
|
||||
|
||||
export default function SSOPage() {
|
||||
return (
|
||||
<>
|
||||
<Hero />
|
||||
<SSOFormSection />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export const meta: BunextPageModuleMeta = {
|
||||
title: `Sign In | ${SiteData["SiteName"]}`,
|
||||
description: `Sign in to your account to access the admin dashboard.`,
|
||||
};
|
||||
@@ -0,0 +1,9 @@
|
||||
import type { BunextPageServerFn } from "@moduletrace/bunext/types";
|
||||
|
||||
export const server: BunextPageServerFn = async (ctx) => {
|
||||
return {
|
||||
redirect: {
|
||||
destination: `/auth/login`,
|
||||
},
|
||||
};
|
||||
};
|
||||
Executable
+16
@@ -0,0 +1,16 @@
|
||||
import type { BunextPageModuleMeta } from "@moduletrace/bunext/types";
|
||||
import Hero from "./(sections)/hero";
|
||||
import { SiteData } from "../data/site-data";
|
||||
|
||||
export default function Home() {
|
||||
return (
|
||||
<>
|
||||
<Hero />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export const meta: BunextPageModuleMeta = {
|
||||
title: `Homepage | ${SiteData["SiteName"]}`,
|
||||
description: "Creating Smiles.",
|
||||
};
|
||||
@@ -0,0 +1,245 @@
|
||||
import Section from "@/src/components/twui/layout/Section";
|
||||
import Container from "@/src/components/twui/layout/Container";
|
||||
import Stack from "@/src/components/twui/layout/Stack";
|
||||
import H2 from "@/src/components/twui/layout/H2";
|
||||
import { SiteData } from "@/src/data/site-data";
|
||||
|
||||
export default function PrivacySection() {
|
||||
const siteName = SiteData["SiteName"];
|
||||
const siteUrl = SiteData["SiteURL"];
|
||||
|
||||
return (
|
||||
<Section>
|
||||
<Container>
|
||||
<Stack className="w-full max-w-4xl mx-auto gap-6 [&_p]:leading-relaxed [&_p]:text-lg">
|
||||
<p>
|
||||
At {siteName}, accessible from {siteUrl}, one of our
|
||||
main priorities is the privacy of our visitors. This
|
||||
Privacy Policy document contains types of information
|
||||
that is collected and recorded by {siteName} and how we
|
||||
use it.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
If you have additional questions or require more
|
||||
information about our Privacy Policy, do not hesitate to
|
||||
contact us.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
This Privacy Policy applies only to our online
|
||||
activities and is valid for visitors to our website with
|
||||
regards to the information that they shared and/or
|
||||
collect in {siteName}. This policy is not applicable to
|
||||
any information collected offline or via channels other
|
||||
than this website.
|
||||
</p>
|
||||
|
||||
<H2>Consent</H2>
|
||||
<p>
|
||||
By using our website, you hereby consent to our Privacy
|
||||
Policy and agree to its terms.
|
||||
</p>
|
||||
|
||||
<H2>Information we collect</H2>
|
||||
<p>
|
||||
The personal information that you are asked to provide,
|
||||
and the reasons why you are asked to provide it, will be
|
||||
made clear to you at the point we ask you to provide
|
||||
your personal information.
|
||||
</p>
|
||||
<p>
|
||||
If you contact us directly, we may receive additional
|
||||
information about you such as your name, email address,
|
||||
phone number, the contents of the message and/or
|
||||
attachments you may send us, and any other information
|
||||
you may choose to provide.
|
||||
</p>
|
||||
<p>
|
||||
When you register for an Account, we may ask for your
|
||||
contact information, including items such as name,
|
||||
company name, address, email address, and telephone
|
||||
number.
|
||||
</p>
|
||||
|
||||
<H2>How we use your information</H2>
|
||||
<p>
|
||||
We use the information we collect in various ways,
|
||||
including to:
|
||||
</p>
|
||||
<ul className="list-disc pl-6 flex flex-col gap-2">
|
||||
<li>Provide, operate, and maintain our website</li>
|
||||
<li>Improve, personalize, and expand our website</li>
|
||||
<li>Understand and analyze how you use our website</li>
|
||||
<li>
|
||||
Develop new products, services, features, and
|
||||
functionality
|
||||
</li>
|
||||
<li>
|
||||
Communicate with you, either directly or through one
|
||||
of our partners, including for customer service, to
|
||||
provide you with updates and other information
|
||||
relating to the website, and for marketing and
|
||||
promotional purposes
|
||||
</li>
|
||||
<li>Send you emails</li>
|
||||
<li>Find and prevent fraud</li>
|
||||
</ul>
|
||||
|
||||
<H2>Log Files</H2>
|
||||
<p>
|
||||
{siteName} follows a standard procedure of using log
|
||||
files. These files log visitors when they visit
|
||||
websites. All hosting companies do this and a part of
|
||||
hosting services' analytics. The information collected
|
||||
by log files include internet protocol (IP) addresses,
|
||||
browser type, Internet Service Provider (ISP), date and
|
||||
time stamp, referring/exit pages, and possibly the
|
||||
number of clicks. These are not linked to any
|
||||
information that is personally identifiable. The purpose
|
||||
of the information is for analyzing trends,
|
||||
administering the site, tracking users' movement on the
|
||||
website, and gathering demographic information.
|
||||
</p>
|
||||
|
||||
<H2>Cookies and Web Beacons</H2>
|
||||
<p>
|
||||
Like any other website, {siteName} uses "cookies". These
|
||||
cookies are used to store information including
|
||||
visitors' preferences, and the pages on the website that
|
||||
the visitor accessed or visited. The information is used
|
||||
to optimize the users' experience by customizing our web
|
||||
page content based on visitors' browser type and/or
|
||||
other information.
|
||||
</p>
|
||||
|
||||
<H2>Advertising Partners Privacy Policies</H2>
|
||||
<p>
|
||||
You may consult this list to find the Privacy Policy for
|
||||
each of the advertising partners of {siteName}.
|
||||
</p>
|
||||
<p>
|
||||
Third-party ad servers or ad networks uses technologies
|
||||
like cookies, JavaScript, or Web Beacons that are used
|
||||
in their respective advertisements and links that appear
|
||||
on {siteName}, which are sent directly to users'
|
||||
browser. They automatically receive your IP address when
|
||||
this occurs. These technologies are used to measure the
|
||||
effectiveness of their advertising campaigns and/or to
|
||||
personalize the advertising content that you see on
|
||||
websites that you visit.
|
||||
</p>
|
||||
<p>
|
||||
Note that {siteName} has no access to or control over
|
||||
these cookies that are used by third-party advertisers.
|
||||
</p>
|
||||
|
||||
<H2>Third Party Privacy Policies</H2>
|
||||
<p>
|
||||
{siteName}'s Privacy Policy does not apply to other
|
||||
advertisers or websites. Thus, we are advising you to
|
||||
consult the respective Privacy Policies of these
|
||||
third-party ad servers for more detailed information. It
|
||||
may include their practices and instructions about how
|
||||
to opt-out of certain options.
|
||||
</p>
|
||||
<p>
|
||||
You can choose to disable cookies through your
|
||||
individual browser options. To know more detailed
|
||||
information about cookie management with specific web
|
||||
browsers, it can be found at the browsers' respective
|
||||
websites.
|
||||
</p>
|
||||
|
||||
<H2>
|
||||
CCPA Privacy Rights (Do Not Sell My Personal
|
||||
Information)
|
||||
</H2>
|
||||
<p>
|
||||
Under the CCPA, among other rights, California consumers
|
||||
have the right to:
|
||||
</p>
|
||||
<p>
|
||||
Request that a business that collects a consumer's
|
||||
personal data disclose the categories and specific
|
||||
pieces of personal data that a business has collected
|
||||
about consumers.
|
||||
</p>
|
||||
<p>
|
||||
Request that a business delete any personal data about
|
||||
the consumer that a business has collected.
|
||||
</p>
|
||||
<p>
|
||||
Request that a business that sells a consumer's personal
|
||||
data, not sell the consumer's personal data.
|
||||
</p>
|
||||
<p>
|
||||
If you make a request, we have one month to respond to
|
||||
you. If you would like to exercise any of these rights,
|
||||
please contact us.
|
||||
</p>
|
||||
|
||||
<H2>GDPR Data Protection Rights</H2>
|
||||
<p>
|
||||
We would like to make sure you are fully aware of all of
|
||||
your data protection rights. Every user is entitled to
|
||||
the following:
|
||||
</p>
|
||||
<p>
|
||||
The right to access – You have the right to request
|
||||
copies of your personal data. We may charge you a small
|
||||
fee for this service.
|
||||
</p>
|
||||
<p>
|
||||
The right to rectification – You have the right to
|
||||
request that we correct any information you believe is
|
||||
inaccurate. You also have the right to request that we
|
||||
complete the information you believe is incomplete.
|
||||
</p>
|
||||
<p>
|
||||
The right to erasure – You have the right to request
|
||||
that we erase your personal data, under certain
|
||||
conditions.
|
||||
</p>
|
||||
<p>
|
||||
The right to restrict processing – You have the right to
|
||||
request that we restrict the processing of your personal
|
||||
data, under certain conditions.
|
||||
</p>
|
||||
<p>
|
||||
The right to object to processing – You have the right
|
||||
to object to our processing of your personal data, under
|
||||
certain conditions.
|
||||
</p>
|
||||
<p>
|
||||
The right to data portability – You have the right to
|
||||
request that we transfer the data that we have collected
|
||||
to another organization, or directly to you, under
|
||||
certain conditions.
|
||||
</p>
|
||||
<p>
|
||||
If you make a request, we have one month to respond to
|
||||
you. If you would like to exercise any of these rights,
|
||||
please contact us.
|
||||
</p>
|
||||
|
||||
<H2>Children's Information</H2>
|
||||
<p>
|
||||
Another part of our priority is adding protection for
|
||||
children while using the internet. We encourage parents
|
||||
and guardians to observe, participate in, and/or monitor
|
||||
and guide their online activity.
|
||||
</p>
|
||||
<p>
|
||||
{siteName} does not knowingly collect any Personal
|
||||
Identifiable Information from children under the age of
|
||||
13. If you think that your child provided this kind of
|
||||
information on our website, we strongly encourage you to
|
||||
contact us immediately and we will do our best efforts
|
||||
to promptly remove such information from our records.
|
||||
</p>
|
||||
</Stack>
|
||||
</Container>
|
||||
</Section>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import type { BunextPageModuleMeta } from "@moduletrace/bunext/types";
|
||||
import PrivacySection from "./(sections)/privacy-section";
|
||||
import InnerPageHeroSection from "@/src/components/general/inner-page-hero-section";
|
||||
import { SiteData } from "@/src/data/site-data";
|
||||
|
||||
export default function PrivacyPage() {
|
||||
return (
|
||||
<>
|
||||
<InnerPageHeroSection title="Privacy Policy" sub_title="Legal" />
|
||||
|
||||
<PrivacySection />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export const meta: BunextPageModuleMeta = {
|
||||
title: `Privacy Policy | ${SiteData["SiteName"]}`,
|
||||
description: `Read our privacy policy to understand how we collect, use, and protect your personal information.`,
|
||||
};
|
||||
@@ -0,0 +1,338 @@
|
||||
import Section from "@/src/components/twui/layout/Section";
|
||||
import Container from "@/src/components/twui/layout/Container";
|
||||
import Stack from "@/src/components/twui/layout/Stack";
|
||||
import H3 from "@/src/components/twui/layout/H3";
|
||||
import { SiteData } from "@/src/data/site-data";
|
||||
|
||||
export default function TermsSection() {
|
||||
const siteName = SiteData["SiteName"];
|
||||
const siteUrl = SiteData["SiteURL"];
|
||||
|
||||
return (
|
||||
<Section className="bg-white!">
|
||||
<Container>
|
||||
<Stack className="w-full max-w-4xl mx-auto gap-6 [&_p]:leading-relaxed [&_p]:text-lg">
|
||||
<p>
|
||||
These terms and conditions outline the rules and
|
||||
regulations for the use of {siteName}'s Website, located
|
||||
at {siteUrl}.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
By accessing this website we assume you accept these
|
||||
terms and conditions. Do not continue to use {siteName}{" "}
|
||||
if you do not agree to take all of the terms and
|
||||
conditions stated on this page.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
The following terminology applies to these Terms and
|
||||
Conditions, Privacy Statement and Disclaimer Notice and
|
||||
all Agreements: "Client", "You" and "Your" refers to
|
||||
you, the person log on this website and compliant to the
|
||||
Company's terms and conditions. "The Company",
|
||||
"Ourselves", "We", "Our" and "Us", refers to our
|
||||
Company. "Party", "Parties", or "Us", refers to both the
|
||||
Client and ourselves. All terms refer to the offer,
|
||||
acceptance and consideration of payment necessary to
|
||||
undertake the process of our assistance to the Client in
|
||||
the most appropriate manner for the express purpose of
|
||||
meeting the Client's needs in respect of provision of
|
||||
the Company's stated services, in accordance with and
|
||||
subject to, prevailing law of Nigeria. Any use of the
|
||||
above terminology or other words in the singular,
|
||||
plural, capitalization and/or he/she or they, are taken
|
||||
as interchangeable and therefore as referring to same.
|
||||
</p>
|
||||
|
||||
<H3>Cookies</H3>
|
||||
<p>
|
||||
We employ the use of cookies. By accessing {siteName},
|
||||
you agreed to use cookies in agreement with the
|
||||
{siteName}'s Privacy Policy.
|
||||
</p>
|
||||
<p>
|
||||
Most interactive websites use cookies to let us retrieve
|
||||
the user's details for each visit. Cookies are used by
|
||||
our website to enable the functionality of certain areas
|
||||
to make it easier for people visiting our website. Some
|
||||
of our affiliate/advertising partners may also use
|
||||
cookies.
|
||||
</p>
|
||||
|
||||
<H3>License</H3>
|
||||
<p>
|
||||
Unless otherwise stated, {siteName} and/or its licensors
|
||||
own the intellectual property rights for all material on{" "}
|
||||
{siteName}. All intellectual property rights are
|
||||
reserved. You may access this from {siteName} for your
|
||||
own personal use subjected to restrictions set in these
|
||||
terms and conditions.
|
||||
</p>
|
||||
<p>You must not:</p>
|
||||
<ul className="list-disc pl-6 flex flex-col gap-2">
|
||||
<li>Republish material from {siteName}</li>
|
||||
<li>
|
||||
Sell, rent or sub-license material from {siteName}
|
||||
</li>
|
||||
<li>
|
||||
Reproduce, duplicate or copy material from{" "}
|
||||
{siteName}
|
||||
</li>
|
||||
<li>Redistribute content from {siteName}</li>
|
||||
</ul>
|
||||
|
||||
<p>This Agreement shall begin on the date hereof.</p>
|
||||
|
||||
<p>
|
||||
Parts of this website offer an opportunity for users to
|
||||
post and exchange opinions and information in certain
|
||||
areas of the website. {siteName} does not filter, edit,
|
||||
publish or review Comments prior to their presence on
|
||||
the website. Comments do not reflect the views and
|
||||
opinions of {siteName},its agents and/or affiliates.
|
||||
Comments reflect the views and opinions of the person
|
||||
who post their views and opinions. To the extent
|
||||
permitted by applicable laws, {siteName} shall not be
|
||||
liable for the Comments or for any liability, damages or
|
||||
expenses caused and/or suffered as a result of any use
|
||||
of and/or posting of and/or appearance of the Comments
|
||||
on this website.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
{siteName} reserves the right to monitor all Comments
|
||||
and to remove any Comments which can be considered
|
||||
inappropriate, offensive or causes breach of these Terms
|
||||
and Conditions.
|
||||
</p>
|
||||
|
||||
<p>You warrant and represent that:</p>
|
||||
<ul className="list-disc pl-6 flex flex-col gap-2">
|
||||
<li>
|
||||
You are entitled to post the Comments on our website
|
||||
and have all necessary licenses and consents to do
|
||||
so;
|
||||
</li>
|
||||
<li>
|
||||
The Comments do not invade any intellectual property
|
||||
right, including without limitation copyright,
|
||||
patent or trademark of any third party;
|
||||
</li>
|
||||
<li>
|
||||
The Comments do not contain any defamatory,
|
||||
libelous, offensive, indecent or otherwise unlawful
|
||||
material which is an invasion of privacy
|
||||
</li>
|
||||
<li>
|
||||
The Comments will not be used to solicit or promote
|
||||
business or custom or present commercial activities
|
||||
or unlawful activity.
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<p>
|
||||
You hereby grant {siteName} a non-exclusive license to
|
||||
use, reproduce, edit and authorize others to use,
|
||||
reproduce and edit any of your Comments in any and all
|
||||
forms, formats or media.
|
||||
</p>
|
||||
|
||||
<H3>Hyperlinking to our Content</H3>
|
||||
<p>
|
||||
The following organizations may link to our Website
|
||||
without prior written approval:
|
||||
</p>
|
||||
<ul className="list-disc pl-6 flex flex-col gap-2">
|
||||
<li>Government agencies;</li>
|
||||
<li>Search engines;</li>
|
||||
<li>News organizations;</li>
|
||||
<li>
|
||||
Online directory distributors may link to our
|
||||
Website in the same manner as they hyperlink to the
|
||||
Websites of other listed businesses; and
|
||||
</li>
|
||||
<li>
|
||||
System wide Accredited Businesses except soliciting
|
||||
non-profit organizations, charity shopping malls,
|
||||
and charity fundraising groups which may not
|
||||
hyperlink to our Web site.
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<p>
|
||||
These organizations may link to our home page, to
|
||||
publications or to other Website information so long as
|
||||
the link: (a) is not in any way deceptive; (b) does not
|
||||
falsely imply sponsorship, endorsement or approval of
|
||||
the linking party and its products and/or services; and
|
||||
(c) fits within the context of the linking party's site.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
We may consider and approve other link requests from the
|
||||
following types of organizations:
|
||||
</p>
|
||||
<ul className="list-disc pl-6 flex flex-col gap-2">
|
||||
<li>
|
||||
commonly-known consumer and/or business information
|
||||
sources;
|
||||
</li>
|
||||
<li>dot.com community sites;</li>
|
||||
<li>
|
||||
associations or other groups representing charities;
|
||||
</li>
|
||||
<li>online directory distributors;</li>
|
||||
<li>internet portals;</li>
|
||||
<li>accounting, law and consulting firms; and</li>
|
||||
<li>
|
||||
educational institutions and trade associations.
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<p>
|
||||
We will approve link requests from these organizations
|
||||
if we decide that: (a) the link would not make us look
|
||||
unfavorably to ourselves or to our accredited
|
||||
businesses; (b) the organization does not have any
|
||||
negative records with us; (c) the benefit to us from the
|
||||
visibility of the hyperlink compensates the absence of{" "}
|
||||
{siteName}; and (d) the link is in the context of
|
||||
general resource information.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
These organizations may link to our home page so long as
|
||||
the link: (a) is not in any way deceptive; (b) does not
|
||||
falsely imply sponsorship, endorsement or approval of
|
||||
the linking party and its products or services; and (c)
|
||||
fits within the context of the linking party's site.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
If you are one of the organizations listed in paragraph
|
||||
2 above and are interested in linking to our website,
|
||||
you must inform us by sending an e-mail to {siteName}.
|
||||
Please include your name, your organization name,
|
||||
contact information as well as the URL of your site, a
|
||||
list of any URLs from which you intend to link to our
|
||||
Website, and a list of the URLs on our site to which you
|
||||
would like to link. Wait 2-3 weeks for a response.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Approved organizations may hyperlink to our Website as
|
||||
follows:
|
||||
</p>
|
||||
<ul className="list-disc pl-6 flex flex-col gap-2">
|
||||
<li>By use of our corporate name; or</li>
|
||||
<li>
|
||||
By use of the uniform resource locator being linked
|
||||
to; or
|
||||
</li>
|
||||
<li>
|
||||
By use of any other description of our Website being
|
||||
linked to that makes sense within the context and
|
||||
format of content on the linking party's site.
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<p>
|
||||
No use of {siteName}'s logo or other artwork will be
|
||||
allowed for linking absent a trademark license
|
||||
agreement.
|
||||
</p>
|
||||
|
||||
<H3>iFrames</H3>
|
||||
<p>
|
||||
Without prior approval and written permission, you may
|
||||
not create frames around our Webpages that alter in any
|
||||
way the visual presentation or appearance of our
|
||||
Website.
|
||||
</p>
|
||||
|
||||
<H3>Content Liability</H3>
|
||||
<p>
|
||||
We shall not be hold responsible for any content that
|
||||
appears on your Website. You agree to protect and defend
|
||||
us against all claims that is rising on your Website. No
|
||||
link(s) should appear on any Website that may be
|
||||
interpreted as libelous, obscene or criminal, or which
|
||||
infringes, otherwise violates, or advocates the
|
||||
infringement or other violation of, any third party
|
||||
rights.
|
||||
</p>
|
||||
|
||||
<H3>Your Privacy</H3>
|
||||
<p>Please read our Privacy Policy.</p>
|
||||
|
||||
<H3>Reservation of Rights</H3>
|
||||
<p>
|
||||
We reserve the right to request that you remove all
|
||||
links or any particular link to our Website. You approve
|
||||
to immediately remove all links to our Website upon
|
||||
request. We also reserve the right to amen these terms
|
||||
and conditions and it's linking policy at any time. By
|
||||
continuously linking to our Website, you agree to be
|
||||
bound to and follow these linking terms and conditions.
|
||||
</p>
|
||||
|
||||
<H3>Removal of links from our website</H3>
|
||||
<p>
|
||||
If you find any link on our Website that is offensive
|
||||
for any reason, you are free to contact and inform us
|
||||
any moment. We will consider requests to remove links
|
||||
but we are not obligated to or so or to respond to you
|
||||
directly.
|
||||
</p>
|
||||
<p>
|
||||
We do not ensure that the information on this website is
|
||||
correct, we do not warrant its completeness or accuracy;
|
||||
nor do we promise to ensure that the website remains
|
||||
available or that the material on the website is kept up
|
||||
to date.
|
||||
</p>
|
||||
|
||||
<H3>Disclaimer</H3>
|
||||
<p>
|
||||
To the maximum extent permitted by applicable law, we
|
||||
exclude all representations, warranties and conditions
|
||||
relating to our website and the use of this website.
|
||||
Nothing in this disclaimer will:
|
||||
</p>
|
||||
<ul className="list-disc pl-6 flex flex-col gap-2">
|
||||
<li>
|
||||
limit or exclude our or your liability for death or
|
||||
personal injury;
|
||||
</li>
|
||||
<li>
|
||||
limit or exclude our or your liability for fraud or
|
||||
fraudulent misrepresentation;
|
||||
</li>
|
||||
<li>
|
||||
limit any of our or your liabilities in any way that
|
||||
is not permitted under applicable law; or
|
||||
</li>
|
||||
<li>
|
||||
exclude any of our or your liabilities that may not
|
||||
be excluded under applicable law.
|
||||
</li>
|
||||
</ul>
|
||||
<p>
|
||||
The limitations and prohibitions of liability set in
|
||||
this Section and elsewhere in this disclaimer: (a) are
|
||||
subject to the preceding paragraph; and (b) govern all
|
||||
liabilities arising under the disclaimer, including
|
||||
liabilities arising in contract, in tort and for breach
|
||||
of statutory duty.
|
||||
</p>
|
||||
<p>
|
||||
As long as the website and the information and services
|
||||
on the website are provided free of charge, we will not
|
||||
be liable for any loss or damage of any nature.
|
||||
</p>
|
||||
</Stack>
|
||||
</Container>
|
||||
</Section>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
import type { BunextPageModuleMeta } from "@moduletrace/bunext/types";
|
||||
import TermsSection from "./(sections)/terms-section";
|
||||
import InnerPageHeroSection from "@/src/components/general/inner-page-hero-section";
|
||||
import { SiteData } from "@/src/data/site-data";
|
||||
|
||||
export default function TermsPage() {
|
||||
return (
|
||||
<>
|
||||
<InnerPageHeroSection
|
||||
title="Terms and Conditions"
|
||||
sub_title="Legal"
|
||||
/>
|
||||
|
||||
<TermsSection />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export const meta: BunextPageModuleMeta = {
|
||||
title: `Terms and Conditions | ${SiteData["SiteName"]}`,
|
||||
description: `Read our terms and conditions for using the ${SiteData["SiteName"]} website.`,
|
||||
};
|
||||
Reference in New Issue
Block a user