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

View file

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

View file

@ -4,9 +4,9 @@ import { default as React, useEffect, useRef } from "react";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import { getLecture } from "../../../../../../../../services/courses/lectures"; import { getLecture } from "@services/courses/lectures";
import AuthProvider from "../../../../../../../../components/Security/AuthProvider"; import AuthProvider from "@components/Security/AuthProvider";
import EditorWrapper from "../../../../../../../../components/Editor/EditorWrapper"; import EditorWrapper from "@components/Editor/EditorWrapper";
import useSWR, { mutate } from "swr"; import useSWR, { mutate } from "swr";
import { getAPIUrl } from "@services/config"; import { getAPIUrl } from "@services/config";
import { swrFetcher } from "@services/utils/requests"; 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 React, { useMemo } from "react";
import Layout from "@components/UI/Layout"; import Layout from "@components/UI/Layout";
import { getLecture } from "@services/courses/lectures"; 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 Canva from "@components/LectureViews/DynamicCanva/DynamicCanva";
import styled from "styled-components"; import styled from "styled-components";
import { getCourse } from "@services/courses/courses"; import { getCourse } from "@services/courses/courses";
@ -39,7 +39,7 @@ function LecturePage(params: any) {
<LectureLayout> <LectureLayout>
<LectureTopWrapper> <LectureTopWrapper>
<LectureThumbnail> <LectureThumbnail>
<Link href={`/org/${orgslug}/course/${courseid}`}> <Link href={getUriWithOrg(orgslug,"") +`/course/${courseid}`}>
<img src={`${getBackendUrl()}content/uploads/img/${course.course.thumbnail}`} alt="" /> <img src={`${getBackendUrl()}content/uploads/img/${course.course.thumbnail}`} alt="" />
</Link> </Link>
</LectureThumbnail> </LectureThumbnail>
@ -56,7 +56,7 @@ function LecturePage(params: any) {
{chapter.lectures.map((lecture: any) => { {chapter.lectures.map((lecture: any) => {
return ( 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} /> <ChapterIndicator key={lecture.id} />
</Link>{" "} </Link>{" "}
</> </>

View file

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

View file

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

View file

@ -2,7 +2,8 @@ import "@styles/globals.css";
import { Menu } from "@components/UI/Elements/Menu"; import { Menu } from "@components/UI/Elements/Menu";
import AuthProvider from "@components/Security/AuthProvider"; 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 ( return (
<> <>
<AuthProvider> <AuthProvider>

View file

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

View file

@ -7,8 +7,8 @@ import { Title } from "../../components/UI/Elements/Styles/Title";
import { loginAndGetToken } from "../../services/auth/auth"; import { loginAndGetToken } from "../../services/auth/auth";
const Login = () => { const Login = () => {
const [email, setEmail] = React.useState(""); const [email, setEmail] = React.useState("admin@admin.admin");
const [password, setPassword] = React.useState(""); const [password, setPassword] = React.useState("admin");
const router = useRouter(); const router = useRouter();
const handleSubmit = (e: any) => { const handleSubmit = (e: any) => {
@ -39,7 +39,7 @@ const Login = () => {
<Title>Login</Title> <Title>Login</Title>
<form> <form>
<input onChange={handleEmailChange} type="text" placeholder="email" /> <input onChange={handleEmailChange} type="text" placeholder="email" />
<input onChange={handlePasswordChange} type="password" placeholder="password" /> <input onChange={handlePasswordChange} type="password" placeholder="password" />
<button onClick={handleSubmit} type="submit"> <button onClick={handleSubmit} type="submit">
Login Login

View file

@ -6,7 +6,7 @@ import { Title } from "../../components/UI/Elements/Styles/Title";
import { deleteOrganizationFromBackend } from "@services/orgs"; import { deleteOrganizationFromBackend } from "@services/orgs";
import useSWR, { mutate } from "swr"; import useSWR, { mutate } from "swr";
import { swrFetcher } from "@services/utils/requests"; import { swrFetcher } from "@services/utils/requests";
import { getAPIUrl } from "@services/config"; import { getAPIUrl, getUriWithOrg } from "@services/config";
const Organizations = () => { const Organizations = () => {
const { data : organizations , error } = useSWR(`${getAPIUrl()}orgs/user/page/1/limit/10`, swrFetcher) const { data : organizations , error } = useSWR(`${getAPIUrl()}orgs/user/page/1/limit/10`, swrFetcher)
@ -32,7 +32,7 @@ const Organizations = () => {
<div> <div>
{organizations.map((org: any) => ( {organizations.map((org: any) => (
<div key={org.org_id}> <div key={org.org_id}>
<Link href={`/org/${org.slug}`}> <Link href={getUriWithOrg(org.slug,"/")}>
<h3>{org.name}</h3> <h3>{org.name}</h3>
</Link> </Link>
<button onClick={() => deleteOrganization(org.org_id)}>Delete</button> <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 { Draggable } from "react-beautiful-dnd";
import { EyeOpenIcon, Pencil2Icon } from '@radix-ui/react-icons' import { EyeOpenIcon, Pencil2Icon } from '@radix-ui/react-icons'
import styled from "styled-components"; import styled from "styled-components";
import { getUriWithOrg } from "@services/config";
function Lecture(props: any) { function Lecture(props: any) {
@ -12,13 +13,13 @@ function Lecture(props: any) {
<LectureWrapper key={props.lecture.id} {...provided.draggableProps} {...provided.dragHandleProps} ref={provided.innerRef}> <LectureWrapper key={props.lecture.id} {...provided.draggableProps} {...provided.dragHandleProps} ref={provided.innerRef}>
<p>{props.lecture.name} </p> <p>{props.lecture.name} </p>
<Link <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"> rel="noopener noreferrer">
&nbsp; <EyeOpenIcon/> &nbsp; <EyeOpenIcon/>
</Link> </Link>
<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"> rel="noopener noreferrer">
&nbsp; <Pencil2Icon/> &nbsp; <Pencil2Icon/>
</Link> </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) // Get hostname of request (e.g. demo.vercel.pub, demo.localhost:3000)
const hostname = req.headers.get("host") || "learnhouse.app"; 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 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 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 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(`.vercel.pub`, "").replace(`.platformize.vercel.app`, "")
: hostname.replace(`.localhost:3000`, ""); : 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")) { if (url.pathname.startsWith("/organizations")) {
url.pathname = url.pathname.replace("/organizations", `/organizations${currentHost}`); url.pathname = url.pathname.replace("/organizations", `/organizations${currentHost}`);
// remove localhost:3000 from url // remove localhost:3000 from url
url.pathname = url.pathname.replace(`localhost:3000`, ""); url.pathname = url.pathname.replace(`localhost:3000`, "");
console.log(url);
return NextResponse.rewrite(url); return NextResponse.rewrite(url);
} }
else if (url.pathname.startsWith("/org")) { console.log("currentHost", url);
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);
}
// rewrite everything else to `/_sites/[site] dynamic route // rewrite everything else to `/_sites/[site] dynamic route
url.pathname = `/_orgs/${currentHost}${url.pathname}`; 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> { export async function loginAndGetToken(username: string, password: string): Promise<LoginAndGetTokenResponse> {
// Request Config // 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 urlencoded = new URLSearchParams({ username: username, password: password });
const requestOptions: any = { const requestOptions: any = {
@ -28,7 +31,8 @@ export async function loginAndGetToken(username: string, password: string): Prom
} }
export async function getUserInfo(token: string): Promise<any> { 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 = { const requestOptions: any = {
method: "GET", method: "GET",

View file

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

View file

@ -18,6 +18,8 @@ class Settings(BaseModel):
authjwt_token_location = {"cookies"} authjwt_token_location = {"cookies"}
authjwt_cookie_csrf_protect = False authjwt_cookie_csrf_protect = False
authjwt_access_token_expires = False # (pre-alpha only) # TODO: set to 1 hour 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 @AuthJWT.load_config # type: ignore