Merge pull request #98 from learnhouse/swve/eng-76-fix-various-bugs-leftovers-cleanup-code

Fix various bugs & issues
This commit is contained in:
Badr B 2023-06-28 17:53:06 +02:00 committed by GitHub
commit aea9da388f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
37 changed files with 503 additions and 387 deletions

20
app.py
View file

@ -1,15 +1,13 @@
import logging
from fastapi import FastAPI, Request
from config.config import LearnHouseConfig, get_learnhouse_config
from src.core.events.events import shutdown_app, startup_app
from src.main import global_router
from src.router import v1_router
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import JSONResponse
from fastapi.staticfiles import StaticFiles
from fastapi_jwt_auth.exceptions import AuthJWTException
from fastapi.middleware.gzip import GZipMiddleware
from src.services.mocks.initial import create_initial_data
# from src.services.mocks.initial import create_initial_data
@ -62,7 +60,7 @@ def authjwt_exception_handler(request: Request, exc: AuthJWTException):
# Global Routes
app.include_router(global_router)
app.include_router(v1_router)
# General Routes
@ -71,18 +69,4 @@ app.include_router(global_router)
async def root():
return {"Message": "Welcome to LearnHouse ✨"}
# Get config
@ app.get("/config")
async def config():
logging.info("Getting config")
config = get_learnhouse_config()
return config.dict()
@app.get("/initial_data")
async def initial_data(request: Request):
await create_initial_data(request)
return {"Message": "Initial data created 🤖"}

View file

@ -14,6 +14,14 @@ class CookieConfig(BaseModel):
domain: str
class GeneralConfig(BaseModel):
development_mode: bool
class SecurityConfig(BaseModel):
auth_jwt_secret_key: str
class HostingConfig(BaseModel):
domain: str
ssl: bool
@ -33,8 +41,10 @@ class LearnHouseConfig(BaseModel):
site_name: str
site_description: str
contact_email: str
general_config: GeneralConfig
hosting_config: HostingConfig
database_config: DatabaseConfig
security_config: SecurityConfig
def get_learnhouse_config() -> LearnHouseConfig:
@ -45,6 +55,18 @@ def get_learnhouse_config() -> LearnHouseConfig:
with open(yaml_path, "r") as f:
yaml_config = yaml.safe_load(f)
# General Config
env_development_mode = os.environ.get("LEARNHOUSE_DEVELOPMENT_MODE")
development_mode = env_development_mode or yaml_config.get("general", {}).get(
"development_mode"
)
# Security Config
env_auth_jwt_secret_key = os.environ.get("LEARNHOUSE_AUTH_JWT_SECRET_KEY")
auth_jwt_secret_key = env_auth_jwt_secret_key or yaml_config.get(
"security", {}
).get("auth_jwt_secret_key")
# Check if environment variables are defined
env_site_name = os.environ.get("LEARNHOUSE_SITE_NAME")
env_site_description = os.environ.get("LEARNHOUSE_SITE_DESCRIPTION")
@ -89,10 +111,10 @@ def get_learnhouse_config() -> LearnHouseConfig:
"self_hosted"
)
cookies_domain = env_cookie_domain or yaml_config.get("hosting_config", {}).get("cookies_config", {}).get("domain")
cookie_config = CookieConfig(
domain=cookies_domain
)
cookies_domain = env_cookie_domain or yaml_config.get("hosting_config", {}).get(
"cookies_config", {}
).get("domain")
cookie_config = CookieConfig(domain=cookies_domain)
# Database config
mongodb_connection_string = env_mongodb_connection_string or yaml_config.get(
@ -146,8 +168,10 @@ def get_learnhouse_config() -> LearnHouseConfig:
site_name=site_name,
site_description=site_description,
contact_email=contact_email,
general_config=GeneralConfig(development_mode=bool(development_mode)),
hosting_config=hosting_config,
database_config=database_config,
security_config=SecurityConfig(auth_jwt_secret_key=auth_jwt_secret_key),
)
return config

View file

@ -2,6 +2,12 @@ site_name: LearnHouse
site_description: LearnHouse is an open-source platform tailored for learning experiences.
contact_email: hi@learnhouse.app
general:
development_mode: true
security:
auth_jwt_secret_key: secret
hosting_config:
domain: learnhouse.app
ssl: true

8
front/.eslintrc Normal file
View file

@ -0,0 +1,8 @@
{
"extends": "next",
"rules": {
"react/no-unescaped-entities": "off",
"@next/next/no-page-custom-font": "off",
"@next/next/no-img-element": "off"
}
}

View file

@ -1,3 +0,0 @@
{
"extends": "next/core-web-vitals"
}

View file

