First Commit

This commit is contained in:
2026-09-12 13:56:36 +01:00
commit 4d75af0418
426 changed files with 36452 additions and 0 deletions
+21
View File
@@ -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>
);
}
+30
View File
@@ -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>
);
}
+44
View File
@@ -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>
);
}
+45
View File
@@ -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",
};
+44
View File
@@ -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;
+89
View File
@@ -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=""
/>
</>
);
}
+20
View File
@@ -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"]}`,
};
+16
View File
@@ -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`,
};
+16
View File
@@ -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,
};
}
+108
View File
@@ -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,
};
}
};
+58
View File
@@ -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}`,
};
}
};
+48
View File
@@ -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,
};
}
};
+87
View File
@@ -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,
};
}
};
+133
View File
@@ -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,
};
}
};
+74
View File
@@ -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,
};
}
};
+95
View File
@@ -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,
};
}
};
+52
View File
@@ -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,
};
}
};
+10
View File
@@ -0,0 +1,10 @@
import type { BunextPageServerFn } from "@moduletrace/bunext/types";
export const server: BunextPageServerFn = async () => {
return {
redirect: {
destination: "/auth/login",
permanent: false,
},
};
};
+31
View File
@@ -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
/>
);
}
+22
View File
@@ -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>
);
}
+16
View File
@@ -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 {};
};
+11
View File
@@ -0,0 +1,11 @@
import Hero from "./(sections)/hero";
import LoginFormSection from "./(sections)/login-form-section";
export default function LoginPage() {
return (
<>
{/* <Hero /> */}
<LoginFormSection />
</>
);
}
+26
View File
@@ -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;
},
};
};
+47
View File
@@ -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.`,
};
+25
View File
@@ -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 {};
};
+19
View File
@@ -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>
</>
);
}
+20
View File
@@ -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>
);
}
+18
View File
@@ -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.`,
};
+9
View File
@@ -0,0 +1,9 @@
import type { BunextPageServerFn } from "@moduletrace/bunext/types";
export const server: BunextPageServerFn = async (ctx) => {
return {
redirect: {
destination: `/auth/login`,
},
};
};
+16
View File
@@ -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>
);
}
+19
View File
@@ -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>
);
}
+22
View File
@@ -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.`,
};