Skip to content

Commit

Permalink
feat: custom domain (EddieHubCommunity#9436)
Browse files Browse the repository at this point in the history
* feat: use custom domain

* feat: custom domain

* fix: profile model domain property

* feat: allow user to set custom domain

* fix: efficiency improvements to middleware

* fix: domain not used

* fix: default value for domain

* fix: deploy custom domain to preview

* fix: hardcoded middleware domain

* fix: debug code for middleware

* fix: hostname and domain in middleware

* fix: hostname replace all

* fix: domain api search

* fix: debugging domain api search

* fix: manage premium page form

* wip: add custom domain to vercel

* feat: premium domain add to vercel

* feat: custom domain to team + project

* fix: remove custom url protocols

* fix: extra custom domain check

* fix: custom domain ignore www

* feat: hide nav+footer if custom domain

* fix: custom domain vercel error handling

* docs: improve premium docs for dns

* fix: extra debugging for custom domain

* fix: more debugging for custom domain

* fix: even more debugging for custom domain

* fix: middleware rewrite condition

* fix: www in custom base domain

* docs: custom domain setup

* feat: custom domain vercel status

* fix: logging when there are errors

* docs: changelog feeature
  • Loading branch information
eddiejaoude authored and malay44 committed Nov 9, 2023
1 parent 5675373 commit 86486c9
Show file tree
Hide file tree
Showing 21 changed files with 626 additions and 64 deletions.
4 changes: 4 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,7 @@ STRIPE_SECRET_KEY=""
STRIPE_PREMIUM_PRICING_ID=""
STRIPE_WEBHOOK_SECRET=""
NEXT_PUBLIC_PREMIUM_SUPPORT_URL=""

VERCEL_PROJECT_ID=""
VERCEL_TEAM_ID=""
VERCEL_AUTH_TOKEN=""
2 changes: 1 addition & 1 deletion .github/workflows/vercel-preview.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ on:
workflow_dispatch:
push:
branches:
- feat-middleware
- feat-custom-domain

jobs:
deploy:
Expand Down
21 changes: 11 additions & 10 deletions components/Button.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,21 +3,22 @@ import Link from "./Link";

export default function Button({
primary = false,
disable,
disabled = false,
className,
overrideClassNames = false,
children,
...restProps
}) {
let defaultClassName =
"w-full inline-flex items-center flex-1 justify-center rounded-md border-2 border-primary-high dark:border-white hover:border-transparent px-5 py-3 text-base font-medium first-letter:bg-white transition duration-400 ease-in-out";
!disable
? (defaultClassName += primary
let defaultClassName = classNames(
"w-full inline-flex items-center flex-1 justify-center rounded-md border-2 border-primary-high dark:border-white hover:border-transparent px-5 py-3 text-base font-medium first-letter:bg-white transition duration-400 ease-in-out",
!disabled
? primary
? " text-primary-medium bg-secondary-medium hover:bg-tertiary-medium"
: " text-secondary-high dark:text-secondary-high-high hover:text-white dark:hover:text-white dark:bg-primary-low hover:bg-secondary-medium dark:hover:bg-secondary-medium")
: (defaultClassName += disable
? " border-2 border-red border shadow-sm bg-primary-low text-primary-medium cursor-not-allowed "
: " cursor-pointer");
: " text-secondary-high dark:text-secondary-high-high hover:text-white dark:hover:text-white dark:bg-primary-low hover:bg-secondary-medium dark:hover:bg-secondary-medium"
: disabled
? " border-2 border-red border shadow-sm bg-primary-low text-primary-medium cursor-not-allowed "
: " cursor-pointer",
);

const link = (
<Link
Expand All @@ -36,7 +37,7 @@ export default function Button({
className={
overrideClassNames ? className : classNames(defaultClassName, className)
}
disabled={disable}
disabled={disabled}
{...restProps}
>
{children}
Expand Down
1 change: 1 addition & 0 deletions components/form/Input.js
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ const Input = forwardRef(
id={name}
name={name}
value={value}
disabled={disabled}
onKeyDown={handleKeydown}
{...restProps}
/>
Expand Down
13 changes: 13 additions & 0 deletions components/layouts/DocsLayout.js
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,10 @@ export const navigation = [
name: "GitHub Repos with Forms",
href: "/docs/how-to-guides/repos-forms",
},
{
name: "Premium Features",
href: "/docs/how-to-guides/premium",
},
],
},
{
Expand Down Expand Up @@ -100,6 +104,15 @@ export const navigation = [
{ name: "Hacktoberfest", href: "/docs/contributing/hacktoberfest" },
],
},
{
name: "Premium",
// icon: ChartPieIcon,
children: [
{ name: "Auto", href: "/docs/premium/auto" },
{ name: "Customisation", href: "/docs/premium/customisation" },
{ name: "Custom Domain", href: "/docs/premium/domain" },
],
},
{
name: "Other",
// icon: ChartPieIcon,
Expand Down
3 changes: 3 additions & 0 deletions config/schemas/serverSchema.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ const envSchema = z.object({
NEXT_PUBLIC_VERCEL_ENV: z.string().optional(),
STRIPE_SECRET_KEY: z.string().optional(),
STRIPE_WEBHOOK_SECRET: z.string().optional(),
VERCEL_PROJECT_ID: z.string().optional(),
VERCEL_TEAM_ID: z.string().optional(),
VERCEL_AUTH_TOKEN: z.string().optional(),
});

const serverEnv = envSchema.safeParse(process.env);
Expand Down
56 changes: 55 additions & 1 deletion middleware.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
import { getToken } from "next-auth/jwt";
import { NextResponse } from "next/server";

// note: logger is not available in middleware, using console.log instead

export const config = {
matcher: [
"/",

// account management
"/account/:path*",
"/api/account/:path*",
Expand All @@ -14,10 +18,60 @@ export const config = {
};

export async function middleware(req) {
const protocol = process.env.NODE_ENV === "development" ? "http" : "https";
const hostname = req.headers.get("host");
const reqPathName = req.nextUrl.pathname;
const sessionRequired = ["/account", "/api/account"];
const adminRequired = ["/admin", "/api/admin"];
const adminUsers = process.env.ADMIN_USERS.split(",");
const reqPathName = req.nextUrl.pathname;
const hostedDomain = process.env.NEXT_PUBLIC_BASE_URL.replace(
/http:\/\/|https:\/\//,
"",
);
const hostedDomains = [hostedDomain, `www.${hostedDomain}`];

// if custom domain + on root path
if (!hostedDomains.includes(hostname) && reqPathName === "/") {
console.log(`custom domain used: "${hostname}"`);

let res;
let profile;
let url = `${
process.env.NEXT_PUBLIC_BASE_URL
}/api/search/${encodeURIComponent(hostname)}`;
try {
res = await fetch(url, {
method: "GET",
headers: {
"Content-Type": "application/json",
},
});
profile = await res.json();
} catch (e) {
console.error(url, e);
return NextResponse.error(e);
}

if (
profile?.username &&
profile.user.type === "premium" &&
profile.settings?.domain &&
profile.settings.domain === hostname
) {
console.log(
`custom domain matched "${hostname}" for username "${profile.username}" (protocol: "${protocol}")`,
);
// if match found rewrite to custom domain and display profile page
return NextResponse.rewrite(
new URL(
`/${profile.username}`,
`${protocol}://${profile.settings.domain}`,
),
);
}

console.error(`custom domain NOT matched "${hostname}"`);
}

// if not in sessionRequired or adminRequired, skip
if (
Expand Down
16 changes: 14 additions & 2 deletions models/Profile.js
Original file line number Diff line number Diff line change
Expand Up @@ -144,10 +144,22 @@ const ProfileSchema = new Schema(
type: Boolean,
default: false,
},
domain: {
type: String,
default: "",
get: (v) => v.replaceAll("|", "."),
set: (v) => v.replaceAll(".", "|"),
validator: function (v) {
return /^[^https?:\/\/](?:www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}$/.test(
v,
);
},
message: (props) => `${props.value} is not a valid domain!`,
},
},
},
{ timestamps: true },
{ timestamps: true, toJSON: { getters: true } },
);

module.exports =
mongoose.models.Profile || mongoose.model("Profile", ProfileSchema);
mongoose.models?.Profile || mongoose.model("Profile", ProfileSchema);
9 changes: 9 additions & 0 deletions pages/[username].js
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,15 @@ export async function getServerSideProps(context) {
profile.cleanBio = profile.bio;
}

// override hiding navbar and footer if custom domain matches
if (
profile.settings?.domain &&
profile.settings.domain.replaceAll("|", ".") === req.headers.host
) {
profile.settings.hideNavbar = true;
profile.settings.hideFooter = true;
}

return {
props: {
data: profile,
Expand Down
2 changes: 1 addition & 1 deletion pages/account/manage/links.js
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ export default function ManageLinks({ BASE_URL, username, links }) {
{!reorder && (
<Button
onClick={() => setReorder(true)}
disable={linkList.length < 2}
disabled={linkList.length < 2}
>
<ArrowPathIcon className="h-5 w-5 mr-2" />
REORDER
Expand Down
Loading

0 comments on commit 86486c9

Please sign in to comment.