@ -14,13 +14,13 @@ const CollectionAdminEditsArea = (props: any) => {
const deleteCollectionUI = async (collectionId: number) => {
await deleteCollection(collectionId);
revalidateTags(["collections"]);
revalidateTags(["collections"], props.orgslug);
// reload the page
router.refresh();
router.push(getUriWithOrg(props.orgslug, "/collections"));
// refresh page (FIX for Next.js BUG)
window.location.reload();
//window.location.reload();
}
return (

View file

@ -43,13 +43,11 @@ function NewCollection(params: any) {
org_id: org.org_id,
};
await createCollection(collection);
revalidateTags(["collections"]);
revalidateTags(["collections"], orgslug);
router.prefetch(getUriWithOrg(orgslug, "/collections"));
router.push(getUriWithOrg(orgslug, "/collections"));
router.refresh();
// refresh page (FIX for Next.js BUG)
window.location.reload();
};

View file

@ -10,6 +10,7 @@ import DocumentPdfActivity from "@components/Pages/Activities/DocumentPdf/Docume
import ActivityIndicators from "@components/Pages/Courses/ActivityIndicators";
import GeneralWrapperStyled from "@components/StyledElements/Wrappers/GeneralWrapper";
import { useRouter } from "next/navigation";
import AuthenticatedClientElement from "@components/Security/AuthenticatedClientElement";
interface ActivityClientProps {
activityid: string;
@ -64,8 +65,10 @@ function ActivityClient(props: ActivityClientProps) {
<h1 className="font-bold text-gray-950 text-2xl first-letter:uppercase" >{activity.name}</h1>
</div>
<div className="flex space-x-2">
<AuthenticatedClientElement checkMethod="authentication">
<MarkStatus activityid={activityid} course={course} orgslug={orgslug} courseid={courseid} />
</AuthenticatedClientElement>
</div>
</div>

View file

@ -22,7 +22,7 @@ const CourseClient = (props: any) => {
async function startCourseUI() {
// Create activity
await startCourse("course_" + courseid, orgslug);
revalidateTags(['courses']);
revalidateTags(['courses'], orgslug);
router.refresh();
// refresh page (FIX for Next.js BUG)
@ -33,7 +33,7 @@ const CourseClient = (props: any) => {
// Close activity
let activity = await removeCourse("course_" + courseid, orgslug);
// Mutate course
revalidateTags(['courses']);
revalidateTags(['courses'], orgslug);
router.refresh();
// refresh page (FIX for Next.js BUG)

View file

@ -34,7 +34,7 @@ function Courses(props: CourseProps) {
async function deleteCourses(course_id: any) {
await deleteCourseFromBackend(course_id);
revalidateTags(['courses']);
revalidateTags(['courses'], orgslug);
router.refresh();
}

View file

@ -1,9 +1,10 @@
import React from "react";
import Courses from "./courses";
import { getOrgCourses } from "@services/courses/courses";
import { getOrgCoursesWithAuthHeader } from "@services/courses/courses";
import { Metadata } from "next";
import { getOrganizationContextInfo } from "@services/organizations/orgs";
import { cookies } from "next/headers";
type MetadataProps = {
params: { orgslug: string };
@ -24,7 +25,9 @@ export async function generateMetadata(
const CoursesPage = async (params: any) => {
const orgslug = params.params.orgslug;
const courses = await getOrgCourses(orgslug, { revalidate: 0, tags: ['courses'] });
const cookieStore = cookies();
const access_token_cookie: any = cookieStore.get('access_token_cookie');
const courses = await getOrgCoursesWithAuthHeader(orgslug, { revalidate: 0, tags: ['courses'] }, access_token_cookie ? access_token_cookie.value : null);
return (
<div>

View file

@ -10,8 +10,8 @@ import Toast from '@components/StyledElements/Toast/Toast';
import { toast } from 'react-hot-toast';
const Login = () => {
const [email, setEmail] = React.useState("admin@admin.admin");
const [password, setPassword] = React.useState("admin");
const [email, setEmail] = React.useState("");
const [password, setPassword] = React.useState("");
const [isSubmitting, setIsSubmitting] = useState(false);
const router = useRouter();

View file

@ -6,9 +6,14 @@ import LearnHouseWhiteLogo from '@public/learnhouse_text_white.png';
import AuthProvider, { AuthContext } from '@components/Security/AuthProvider';
import Avvvatars from 'avvvatars-react';
import Image from 'next/image';
import AuthenticatedClientElement from '@components/Security/AuthenticatedClientElement';
import { getOrganizationContextInfo } from '@services/organizations/orgs';
function SettingsLayout({ children, params }: { children: React.ReactNode, params: any }) {
async function SettingsLayout({ children, params }: { children: React.ReactNode, params: any }) {
const auth: any = React.useContext(AuthContext);
const orgslug = params.orgslug;
let org = await getOrganizationContextInfo(orgslug, {});
return (
<>
@ -28,10 +33,12 @@ function SettingsLayout({ children, params }: { children: React.ReactNode, param
<li><Link href="/settings/account/profile">Profile</Link></li>
<li><Link href="/settings/account/passwords">Passwords</Link></li>
</ul>
<AuthenticatedClientElement checkMethod='roles' orgId={org.org_id} >
<MenuTitle>Organization</MenuTitle>
<ul>
<li><Link href="/settings/organization/general">General</Link></li>
</ul>
</AuthenticatedClientElement>
</LeftMenuWrapper>
</LeftWrapper>
<RightWrapper>

View file

@ -33,12 +33,9 @@ function OrganizationClient(props: any) {
let org_id = org.org_id;
await uploadOrganizationLogo(org_id, selectedFile);
setSelectedFile(null); // Reset the selected file
revalidateTags(['organizations']);
revalidateTags(['organizations'], org.slug);
router.refresh();
// refresh page (FIX for Next.js BUG)
window.location.reload();
}
};
@ -55,6 +52,10 @@ function OrganizationClient(props: any) {
const updateOrg = async (values: OrganizationValues) => {
let org_id = org.org_id;
await updateOrganization(org_id, values);
// Mutate the org
revalidateTags(['organizations'], org.slug);
router.refresh();
}

View file

@ -348,6 +348,42 @@ export const EditorContentWrapper = styled.div`
// disable chrome outline
.ProseMirror {
h1 {
font-size: 30px;
font-weight: 600;
margin-top: 10px;
margin-bottom: 10px;
}
h2 {
font-size: 25px;
font-weight: 600;
margin-top: 10px;
margin-bottom: 10px;
}
h3 {
font-size: 20px;
font-weight: 600;
margin-top: 10px;
margin-bottom: 10px;
}
h4 {
font-size: 18px;
font-weight: 600;
margin-top: 10px;
margin-bottom: 10px;
}
h5 {
font-size: 16px;
font-weight: 600;
margin-top: 10px;
margin-bottom: 10px;
}
padding-left: 20px;
padding-right: 20px;
padding-bottom: 20px;

View file

@ -1,8 +1,24 @@
import React from 'react'
'use client';
import { motion } from "framer-motion";
const variants = {
hidden: { opacity: 0, x: 0, y: 0 },
enter: { opacity: 1, x: 0, y: 0 },
exit: { opacity: 0, x: 0, y: 0 },
};
function PageLoading() {
return (
<div className="max-w-7xl mx-auto px-4 py-20">
<motion.main
variants={variants} // Pass the variant object into Framer Motion
initial="hidden" // Set the initial state to variants.hidden
animate="enter" // Animated state to variants.enter
exit="exit" // Exit state (used later) to variants.exit
transition={{ type: "linear" }} // Set the transition to linear
className=""
>
<div className="max-w-7xl mx-auto px-4 py-20 transition-all">
<div className="animate-pulse mx-auto flex space-x-4">
<svg className="mx-auto" width="295" height="295" viewBox="0 0 295 295" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect opacity="0.51" x="6.5" y="6.5" width="282" height="282" rx="78.5" stroke="#454545" strokeOpacity="0.46" stroke-width="13" stroke-dasharray="11 11" />
@ -10,6 +26,7 @@ function PageLoading() {
</svg>
</div>
</div>
</motion.main>
)
}

View file

@ -43,7 +43,7 @@ function CreateCourseModal({ closeModal, orgslug }: any) {
e.preventDefault();
setIsSubmitting(true);
let status = await createNewCourse(orgId, { name, description }, thumbnail);
revalidateTags(['courses']);
revalidateTags(['courses'], orgslug);
setIsSubmitting(false);
if (status.org_id == orgId) {

View file

@ -18,7 +18,7 @@ function TrailCourseElement(props: TrailCourseElementProps) {
// Close activity
let activity = await removeCourse(course_id, props.orgslug);
// Mutate course
revalidateTags(['courses']);
revalidateTags(['courses'], props.orgslug);
// Mutate
mutate(`${getAPIUrl()}trail/org_slug/${props.orgslug}/trail`);

View file

@ -9,7 +9,7 @@ interface AuthenticatedClientElementProps {
}
function AuthenticatedClientElement(props: AuthenticatedClientElementProps) {
export const AuthenticatedClientElement = (props: AuthenticatedClientElementProps) => {
const auth: any = React.useContext(AuthContext);
// Available roles

View file

@ -3,6 +3,7 @@ function GeneralWrapperStyled({ children }: { children: React.ReactNode }) {
return (
<div
className='max-w-screen-2xl mx-auto px-16 py-5 tracking-tight'
>{children}</div>
)
}

371
front/package-lock.json generated
View file

@ -51,7 +51,7 @@
"@types/styled-components": "^5.1.26",
"@types/uuid": "^9.0.0",
"autoprefixer": "^10.4.14",
"eslint": "8.23.1",
"eslint": "^8.43.0",
"eslint-config-next": "^13.0.6",
"postcss": "^8.4.23",
"tailwindcss": "^3.3.2",
@ -1930,16 +1930,40 @@
"resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.8.0.tgz",
"integrity": "sha512-VINS5vEYAscRl2ZUDiT3uMPlrFQupiKgHz5AA4bCH1miKBg4qtwkim1qPmJj/4WG6TreYMY111rEFsjupcOKHw=="
},
"node_modules/@eslint-community/eslint-utils": {
"version": "4.4.0",
"resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz",
"integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==",
"dev": true,
"dependencies": {
"eslint-visitor-keys": "^3.3.0"
},
"engines": {
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
},
"peerDependencies": {
"eslint": "^6.0.0 || ^7.0.0 || >=8.0.0"
}
},
"node_modules/@eslint-community/regexpp": {
"version": "4.5.1",
"resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.5.1.tgz",
"integrity": "sha512-Z5ba73P98O1KUYCCJTUeVpja9RcGoMdncZ6T49FCUl2lN38JtCJ+3WgIDBv0AuY4WChU5PmtJmOCTlN6FZTFKQ==",
"dev": true,
"engines": {
"node": "^12.0.0 || ^14.0.0 || >=16.0.0"
}
},
"node_modules/@eslint/eslintrc": {
"version": "1.3.2",
"resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-1.3.2.tgz",
"integrity": "sha512-AXYd23w1S/bv3fTs3Lz0vjiYemS08jWkI3hYyS9I1ry+0f+Yjs1wm+sU0BS8qDOPrBIkp4qHYC16I8uVtpLajQ==",
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.0.3.tgz",
"integrity": "sha512-+5gy6OQfk+xx3q0d6jGZZC3f3KzAkXc/IanVxd1is/VIIziRqqt3ongQz0FiTUXqTk0c7aDB3OaFuKnuSoJicQ==",
"dev": true,
"dependencies": {
"ajv": "^6.12.4",
"debug": "^4.3.2",
"espree": "^9.4.0",
"globals": "^13.15.0",
"espree": "^9.5.2",
"globals": "^13.19.0",
"ignore": "^5.2.0",
"import-fresh": "^3.2.1",
"js-yaml": "^4.1.0",
@ -1953,6 +1977,15 @@
"url": "https://opencollective.com/eslint"
}
},
"node_modules/@eslint/js": {
"version": "8.43.0",
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.43.0.tgz",
"integrity": "sha512-s2UHCoiXfxMvmfzqoN+vrQ84ahUSYde9qNO1MdxmoEhyHWsfmwOpFlwYV+ePJEVc7gFnATGUi376WowX1N7tFg==",
"dev": true,
"engines": {
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
}
},
"node_modules/@floating-ui/core": {
"version": "0.7.3",
"resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-0.7.3.tgz",
@ -1980,29 +2013,19 @@
}
},
"node_modules/@humanwhocodes/config-array": {
"version": "0.10.4",
"resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.10.4.tgz",
"integrity": "sha512-mXAIHxZT3Vcpg83opl1wGlVZ9xydbfZO3r5YfRSH6Gpp2J/PfdBP0wbDa2sO6/qRbcalpoevVyW6A/fI6LfeMw==",
"version": "0.11.10",
"resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.10.tgz",
"integrity": "sha512-KVVjQmNUepDVGXNuoRRdmmEjruj0KfiGSbS8LVc12LMsWDQzRXJ0qdhN8L8uUigKpfEHRhlaQFY0ib1tnUbNeQ==",
"dev": true,
"dependencies": {
"@humanwhocodes/object-schema": "^1.2.1",
"debug": "^4.1.1",
"minimatch": "^3.0.4"
"minimatch": "^3.0.5"
},
"engines": {
"node": ">=10.10.0"
}
},
"node_modules/@humanwhocodes/gitignore-to-minimatch": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/@humanwhocodes/gitignore-to-minimatch/-/gitignore-to-minimatch-1.0.2.tgz",
"integrity": "sha512-rSqmMJDdLFUsyxR6FMtD00nfQKKLFb1kv+qBbOVKqErvloEIJLo5bDTJTQNTYgeyp78JsA7u/NPi5jT1GR/MuA==",
"dev": true,
"funding": {
"type": "github",
"url": "https://github.com/sponsors/nzakas"
}
},
"node_modules/@humanwhocodes/module-importer": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz",
@ -3770,9 +3793,9 @@
}
},
"node_modules/acorn": {
"version": "8.8.0",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.0.tgz",
"integrity": "sha512-QOxyigPVrpZ2GXT+PFyZTl6TtOFc5egxHIP9IlQ+RbupQuX4RkT/Bee4/kQuC02Xkzg84JcT7oLYtDIQxp+v7w==",
"version": "8.9.0",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.9.0.tgz",
"integrity": "sha512-jaVNAFBHNLXspO543WnNNPZFRtavh3skAkITqD0/2aeMkKZTN+254PyhwxFYrk3vQ1xfY+2wbesJMs/JC8/PwQ==",
"dev": true,
"bin": {
"acorn": "bin/acorn"
@ -4663,39 +4686,40 @@
}
},
"node_modules/eslint": {
"version": "8.23.1",
"resolved": "https://registry.npmjs.org/eslint/-/eslint-8.23.1.tgz",
"integrity": "sha512-w7C1IXCc6fNqjpuYd0yPlcTKKmHlHHktRkzmBPZ+7cvNBQuiNjx0xaMTjAJGCafJhQkrFJooREv0CtrVzmHwqg==",
"version": "8.43.0",
"resolved": "https://registry.npmjs.org/eslint/-/eslint-8.43.0.tgz",
"integrity": "sha512-aaCpf2JqqKesMFGgmRPessmVKjcGXqdlAYLLC3THM8t5nBRZRQ+st5WM/hoJXkdioEXLLbXgclUpM0TXo5HX5Q==",
"dev": true,
"dependencies": {
"@eslint/eslintrc": "^1.3.2",
"@humanwhocodes/config-array": "^0.10.4",
"@humanwhocodes/gitignore-to-minimatch": "^1.0.2",
"@eslint-community/eslint-utils": "^4.2.0",
"@eslint-community/regexpp": "^4.4.0",
"@eslint/eslintrc": "^2.0.3",
"@eslint/js": "8.43.0",
"@humanwhocodes/config-array": "^0.11.10",
"@humanwhocodes/module-importer": "^1.0.1",
"@nodelib/fs.walk": "^1.2.8",
"ajv": "^6.10.0",
"chalk": "^4.0.0",
"cross-spawn": "^7.0.2",
"debug": "^4.3.2",
"doctrine": "^3.0.0",
"escape-string-regexp": "^4.0.0",
"eslint-scope": "^7.1.1",
"eslint-utils": "^3.0.0",
"eslint-visitor-keys": "^3.3.0",
"espree": "^9.4.0",
"esquery": "^1.4.0",
"eslint-scope": "^7.2.0",
"eslint-visitor-keys": "^3.4.1",
"espree": "^9.5.2",
"esquery": "^1.4.2",
"esutils": "^2.0.2",
"fast-deep-equal": "^3.1.3",
"file-entry-cache": "^6.0.1",
"find-up": "^5.0.0",
"glob-parent": "^6.0.1",
"globals": "^13.15.0",
"globby": "^11.1.0",
"grapheme-splitter": "^1.0.4",
"glob-parent": "^6.0.2",
"globals": "^13.19.0",
"graphemer": "^1.4.0",
"ignore": "^5.2.0",
"import-fresh": "^3.0.0",
"imurmurhash": "^0.1.4",
"is-glob": "^4.0.0",
"js-sdsl": "^4.1.4",
"is-path-inside": "^3.0.3",
"js-yaml": "^4.1.0",
"json-stable-stringify-without-jsonify": "^1.0.1",
"levn": "^0.4.1",
@ -4703,7 +4727,6 @@
"minimatch": "^3.1.2",
"natural-compare": "^1.4.0",
"optionator": "^0.9.1",
"regexpp": "^3.2.0",
"strip-ansi": "^6.0.1",
"strip-json-comments": "^3.1.0",
"text-table": "^0.2.0"
@ -5014,9 +5037,9 @@
}
},
"node_modules/eslint-scope": {
"version": "7.1.1",
"resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.1.1.tgz",
"integrity": "sha512-QKQM/UXpIiHcLqJ5AOyIW7XZmzjkzQXYE54n1++wb0u9V/abW3l9uQnxX8Z5Xd18xyKIMTUAyQ0k1e8pz6LUrw==",
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.0.tgz",
"integrity": "sha512-DYj5deGlHBfMt15J7rdtyKNq/Nqlv5KfU4iodrQ019XESsRnwXH9KAE0y3cwtUHDo2ob7CypAnCqefh6vioWRw==",
"dev": true,
"dependencies": {
"esrecurse": "^4.3.0",
@ -5024,53 +5047,32 @@
},
"engines": {
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
}
},
"node_modules/eslint-utils": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-3.0.0.tgz",
"integrity": "sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA==",
"dev": true,
"dependencies": {
"eslint-visitor-keys": "^2.0.0"
},
"engines": {
"node": "^10.0.0 || ^12.0.0 || >= 14.0.0"
},
"funding": {
"url": "https://github.com/sponsors/mysticatea"
},
"peerDependencies": {
"eslint": ">=5"
}
},
"node_modules/eslint-utils/node_modules/eslint-visitor-keys": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz",
"integrity": "sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==",
"dev": true,
"engines": {
"node": ">=10"
"url": "https://opencollective.com/eslint"
}
},
"node_modules/eslint-visitor-keys": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.3.0.tgz",
"integrity": "sha512-mQ+suqKJVyeuwGYHAdjMFqjCyfl8+Ldnxuyp3ldiMBFKkvytrXUZWaiPCEav8qDHKty44bD+qV1IP4T+w+xXRA==",
"version": "3.4.1",
"resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.1.tgz",
"integrity": "sha512-pZnmmLwYzf+kWaM/Qgrvpen51upAktaaiI01nsJD/Yr3lMOdNtq0cxkrrg16w64VtisN6okbs7Q8AfGqj4c9fA==",
"dev": true,
"engines": {
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
},
"funding": {
"url": "https://opencollective.com/eslint"
}
},
"node_modules/espree": {
"version": "9.4.0",
"resolved": "https://registry.npmjs.org/espree/-/espree-9.4.0.tgz",
"integrity": "sha512-DQmnRpLj7f6TgN/NYb0MTzJXL+vJF9h3pHy4JhCIs3zwcgez8xmGg3sXHcEO97BrmO2OSvCwMdfdlyl+E9KjOw==",
"version": "9.5.2",
"resolved": "https://registry.npmjs.org/espree/-/espree-9.5.2.tgz",
"integrity": "sha512-7OASN1Wma5fum5SrNhFMAMJxOUAbhyfQ8dQ//PJaJbNw0URTPWqIghHWt1MmAANKhHZIYOHruW4Kw4ruUWOdGw==",
"dev": true,
"dependencies": {
"acorn": "^8.8.0",
"acorn-jsx": "^5.3.2",
"eslint-visitor-keys": "^3.3.0"
"eslint-visitor-keys": "^3.4.1"
},
"engines": {
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
@ -5080,9 +5082,9 @@
}
},
"node_modules/esquery": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/esquery/-/esquery-1.4.0.tgz",
"integrity": "sha512-cCDispWt5vHHtwMY2YrAQ4ibFkAL8RbH5YGBnZBc90MolvvfkkQcJro/aZiAQUlQ3qgrYS6D6v8Gc5G5CQsc9w==",
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/esquery/-/esquery-1.5.0.tgz",
"integrity": "sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==",
"dev": true,
"dependencies": {
"estraverse": "^5.1.0"
@ -5475,9 +5477,9 @@
"integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw=="
},
"node_modules/globals": {
"version": "13.17.0",
"resolved": "https://registry.npmjs.org/globals/-/globals-13.17.0.tgz",
"integrity": "sha512-1C+6nQRb1GwGMKm2dH/E7enFAMxGTmGI7/dEdhy/DNelv85w9B72t3uc5frtMNXIbzrarJJ/lTCjcaZwbLJmyw==",
"version": "13.20.0",
"resolved": "https://registry.npmjs.org/globals/-/globals-13.20.0.tgz",
"integrity": "sha512-Qg5QtVkCy/kv3FUSlu4ukeZDVf9ee0iXLAUYX13gbR17bnejFTzr4iS9bY7kwCf1NztRNm1t91fjOiyx4CSwPQ==",
"dev": true,
"dependencies": {
"type-fest": "^0.20.2"
@ -5546,10 +5548,10 @@
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.10.tgz",
"integrity": "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA=="
},
"node_modules/grapheme-splitter": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/grapheme-splitter/-/grapheme-splitter-1.0.4.tgz",
"integrity": "sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ==",
"node_modules/graphemer": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz",
"integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==",
"dev": true
},
"node_modules/has": {
@ -5888,6 +5890,15 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/is-path-inside": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz",
"integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==",
"dev": true,
"engines": {
"node": ">=8"
}
},
"node_modules/is-reference": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/is-reference/-/is-reference-1.2.1.tgz",
@ -6001,12 +6012,6 @@
"jiti": "bin/jiti.js"
}
},
"node_modules/js-sdsl": {
"version": "4.1.4",
"resolved": "https://registry.npmjs.org/js-sdsl/-/js-sdsl-4.1.4.tgz",
"integrity": "sha512-Y2/yD55y5jteOAmY50JbUZYwk3CP3wnLPEZnlR1w9oKhITrBEtAxwuWKebFf8hMrPMgbYwFoWK/lH2sBkErELw==",
"dev": true
},
"node_modules/js-tokens": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
@ -7005,9 +7010,9 @@
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="
},
"node_modules/punycode": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz",
"integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==",
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.0.tgz",
"integrity": "sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==",
"dev": true,
"engines": {
"node": ">=6"
@ -7350,18 +7355,6 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/regexpp": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/regexpp/-/regexpp-3.2.0.tgz",
"integrity": "sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg==",
"dev": true,
"engines": {
"node": ">=8"
},
"funding": {
"url": "https://github.com/sponsors/mysticatea"
}
},
"node_modules/regexpu-core": {
"version": "5.2.2",
"resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-5.2.2.tgz",
@ -9895,16 +9888,31 @@
"resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.8.0.tgz",
"integrity": "sha512-VINS5vEYAscRl2ZUDiT3uMPlrFQupiKgHz5AA4bCH1miKBg4qtwkim1qPmJj/4WG6TreYMY111rEFsjupcOKHw=="
},
"@eslint-community/eslint-utils": {
"version": "4.4.0",
"resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz",
"integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==",
"dev": true,
"requires": {
"eslint-visitor-keys": "^3.3.0"
}
},
"@eslint-community/regexpp": {
"version": "4.5.1",
"resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.5.1.tgz",
"integrity": "sha512-Z5ba73P98O1KUYCCJTUeVpja9RcGoMdncZ6T49FCUl2lN38JtCJ+3WgIDBv0AuY4WChU5PmtJmOCTlN6FZTFKQ==",
"dev": true
},
"@eslint/eslintrc": {
"version": "1.3.2",
"resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-1.3.2.tgz",
"integrity": "sha512-AXYd23w1S/bv3fTs3Lz0vjiYemS08jWkI3hYyS9I1ry+0f+Yjs1wm+sU0BS8qDOPrBIkp4qHYC16I8uVtpLajQ==",
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.0.3.tgz",
"integrity": "sha512-+5gy6OQfk+xx3q0d6jGZZC3f3KzAkXc/IanVxd1is/VIIziRqqt3ongQz0FiTUXqTk0c7aDB3OaFuKnuSoJicQ==",
"dev": true,
"requires": {
"ajv": "^6.12.4",
"debug": "^4.3.2",
"espree": "^9.4.0",
"globals": "^13.15.0",
"espree": "^9.5.2",
"globals": "^13.19.0",
"ignore": "^5.2.0",
"import-fresh": "^3.2.1",
"js-yaml": "^4.1.0",
@ -9912,6 +9920,12 @@
"strip-json-comments": "^3.1.1"
}
},
"@eslint/js": {
"version": "8.43.0",
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.43.0.tgz",
"integrity": "sha512-s2UHCoiXfxMvmfzqoN+vrQ84ahUSYde9qNO1MdxmoEhyHWsfmwOpFlwYV+ePJEVc7gFnATGUi376WowX1N7tFg==",
"dev": true
},
"@floating-ui/core": {
"version": "0.7.3",
"resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-0.7.3.tgz",
@ -9935,22 +9949,16 @@
}
},
"@humanwhocodes/config-array": {
"version": "0.10.4",
"resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.10.4.tgz",
"integrity": "sha512-mXAIHxZT3Vcpg83opl1wGlVZ9xydbfZO3r5YfRSH6Gpp2J/PfdBP0wbDa2sO6/qRbcalpoevVyW6A/fI6LfeMw==",
"version": "0.11.10",
"resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.10.tgz",
"integrity": "sha512-KVVjQmNUepDVGXNuoRRdmmEjruj0KfiGSbS8LVc12LMsWDQzRXJ0qdhN8L8uUigKpfEHRhlaQFY0ib1tnUbNeQ==",
"dev": true,
"requires": {
"@humanwhocodes/object-schema": "^1.2.1",
"debug": "^4.1.1",
"minimatch": "^3.0.4"
"minimatch": "^3.0.5"
}
},
"@humanwhocodes/gitignore-to-minimatch": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/@humanwhocodes/gitignore-to-minimatch/-/gitignore-to-minimatch-1.0.2.tgz",
"integrity": "sha512-rSqmMJDdLFUsyxR6FMtD00nfQKKLFb1kv+qBbOVKqErvloEIJLo5bDTJTQNTYgeyp78JsA7u/NPi5jT1GR/MuA==",
"dev": true
},
"@humanwhocodes/module-importer": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz",
@ -11206,9 +11214,9 @@
}
},
"acorn": {
"version": "8.8.0",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.0.tgz",
"integrity": "sha512-QOxyigPVrpZ2GXT+PFyZTl6TtOFc5egxHIP9IlQ+RbupQuX4RkT/Bee4/kQuC02Xkzg84JcT7oLYtDIQxp+v7w==",
"version": "8.9.0",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.9.0.tgz",
"integrity": "sha512-jaVNAFBHNLXspO543WnNNPZFRtavh3skAkITqD0/2aeMkKZTN+254PyhwxFYrk3vQ1xfY+2wbesJMs/JC8/PwQ==",
"dev": true
},
"acorn-jsx": {
@ -11820,39 +11828,40 @@
"dev": true
},
"eslint": {
"version": "8.23.1",
"resolved": "https://registry.npmjs.org/eslint/-/eslint-8.23.1.tgz",
"integrity": "sha512-w7C1IXCc6fNqjpuYd0yPlcTKKmHlHHktRkzmBPZ+7cvNBQuiNjx0xaMTjAJGCafJhQkrFJooREv0CtrVzmHwqg==",
"version": "8.43.0",
"resolved": "https://registry.npmjs.org/eslint/-/eslint-8.43.0.tgz",
"integrity": "sha512-aaCpf2JqqKesMFGgmRPessmVKjcGXqdlAYLLC3THM8t5nBRZRQ+st5WM/hoJXkdioEXLLbXgclUpM0TXo5HX5Q==",
"dev": true,
"requires": {
"@eslint/eslintrc": "^1.3.2",
"@humanwhocodes/config-array": "^0.10.4",
"@humanwhocodes/gitignore-to-minimatch": "^1.0.2",
"@eslint-community/eslint-utils": "^4.2.0",
"@eslint-community/regexpp": "^4.4.0",
"@eslint/eslintrc": "^2.0.3",
"@eslint/js": "8.43.0",
"@humanwhocodes/config-array": "^0.11.10",
"@humanwhocodes/module-importer": "^1.0.1",
"@nodelib/fs.walk": "^1.2.8",
"ajv": "^6.10.0",
"chalk": "^4.0.0",
"cross-spawn": "^7.0.2",
"debug": "^4.3.2",
"doctrine": "^3.0.0",
"escape-string-regexp": "^4.0.0",
"eslint-scope": "^7.1.1",
"eslint-utils": "^3.0.0",
"eslint-visitor-keys": "^3.3.0",
"espree": "^9.4.0",
"esquery": "^1.4.0",
"eslint-scope": "^7.2.0",
"eslint-visitor-keys": "^3.4.1",
"espree": "^9.5.2",
"esquery": "^1.4.2",
"esutils": "^2.0.2",
"fast-deep-equal": "^3.1.3",
"file-entry-cache": "^6.0.1",
"find-up": "^5.0.0",
"glob-parent": "^6.0.1",
"globals": "^13.15.0",
"globby": "^11.1.0",
"grapheme-splitter": "^1.0.4",
"glob-parent": "^6.0.2",
"globals": "^13.19.0",
"graphemer": "^1.4.0",
"ignore": "^5.2.0",
"import-fresh": "^3.0.0",
"imurmurhash": "^0.1.4",
"is-glob": "^4.0.0",
"js-sdsl": "^4.1.4",
"is-path-inside": "^3.0.3",
"js-yaml": "^4.1.0",
"json-stable-stringify-without-jsonify": "^1.0.1",
"levn": "^0.4.1",
@ -11860,7 +11869,6 @@
"minimatch": "^3.1.2",
"natural-compare": "^1.4.0",
"optionator": "^0.9.1",
"regexpp": "^3.2.0",
"strip-ansi": "^6.0.1",
"strip-json-comments": "^3.1.0",
"text-table": "^0.2.0"
@ -12094,53 +12102,36 @@
"requires": {}
},
"eslint-scope": {
"version": "7.1.1",
"resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.1.1.tgz",
"integrity": "sha512-QKQM/UXpIiHcLqJ5AOyIW7XZmzjkzQXYE54n1++wb0u9V/abW3l9uQnxX8Z5Xd18xyKIMTUAyQ0k1e8pz6LUrw==",
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.0.tgz",
"integrity": "sha512-DYj5deGlHBfMt15J7rdtyKNq/Nqlv5KfU4iodrQ019XESsRnwXH9KAE0y3cwtUHDo2ob7CypAnCqefh6vioWRw==",
"dev": true,
"requires": {
"esrecurse": "^4.3.0",
"estraverse": "^5.2.0"
}
},
"eslint-utils": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-3.0.0.tgz",
"integrity": "sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA==",
"dev": true,
"requires": {
"eslint-visitor-keys": "^2.0.0"
},
"dependencies": {
"eslint-visitor-keys": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz",
"integrity": "sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==",
"dev": true
}
}
},
"eslint-visitor-keys": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.3.0.tgz",
"integrity": "sha512-mQ+suqKJVyeuwGYHAdjMFqjCyfl8+Ldnxuyp3ldiMBFKkvytrXUZWaiPCEav8qDHKty44bD+qV1IP4T+w+xXRA==",
"version": "3.4.1",
"resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.1.tgz",
"integrity": "sha512-pZnmmLwYzf+kWaM/Qgrvpen51upAktaaiI01nsJD/Yr3lMOdNtq0cxkrrg16w64VtisN6okbs7Q8AfGqj4c9fA==",
"dev": true
},
"espree": {
"version": "9.4.0",
"resolved": "https://registry.npmjs.org/espree/-/espree-9.4.0.tgz",
"integrity": "sha512-DQmnRpLj7f6TgN/NYb0MTzJXL+vJF9h3pHy4JhCIs3zwcgez8xmGg3sXHcEO97BrmO2OSvCwMdfdlyl+E9KjOw==",
"version": "9.5.2",
"resolved": "https://registry.npmjs.org/espree/-/espree-9.5.2.tgz",
"integrity": "sha512-7OASN1Wma5fum5SrNhFMAMJxOUAbhyfQ8dQ//PJaJbNw0URTPWqIghHWt1MmAANKhHZIYOHruW4Kw4ruUWOdGw==",
"dev": true,
"requires": {
"acorn": "^8.8.0",
"acorn-jsx": "^5.3.2",
"eslint-visitor-keys": "^3.3.0"
"eslint-visitor-keys": "^3.4.1"
}
},
"esquery": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/esquery/-/esquery-1.4.0.tgz",
"integrity": "sha512-cCDispWt5vHHtwMY2YrAQ4ibFkAL8RbH5YGBnZBc90MolvvfkkQcJro/aZiAQUlQ3qgrYS6D6v8Gc5G5CQsc9w==",
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/esquery/-/esquery-1.5.0.tgz",
"integrity": "sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==",
"dev": true,
"requires": {
"estraverse": "^5.1.0"
@ -12441,9 +12432,9 @@
"integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw=="
},
"globals": {
"version": "13.17.0",
"resolved": "https://registry.npmjs.org/globals/-/globals-13.17.0.tgz",
"integrity": "sha512-1C+6nQRb1GwGMKm2dH/E7enFAMxGTmGI7/dEdhy/DNelv85w9B72t3uc5frtMNXIbzrarJJ/lTCjcaZwbLJmyw==",
"version": "13.20.0",
"resolved": "https://registry.npmjs.org/globals/-/globals-13.20.0.tgz",
"integrity": "sha512-Qg5QtVkCy/kv3FUSlu4ukeZDVf9ee0iXLAUYX13gbR17bnejFTzr4iS9bY7kwCf1NztRNm1t91fjOiyx4CSwPQ==",
"dev": true,
"requires": {
"type-fest": "^0.20.2"
@ -12495,10 +12486,10 @@
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.10.tgz",
"integrity": "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA=="
},
"grapheme-splitter": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/grapheme-splitter/-/grapheme-splitter-1.0.4.tgz",
"integrity": "sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ==",
"graphemer": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz",
"integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==",
"dev": true
},
"has": {
@ -12724,6 +12715,12 @@
"has-tostringtag": "^1.0.0"
}
},
"is-path-inside": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz",
"integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==",
"dev": true
},
"is-reference": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/is-reference/-/is-reference-1.2.1.tgz",
@ -12803,12 +12800,6 @@
"integrity": "sha512-QAdOptna2NYiSSpv0O/BwoHBSmz4YhpzJHyi+fnMRTXFjp7B8i/YG5Z8IfusxB1ufjcD2Sre1F3R+nX3fvy7gg==",
"dev": true
},
"js-sdsl": {
"version": "4.1.4",
"resolved": "https://registry.npmjs.org/js-sdsl/-/js-sdsl-4.1.4.tgz",
"integrity": "sha512-Y2/yD55y5jteOAmY50JbUZYwk3CP3wnLPEZnlR1w9oKhITrBEtAxwuWKebFf8hMrPMgbYwFoWK/lH2sBkErELw==",
"dev": true
},
"js-tokens": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
@ -13533,9 +13524,9 @@
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="
},
"punycode": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz",
"integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==",
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.0.tgz",
"integrity": "sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==",
"dev": true
},
"queue-microtask": {
@ -13767,12 +13758,6 @@
"functions-have-names": "^1.2.2"
}
},
"regexpp": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/regexpp/-/regexpp-3.2.0.tgz",
"integrity": "sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg==",
"dev": true
},
"regexpu-core": {
"version": "5.2.2",
"resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-5.2.2.tgz",

View file

@ -52,7 +52,7 @@
"@types/styled-components": "^5.1.26",
"@types/uuid": "^9.0.0",
"autoprefixer": "^10.4.14",
"eslint": "8.23.1",
"eslint": "^8.43.0",
"eslint-config-next": "^13.0.6",
"postcss": "^8.4.23",
"tailwindcss": "^3.3.2",

View file

@ -9,7 +9,6 @@ import { RequestBody, RequestBodyForm, RequestBodyWithAuthHeader, errorHandling
export async function getOrgCourses(org_id: number, next: any) {
const result: any = await fetch(`${getAPIUrl()}courses/org_slug/${org_id}/page/1/limit/10`, RequestBody("GET", null, next));
const res = await errorHandling(result);
return res;
}

View file

@ -1,6 +1,6 @@
import { AppRouterInstance } from "next/dist/shared/lib/app-router-context";
import { denyAccessToUser } from "../react/middlewares/views";
import { LEARNHOUSE_DOMAIN, LEARNHOUSE_HTTP_PROTOCOL } from "@services/config/config";
import { getUriWithOrg, LEARNHOUSE_DOMAIN, LEARNHOUSE_HTTP_PROTOCOL } from "@services/config/config";
export const RequestBody = (method: string, data: any, next: any) => {
let HeadersConfig = new Headers({ "Content-Type": "application/json" });
@ -78,15 +78,16 @@ export const swrFetcher = async (url: string, body: any, router?: AppRouterInsta
export const errorHandling = (res: any) => {
if (!res.ok) {
const error: any = new Error(`${res.status}: ${res.statusText}`, {});
const error: any = new Error(`${res.statusText}`);
error.status = res.status;
throw error;
}
return res.json();
};
export const revalidateTags = (tags: string[]) => {
export const revalidateTags = (tags: string[], orgslug: string) => {
const url = getUriWithOrg(orgslug, "");
tags.forEach((tag) => {
fetch(`${LEARNHOUSE_HTTP_PROTOCOL}${LEARNHOUSE_DOMAIN}/api/revalidate?tag=${tag}`);
fetch(`${url}/api/revalidate?tag=${tag}`);
});
};

View file

@ -1,21 +0,0 @@
from fastapi import APIRouter
from src.routers import blocks, trail, users, auth, orgs, roles
from src.routers.courses import chapters, collections, courses,activities
global_router = APIRouter(prefix="/api")
# API Routes
global_router.include_router(users.router, prefix="/users", tags=["users"])
global_router.include_router(auth.router, prefix="/auth", tags=["auth"])
global_router.include_router(orgs.router, prefix="/orgs", tags=["orgs"])
global_router.include_router(roles.router, prefix="/roles", tags=["roles"])
global_router.include_router(blocks.router, prefix="/blocks", tags=["blocks"])
global_router.include_router(courses.router, prefix="/courses", tags=["courses"])
global_router.include_router(chapters.router, prefix="/chapters", tags=["chapters"])
global_router.include_router(activities.router, prefix="/activities", tags=["activities"])
global_router.include_router(collections.router, prefix="/collections", tags=["collections"])
global_router.include_router(trail.router, prefix="/trail", tags=["trail"])

25
src/router.py Normal file
View file

@ -0,0 +1,25 @@
from fastapi import APIRouter, Depends
from src.routers import blocks, dev, trail, users, auth, orgs, roles
from src.routers.courses import chapters, collections, courses, activities
from src.services.dev.dev import isDevModeEnabled
v1_router = APIRouter(prefix="/api/v1")
# API Routes
v1_router.include_router(users.router, prefix="/users", tags=["users"])
v1_router.include_router(auth.router, prefix="/auth", tags=["auth"])
v1_router.include_router(orgs.router, prefix="/orgs", tags=["orgs"])
v1_router.include_router(roles.router, prefix="/roles", tags=["roles"])
v1_router.include_router(blocks.router, prefix="/blocks", tags=["blocks"])
v1_router.include_router(courses.router, prefix="/courses", tags=["courses"])
v1_router.include_router(chapters.router, prefix="/chapters", tags=["chapters"])
v1_router.include_router(activities.router, prefix="/activities", tags=["activities"])
v1_router.include_router( collections.router, prefix="/collections", tags=["collections"])
v1_router.include_router(trail.router, prefix="/trail", tags=["trail"])
# Dev Routes
v1_router.include_router(
dev.router, prefix="/dev", tags=["dev"], dependencies=[Depends(isDevModeEnabled)]
)

View file

@ -1,5 +1,6 @@
from fastapi import Depends, APIRouter, HTTPException, Response, status, Request
from fastapi.security import OAuth2PasswordRequestForm
from config.config import get_learnhouse_config
from src.security.auth import AuthJWT, authenticate_user
from src.services.users.users import PublicUser
@ -41,7 +42,7 @@ async def login(
refresh_token = Authorize.create_refresh_token(subject=form_data.username)
Authorize.set_refresh_cookies(refresh_token)
# set cookies using fastapi
response.set_cookie(key="access_token_cookie", value=access_token, httponly=False)
response.set_cookie(key="access_token_cookie", value=access_token, httponly=False, domain=get_learnhouse_config().hosting_config.cookie_config.domain)
user = PublicUser(**user.dict())
result = {

View file

@ -1,7 +1,7 @@
from fastapi import APIRouter, Depends, UploadFile, Form, Request
from src.security.auth import get_current_user
from src.services.courses.courses import Course, create_course, get_course, get_course_meta, get_courses, get_courses_orgslug, update_course, delete_course, update_course_thumbnail
from src.services.courses.courses import Course, create_course, get_course, get_course_meta, get_courses_orgslug, update_course, delete_course, update_course_thumbnail
from src.services.users.users import PublicUser
@ -41,21 +41,12 @@ async def api_get_course_meta(request: Request, course_id: str, current_user: P
"""
return await get_course_meta(request, course_id, current_user=current_user)
@router.get("/org_id/{org_id}/page/{page}/limit/{limit}")
async def api_get_course_by(request: Request, page: int, limit: int, org_id: str):
"""
Get houses by page and limit
"""
return await get_courses(request, page, limit, org_id)
@router.get("/org_slug/{org_slug}/page/{page}/limit/{limit}")
async def api_get_course_by_orgslug(request: Request, page: int, limit: int, org_slug: str):
async def api_get_course_by_orgslug(request: Request, page: int, limit: int, org_slug: str, current_user: PublicUser = Depends(get_current_user)):
"""
Get houses by page and limit
"""
return await get_courses_orgslug(request, page, limit, org_slug)
return await get_courses_orgslug(request, current_user, page, limit, org_slug)
@router.put("/{course_id}")

18
src/routers/dev.py Normal file
View file

@ -0,0 +1,18 @@
from fastapi import APIRouter, Request
from config.config import get_learnhouse_config
from src.services.dev.mocks.initial import create_initial_data
router = APIRouter()
@router.get("/config")
async def config():
config = get_learnhouse_config()
return config.dict()
@router.get("/mock/initial")
async def initial_data(request: Request):
await create_initial_data(request)
return {"Message": "Initial data created 🤖"}

View file

@ -4,6 +4,7 @@ from fastapi import Depends, HTTPException, Request, status
from fastapi.security import OAuth2PasswordBearer
from jose import JWTError, jwt
from datetime import datetime, timedelta
from src.services.dev.dev import isDevModeEnabled
from src.services.users.schemas.users import AnonymousUser, PublicUser
from src.services.users.users import security_get_user, security_verify_password
from src.security.security import ALGORITHM, SECRET_KEY
@ -14,10 +15,10 @@ oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/api/auth/login")
#### JWT Auth ####################################################
class Settings(BaseModel):
authjwt_secret_key: str = "secret"
authjwt_secret_key: str = "secret" if isDevModeEnabled() else SECRET_KEY
authjwt_token_location = {"cookies", "headers"}
authjwt_cookie_csrf_protect = False
authjwt_access_token_expires = False # (pre-alpha only) # TODO: set to 1 hour
authjwt_access_token_expires = False if isDevModeEnabled() else 3600
authjwt_cookie_samesite = "lax"
authjwt_cookie_secure = True
authjwt_cookie_domain = get_learnhouse_config().hosting_config.cookie_config.domain

View file

@ -1,6 +1,7 @@
from fastapi import HTTPException, status, Request
from passlib.context import CryptContext
from passlib.hash import pbkdf2_sha256
from config.config import get_learnhouse_config
from src.services.roles.schemas.roles import RoleInDB
from src.services.users.schemas.users import UserInDB, UserRolesInOrganization
@ -10,7 +11,7 @@ from src.services.users.schemas.users import UserInDB, UserRolesInOrganization
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
ACCESS_TOKEN_EXPIRE_MINUTES = 30
SECRET_KEY = "09d25e094faa6ca2556c818166b7a9563b93f7099f6f0f4caa6cf63b88e8d3e7"
SECRET_KEY = get_learnhouse_config().security_config.auth_jwt_secret_key
ALGORITHM = "HS256"
### 🔒 JWT ##############################################################
@ -18,6 +19,7 @@ ALGORITHM = "HS256"
### 🔒 Passwords Hashing ##############################################################
async def security_hash_password(password: str):
return pbkdf2_sha256.hash(password)
@ -25,63 +27,64 @@ async def security_hash_password(password: str):
async def security_verify_password(plain_password: str, hashed_password: str):
return pbkdf2_sha256.verify(plain_password, hashed_password)
### 🔒 Passwords Hashing ##############################################################
### 🔒 Roles checking ##############################################################
async def verify_user_rights_with_roles(request: Request, action: str, user_id: str, element_id: str, element_org_id: str):
async def verify_user_rights_with_roles(
request: Request, action: str, user_id: str, element_id: str, element_org_id: str
):
"""
Check if the user has the right to perform the action on the element
"""
roles = request.app.db["roles"]
request.app.db["roles"]
users = request.app.db["users"]
user = await users.find_one({"user_id": user_id})
# Check if user is available
#########
# Users existence verification
#########
if not user and user_id != "anonymous":
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, detail="User not found")
status_code=status.HTTP_404_NOT_FOUND, detail="User rights : User not found"
)
# Check if user is anonymous
if user_id == "anonymous":
return False
# Check if the user is an admin
# Get User
user: UserInDB = UserInDB(**await users.find_one({"user_id": user_id}))
# Organization roles verification
#########
# Organization Roles verification
#########
for org in user.orgs:
# TODO: Check if the org_id (user) is the same as the org_id (element)
if org.org_id == element_org_id:
return True
# Check if user is owner or reader of the organization
if org.org_role == ("owner" or "editor"):
return True
# If the user is not an owner or a editor, check if he has a role
# Get user roles
#########
# Roles verification
#########
user_roles = user.roles
# TODO: Check if the org_id of the role is the same as the org_id of the element using find
if action != "create":
await check_user_role_org_with_element_org(request, element_id, user_roles)
# Check if user has the right role
element_type = await check_element_type(element_id)
for role_id in user_roles:
role = RoleInDB(**await roles.find_one({"role_id": role_id}))
if role.elements[element_type][f"action_{action}"]:
return True
return await check_user_role_org_with_element_org(
request, element_id, user_roles, action
)
# If no role is found, raise an error
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN, detail="You don't have the right to perform this action")
status_code=status.HTTP_403_FORBIDDEN,
detail="User rights : You don't have the right to perform this action",
)
async def check_element_type(element_id):
@ -104,11 +107,17 @@ async def check_element_type(element_id):
return "activities"
else:
raise HTTPException(
status_code=status.HTTP_409_CONFLICT, detail="Issue verifying element nature")
status_code=status.HTTP_409_CONFLICT,
detail="User rights : Issue verifying element nature",
)
async def check_user_role_org_with_element_org(request: Request, element_id: str, roles_list: list[UserRolesInOrganization]):
async def check_user_role_org_with_element_org(
request: Request,
element_id: str,
roles_list: list[UserRolesInOrganization],
action: str,
):
element_type = await check_element_type(element_id)
element = request.app.db[element_type]
roles = request.app.db["roles"]
@ -120,16 +129,23 @@ async def check_user_role_org_with_element_org(request: Request, element_id: str
element_org = await element.find_one({element_type_id: element_id})
for role_id in roles_list:
role = RoleInDB(**await roles.find_one({"role_id": role_id}))
if role.org_id == element_org["org_id"]:
return True
if role.org_id == "*":
for role in roles_list:
# Check if The role belongs to the same organization as the element
role_db = await roles.find_one({"role_id": role.role_id})
role = RoleInDB(**role_db)
if role.org_id == element_org["org_id"] or role.org_id == "*":
# Check if user has the right role
for role in roles_list:
role_db = await roles.find_one({"role_id": role.role_id})
role = RoleInDB(**role_db)
if role.elements[element_type][f"action_{action}"]:
return True
else:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN, detail="You don't have the right to perform this action")
status_code=status.HTTP_403_FORBIDDEN,
detail="User rights (roles) : You don't have the right to perform this action",
)
### 🔒 Roles checking ##############################################################

View file

@ -84,6 +84,11 @@ async def get_activity(request: Request, activity_id: str, current_user: PublicU
course = await courses.find_one({"chapters": coursechapter_id})
isCoursePublic = course["public"]
isAuthor = current_user.user_id in course["authors"]
if isAuthor:
activity = ActivityInDB(**activity)
return activity
# verify course rights
hasRoleRights = await verify_user_rights_with_roles(

View file

@ -313,31 +313,15 @@ async def delete_course(request: Request, course_id: str, current_user: PublicUs
####################################################
async def get_courses(
request: Request, page: int = 1, limit: int = 10, org_id: str | None = None
):
courses = request.app.db["courses"]
# TODO : Get only courses that user is admin/has roles of
# get all courses from database
all_courses = (
courses.find({"org_id": org_id})
.sort("name", 1)
.skip(10 * (page - 1))
.limit(limit)
)
return [
json.loads(json.dumps(course, default=str))
for course in await all_courses.to_list(length=100)
]
async def get_courses_orgslug(
request: Request, page: int = 1, limit: int = 10, org_slug: str | None = None
request: Request,
current_user: PublicUser,
page: int = 1,
limit: int = 10,
org_slug: str | None = None,
):
courses = request.app.db["courses"]
orgs = request.app.db["organizations"]
# TODO : Get only courses that user is admin/has roles of
# get org_id from slug
org = await orgs.find_one({"slug": org_slug})
@ -347,7 +331,15 @@ async def get_courses_orgslug(
status_code=status.HTTP_409_CONFLICT, detail="Organization does not exist"
)
# get all courses from database
# show only public courses if user is not logged in
if current_user.user_id == "anonymous":
all_courses = (
courses.find({"org_id": org["org_id"], "public": True})
.sort("name", 1)
.skip(10 * (page - 1))
.limit(limit)
)
else:
all_courses = (
courses.find({"org_id": org["org_id"]})
.sort("name", 1)
@ -374,6 +366,11 @@ async def verify_rights(
course = await courses.find_one({"course_id": course_id})
isAuthor = current_user.user_id in course["authors"]
if isAuthor:
return True
if (
current_user.user_id == "anonymous"
and course["public"] is True
@ -390,7 +387,6 @@ async def verify_rights(
hasRoleRights = await verify_user_rights_with_roles(
request, action, current_user.user_id, course_id, course["org_id"]
)
isAuthor = current_user.user_id in course["authors"]
if not hasRoleRights and not isAuthor:
raise HTTPException(

View file

14
src/services/dev/dev.py Normal file
View file

@ -0,0 +1,14 @@
from fastapi import HTTPException
from config.config import get_learnhouse_config
def isDevModeEnabled():
config = get_learnhouse_config()
if config.general_config.development_mode:
return True
else:
raise HTTPException(
status_code=403,
detail="Development mode is not enabled",
)

View file

View file

@ -124,7 +124,7 @@ async def create_initial_data(request: Request):
await database_orgs.delete_many({})
organizations = []
for i in range(0, 5):
for i in range(0, 2):
company = fake.company()
# remove whitespace and special characters and make lowercase
slug = ''.join(e for e in company if e.isalnum()).lower()