feat: use subdomains for organizations

This commit is contained in:
swve 2023-03-02 20:04:23 +01:00
parent 600bb96603
commit fac6b57ab3
16 changed files with 57 additions and 60 deletions

3
app.py
View file

@ -1,5 +1,6 @@
import logging
from fastapi import FastAPI, Request
import re
from src.core.config.config import Settings, get_settings
from src.core.events.events import shutdown_app, startup_app
from src.main import global_router
@ -23,9 +24,11 @@ app = FastAPI(
root_path="/"
)
origin_regex = re.compile(r"^http://[\w.-]+\.localhost:3000$")
app.add_middleware(
CORSMiddleware,
allow_origin_regex=str(origin_regex.pattern),
allow_origins=["http://localhost:3000", "http://localhost:3001"],
allow_methods=["*"],
allow_credentials=True,

View file

@ -6,12 +6,6 @@ services:
- "1338:80"
volumes:
- .:/usr/learnhouse
frontend:
build: ./front
ports:
- "3001:3000"
volumes:
- ./front:/usr/learnhouse/front
mongo:
image: mongo:5.0
restart: always

View file

@ -4,9 +4,9 @@ import { default as React, useEffect, useRef } from "react";
import { useRouter } from "next/navigation";
import { getLecture } from "../../../../../../../../services/courses/lectures";
import AuthProvider from "../../../../../../../../components/Security/AuthProvider";
import EditorWrapper from "../../../../../../../../components/Editor/EditorWrapper";
import { getLecture } from "@services/courses/lectures";
import AuthProvider from "@components/Security/AuthProvider";
import EditorWrapper from "@components/Editor/EditorWrapper";
import useSWR, { mutate } from "swr";
import { getAPIUrl } from "@services/config";
import { swrFetcher } from "@services/utils/requests";

View file

@ -0,0 +1 @@
export const EDITOR = "main";

View file

@ -4,7 +4,7 @@ import Link from "next/link";
import React, { useMemo } from "react";
import Layout from "@components/UI/Layout";
import { getLecture } from "@services/courses/lectures";
import { getAPIUrl, getBackendUrl } from "@services/config";
import { getAPIUrl, getBackendUrl, getUriWithOrg } from "@services/config";
import Canva from "@components/LectureViews/DynamicCanva/DynamicCanva";
import styled from "styled-components";
import { getCourse } from "@services/courses/courses";
@ -39,7 +39,7 @@ function LecturePage(params: any) {
<LectureLayout>
<LectureTopWrapper>
<LectureThumbnail>
<Link href={`/org/${orgslug}/course/${courseid}`}>
<Link href={getUriWithOrg(orgslug,"") +`/course/${courseid}`}>
<img src={`${getBackendUrl()}content/uploads/img/${course.course.thumbnail}`} alt="" />
</Link>
</LectureThumbnail>
@ -56,7 +56,7 @@ function LecturePage(params: any) {
{chapter.lectures.map((lecture: any) => {
return (
<>
<Link href={`/org/${orgslug}/course/${courseid}/lecture/${lecture.id.replace("lecture_", "")}`}>
<Link href={getUriWithOrg(orgslug,"") +`/course/${courseid}/lecture/${lecture.id.replace("lecture_", "")}`}>
<ChapterIndicator key={lecture.id} />
</Link>{" "}
</>

View file

@ -4,7 +4,7 @@ import { closeActivity, createActivity } from "@services/courses/activity";
import Link from "next/link";
import React from "react";
import styled from "styled-components";
import { getAPIUrl, getBackendUrl } from "@services/config";
import { getAPIUrl, getBackendUrl, getUriWithOrg } from "@services/config";
import useSWR, { mutate } from "swr";
import { swrFetcher } from "@services/utils/requests";
@ -45,7 +45,7 @@ const CourseIdPage = (params: any) => {
<p>Course</p>
<h1>
{course.course.name}{" "}
<Link href={`/org/${orgslug}/course/${courseid}/edit`} rel="noopener noreferrer">
<Link href={getUriWithOrg(orgslug,"") +`/course/${courseid}/edit`} rel="noopener noreferrer">
<Pencil2Icon />
</Link>{" "}
</h1>
@ -56,7 +56,7 @@ const CourseIdPage = (params: any) => {
{chapter.lectures.map((lecture: any) => {
return (
<>
<Link href={`/org/${orgslug}/course/${courseid}/lecture/${lecture.id.replace("lecture_", "")}`}>
<Link href={getUriWithOrg(orgslug,"") +`/course/${courseid}/lecture/${lecture.id.replace("lecture_", "")}`}>
<ChapterIndicator />
</Link>{" "}
</>
@ -97,7 +97,7 @@ const CourseIdPage = (params: any) => {
<>
<p>
Lecture {lecture.name}
<Link href={`/org/${orgslug}/course/${courseid}/lecture/${lecture.id.replace("lecture_", "")}`} rel="noopener noreferrer">
<Link href={getUriWithOrg(orgslug,"") +`/course/${courseid}/lecture/${lecture.id.replace("lecture_", "")}`} rel="noopener noreferrer">
<EyeOpenIcon />
</Link>{" "}
</p>

View file

@ -44,8 +44,8 @@ const CoursesIndexPage = (params: any) => {
<button style={{ backgroundColor: "red", border: "none" }} onClick={() => deleteCourses(course.course_id)}>
Delete <Trash size={10}></Trash>
</button>
<Link href={"/org/" + orgslug + "/course/" + removeCoursePrefix(course.course_id)}>
<Link href={"/org/" + orgslug + "/course/" + removeCoursePrefix(course.course_id) + "/edit"}>
<Link href={getUriWithOrg(orgslug,"") + "/course/" + removeCoursePrefix(course.course_id)}>
<Link href={getUriWithOrg(orgslug,"") + "/course/" + removeCoursePrefix(course.course_id) + "/edit"}>
<button>
Edit <Edit2 size={10}></Edit2>
</button>

View file

@ -2,7 +2,8 @@ import "@styles/globals.css";
import { Menu } from "@components/UI/Elements/Menu";
import AuthProvider from "@components/Security/AuthProvider";
export default function RootLayout({ children }: { children: React.ReactNode }) {
export default function RootLayout({ children, params }: { children: React.ReactNode , params:any}) {
return (
<>
<AuthProvider>

View file

@ -1,5 +1,6 @@
"use client";
import { Title } from "@components/UI/Elements/Styles/Title";
import { getUriWithOrg } from "@services/config";
import Link from "next/link";
import { usePathname } from "next/navigation";
@ -10,7 +11,7 @@ const OrgHomePage = (params: any) => {
return (
<div>
<Title>Welcome {orgslug} 👋🏻</Title>
<Link href={pathname + "/courses"}>
<Link href={getUriWithOrg(orgslug,"/courses")}>
<button>See Courses </button>
</Link>
</div>

View file

@ -7,8 +7,8 @@ import { Title } from "../../components/UI/Elements/Styles/Title";
import { loginAndGetToken } from "../../services/auth/auth";
const Login = () => {
const [email, setEmail] = React.useState("");
const [password, setPassword] = React.useState("");
const [email, setEmail] = React.useState("admin@admin.admin");
const [password, setPassword] = React.useState("admin");
const router = useRouter();
const handleSubmit = (e: any) => {

View file

@ -6,7 +6,7 @@ import { Title } from "../../components/UI/Elements/Styles/Title";
import { deleteOrganizationFromBackend } from "@services/orgs";
import useSWR, { mutate } from "swr";
import { swrFetcher } from "@services/utils/requests";
import { getAPIUrl } from "@services/config";
import { getAPIUrl, getUriWithOrg } from "@services/config";
const Organizations = () => {
const { data : organizations , error } = useSWR(`${getAPIUrl()}orgs/user/page/1/limit/10`, swrFetcher)
@ -32,7 +32,7 @@ const Organizations = () => {
<div>
{organizations.map((org: any) => (
<div key={org.org_id}>
<Link href={`/org/${org.slug}`}>
<Link href={getUriWithOrg(org.slug,"/")}>
<h3>{org.name}</h3>
</Link>
<button onClick={() => deleteOrganization(org.org_id)}>Delete</button>

View file

@ -3,6 +3,7 @@ import React from "react";
import { Draggable } from "react-beautiful-dnd";
import { EyeOpenIcon, Pencil2Icon } from '@radix-ui/react-icons'
import styled from "styled-components";
import { getUriWithOrg } from "@services/config";
function Lecture(props: any) {
@ -12,13 +13,13 @@ function Lecture(props: any) {
<LectureWrapper key={props.lecture.id} {...provided.draggableProps} {...provided.dragHandleProps} ref={provided.innerRef}>
<p>{props.lecture.name} </p>
<Link
href={`/org/${props.orgslug}/course/${props.courseid}/lecture/${props.lecture.id.replace("lecture_", "")}`}
href={getUriWithOrg(props.orgslug,"")+`/course/${props.courseid}/lecture/${props.lecture.id.replace("lecture_", "")}`}
rel="noopener noreferrer">
&nbsp; <EyeOpenIcon/>
</Link>
<Link
href={`/org/${props.orgslug}/course/${props.courseid}/lecture/${props.lecture.id.replace("lecture_", "")}/edit`}
href={getUriWithOrg(props.orgslug,"") +`/course/${props.courseid}/lecture/${props.lecture.id.replace("lecture_", "")}/edit`}
rel="noopener noreferrer">
&nbsp; <Pencil2Icon/>
</Link>

View file

@ -20,11 +20,6 @@ export default function middleware(req: NextRequest) {
// Get hostname of request (e.g. demo.vercel.pub, demo.localhost:3000)
const hostname = req.headers.get("host") || "learnhouse.app";
// Only for demo purposes - remove this if you want to use your root domain as the landing page
if (hostname === "vercel.pub" || hostname === "platforms.vercel.app") {
return NextResponse.redirect("https://demo.vercel.pub");
}
/* You have to replace ".vercel.pub" with your own domain if you deploy this example under your domain.
You can also use wildcard subdomains on .vercel.app links that are associated with your Vercel team slug
in this case, our team slug is "platformize", thus *.platformize.vercel.app works. Do note that you'll
@ -34,30 +29,28 @@ export default function middleware(req: NextRequest) {
? hostname.replace(`.vercel.pub`, "").replace(`.platformize.vercel.app`, "")
: hostname.replace(`.localhost:3000`, "");
// if url starts with "/organizations" rewrite to path
/* Editor route */
if (url.pathname.match(/^\/course\/[^/]+\/lecture\/[^/]+\/edit$/)) {
url.pathname = `/_editor${url.pathname}`;
console.log("editor route", url.pathname);
return NextResponse.rewrite(url, { headers: { orgslug: currentHost } });
}
/* Organizations route */
if (url.pathname.startsWith("/organizations")) {
url.pathname = url.pathname.replace("/organizations", `/organizations${currentHost}`);
// remove localhost:3000 from url
url.pathname = url.pathname.replace(`localhost:3000`, "");
console.log(url);
return NextResponse.rewrite(url);
}
else if (url.pathname.startsWith("/org")) {
url.pathname = url.pathname.replace("/organizations", `/_orgs/${currentHost}`);
// remove localhost:3000 from url
url.pathname = `/_orgs/${currentHost}${url.pathname}`;
url.pathname = url.pathname.replace(`localhost:3000/org/`, "");
console.log(url);
return NextResponse.rewrite(url);
}
console.log("currentHost", url);
// rewrite everything else to `/_sites/[site] dynamic route
url.pathname = `/_orgs/${currentHost}${url.pathname}`;
console.log(url);
return NextResponse.rewrite(url, { headers: { "olgslug": currentHost } });
return NextResponse.rewrite(url, { headers: { olgslug: currentHost } });
}

View file

@ -10,7 +10,10 @@ interface LoginAndGetTokenResponse {
export async function loginAndGetToken(username: string, password: string): Promise<LoginAndGetTokenResponse> {
// Request Config
const HeadersConfig = new Headers({ "Content-Type": "application/x-www-form-urlencoded" , Origin: "http://localhost:3000" });
// get origin
const origin = window.location.origin;
const HeadersConfig = new Headers({ "Content-Type": "application/x-www-form-urlencoded" , Origin: origin });
const urlencoded = new URLSearchParams({ username: username, password: password });
const requestOptions: any = {
@ -28,7 +31,8 @@ export async function loginAndGetToken(username: string, password: string): Prom
}
export async function getUserInfo(token: string): Promise<any> {
const HeadersConfig = new Headers({ Authorization: `Bearer ${token}`, Origin: "http://localhost:3000" });
const origin = window.location.origin;
const HeadersConfig = new Headers({ Authorization: `Bearer ${token}`, Origin:origin });
const requestOptions: any = {
method: "GET",

View file

@ -6,16 +6,13 @@ export const getAPIUrl = () => LEARNHOUSE_API_URL;
export const getBackendUrl = () => LEARNHOUSE_BACKEND_URL;
export const getUriWithOrg = (orgslug: string, path: string) => {
return `http://localhost:3000/org/${orgslug}${path}`;
return `http://${orgslug}.localhost:3000${path}`;
};
export const getOrgFromUri = (uri: any) => {
// if url contains /org
if (uri.includes("/org/")) {
let org = uri.match(/\/org\/([\w]+)/)[1];
return org;
}
else {
return "";
}
export const getOrgFromUri = () => {
const hostname = window.location.hostname;
// get the orgslug from the hostname
const orgslug = hostname.split(".")[0];
return orgslug;
};

View file

@ -18,6 +18,8 @@ class Settings(BaseModel):
authjwt_token_location = {"cookies"}
authjwt_cookie_csrf_protect = False
authjwt_access_token_expires = False # (pre-alpha only) # TODO: set to 1 hour
authjwt_cookie_samesite = "none"
authjwt_cookie_secure = True
@AuthJWT.load_config # type: ignore