From a6742d17c17dbaac317836075e9c4ab6e44d427d Mon Sep 17 00:00:00 2001 From: swve Date: Fri, 19 Apr 2024 19:00:49 +0200 Subject: [PATCH 1/5] feat: init easy backend install from cli --- apps/api/cli.py | 112 ++++++++++++++++++ apps/api/poetry.lock | 45 +------ apps/api/pyproject.toml | 1 + apps/api/src/routers/install/install.py | 7 +- apps/api/src/security/auth.py | 2 +- apps/api/src/security/security.py | 4 +- apps/api/src/services/install/install.py | 8 +- apps/api/src/services/users/password_reset.py | 2 +- apps/api/src/services/users/users.py | 8 +- 9 files changed, 134 insertions(+), 55 deletions(-) create mode 100644 apps/api/cli.py diff --git a/apps/api/cli.py b/apps/api/cli.py new file mode 100644 index 00000000..9988b131 --- /dev/null +++ b/apps/api/cli.py @@ -0,0 +1,112 @@ +from typing import Annotated +from pydantic import EmailStr +from sqlalchemy import create_engine +from sqlmodel import SQLModel, Session +import typer + +from config.config import get_learnhouse_config +from src.db.organizations import OrganizationCreate +from src.db.users import UserCreate +from src.services.install.install import ( + install_create_organization, + install_create_organization_user, + install_default_elements, +) + +cli = typer.Typer() + + +@cli.command() +def install( + short: Annotated[bool, typer.Option(help="Install with predefined values")] = False +): + # Get the database session + learnhouse_config = get_learnhouse_config() + engine = create_engine( + learnhouse_config.database_config.sql_connection_string, echo=False, pool_pre_ping=True # type: ignore + ) + SQLModel.metadata.create_all(engine) + + db_session = Session(engine) + + if short: + # Install the default elements + print("Installing default elements...") + install_default_elements(db_session) + print("Default elements installed ✅") + + # Create the Organization + print("Creating default organization...") + org = OrganizationCreate( + name="Default Organization", + description="Default Organization", + slug="default", + email="", + logo_image="", + ) + install_create_organization(org, db_session) + print("Default organization created ✅") + + # Create Organization User + print("Creating default organization user...") + user = UserCreate( + username="admin", email=EmailStr("admin@school.io"), password="adminsecret" + ) + install_create_organization_user(user, "default", db_session) + print("Default organization user created ✅") + + # Show the user how to login + print("Installation completed ✅") + print("") + print("Login with the following credentials:") + print("email: admin@school.io") + print("password: adminsecret") + + else: + # Install the default elements + print("Installing default elements...") + install_default_elements(db_session) + print("Default elements installed ✅") + + # Create the Organization + print("Creating your organization...") + orgname = typer.prompt("What's shall we call your organization?") + slug = typer.prompt( + "What's the slug for your organization? (e.g. school, acme)" + ) + org = OrganizationCreate( + name=orgname, + description="Default Organization", + slug=slug.lower(), + email="", + logo_image="", + ) + install_create_organization(org, db_session) + print(orgname + " Organization created ✅") + + # Create Organization User + print("Creating your organization user...") + username = typer.prompt("What's the username for the user?") + email = typer.prompt("What's the email for the user?") + password = typer.prompt("What's the password for the user?", hide_input=True) + user = UserCreate(username=username, email=EmailStr(email), password=password) + install_create_organization_user(user, slug, db_session) + print(username + " user created ✅") + + # Show the user how to login + print("Installation completed ✅") + print("") + print("Login with the following credentials:") + print("email: " + email) + print("password: The password you entered") + + + + +@cli.command() +def main(): + cli() + + +if __name__ == "__main__": + cli() diff --git a/apps/api/poetry.lock b/apps/api/poetry.lock index d8a658e3..37f5d46c 100644 --- a/apps/api/poetry.lock +++ b/apps/api/poetry.lock @@ -3186,54 +3186,21 @@ telegram = ["requests"] [[package]] name = "typer" -version = "0.12.0" +version = "0.12.3" description = "Typer, build great CLIs. Easy to code. Based on Python type hints." optional = false python-versions = ">=3.7" files = [ - {file = "typer-0.12.0-py3-none-any.whl", hash = "sha256:0441a0bb8962fb4383b8537ada9f7eb2d0deda0caa2cfe7387cc221290f617e4"}, - {file = "typer-0.12.0.tar.gz", hash = "sha256:900fe786ce2d0ea44653d3c8ee4594a22a496a3104370ded770c992c5e3c542d"}, -] - -[package.dependencies] -typer-cli = "0.12.0" -typer-slim = {version = "0.12.0", extras = ["standard"]} - -[[package]] -name = "typer-cli" -version = "0.12.0" -description = "Typer, build great CLIs. Easy to code. Based on Python type hints." -optional = false -python-versions = ">=3.7" -files = [ - {file = "typer_cli-0.12.0-py3-none-any.whl", hash = "sha256:7b7e2dd49f59974bb5a869747045d5444b17bffb851e006cd424f602d3578104"}, - {file = "typer_cli-0.12.0.tar.gz", hash = "sha256:603ed3d5a278827bd497e4dc73a39bb714b230371c8724090b0de2abdcdd9f6e"}, -] - -[package.dependencies] -typer-slim = {version = "0.12.0", extras = ["standard"]} - -[[package]] -name = "typer-slim" -version = "0.12.0" -description = "Typer, build great CLIs. Easy to code. Based on Python type hints." -optional = false -python-versions = ">=3.7" -files = [ - {file = "typer_slim-0.12.0-py3-none-any.whl", hash = "sha256:ddd7042b29a32140528caa415750bcae54113ba0c32270ca11a6f64069ddadf9"}, - {file = "typer_slim-0.12.0.tar.gz", hash = "sha256:3e8a3f17286b173d76dca0fd4e02651c9a2ce1467b3754876b1ac4bd72572beb"}, + {file = "typer-0.12.3-py3-none-any.whl", hash = "sha256:070d7ca53f785acbccba8e7d28b08dcd88f79f1fbda035ade0aecec71ca5c914"}, + {file = "typer-0.12.3.tar.gz", hash = "sha256:49e73131481d804288ef62598d97a1ceef3058905aa536a1134f90891ba35482"}, ] [package.dependencies] click = ">=8.0.0" -rich = {version = ">=10.11.0", optional = true, markers = "extra == \"standard\""} -shellingham = {version = ">=1.3.0", optional = true, markers = "extra == \"standard\""} +rich = ">=10.11.0" +shellingham = ">=1.3.0" typing-extensions = ">=3.7.4.3" -[package.extras] -all = ["rich (>=10.11.0)", "shellingham (>=1.3.0)"] -standard = ["rich (>=10.11.0)", "shellingham (>=1.3.0)"] - [[package]] name = "typing-extensions" version = "4.10.0" @@ -3730,4 +3697,4 @@ testing = ["big-O", "jaraco.functools", "jaraco.itertools", "more-itertools", "p [metadata] lock-version = "2.0" python-versions = "^3.12" -content-hash = "0f7dbd7ca5511470158142c475dab8fad69ffb35fb31da437847bda67f3efbcb" +content-hash = "849c99445d4d0dc5fb06a50359c4e166b357f3705003c2989103a86fd388decb" diff --git a/apps/api/pyproject.toml b/apps/api/pyproject.toml index 6de4ab7f..8d1fd38c 100644 --- a/apps/api/pyproject.toml +++ b/apps/api/pyproject.toml @@ -37,6 +37,7 @@ sentry-sdk = {extras = ["fastapi"], version = "^1.45.0"} sqlmodel = "^0.0.16" tiktoken = "^0.6.0" uvicorn = "0.29.0" +typer = "^0.12.3" [build-system] build-backend = "poetry.core.masonry.api" diff --git a/apps/api/src/routers/install/install.py b/apps/api/src/routers/install/install.py index 40d9202c..11f8b847 100644 --- a/apps/api/src/routers/install/install.py +++ b/apps/api/src/routers/install/install.py @@ -3,7 +3,6 @@ from src.db.install import InstallRead from src.core.events.database import get_db_session from src.db.organizations import OrganizationCreate from src.db.users import UserCreate - from src.services.install.install import ( create_install_instance, get_latest_install_instance, @@ -43,7 +42,7 @@ async def api_get_latest_install_instance( async def api_install_def_elements( db_session=Depends(get_db_session), ): - elements = await install_default_elements(db_session) + elements = install_default_elements(db_session) return elements @@ -53,7 +52,7 @@ async def api_install_org( org: OrganizationCreate, db_session=Depends(get_db_session), ): - organization = await install_create_organization(org, db_session) + organization = install_create_organization(org, db_session) return organization @@ -64,7 +63,7 @@ async def api_install_user( org_slug: str, db_session=Depends(get_db_session), ): - user = await install_create_organization_user(data, org_slug, db_session) + user = install_create_organization_user(data, org_slug, db_session) return user diff --git a/apps/api/src/security/auth.py b/apps/api/src/security/auth.py index dc9c6fd7..e85818df 100644 --- a/apps/api/src/security/auth.py +++ b/apps/api/src/security/auth.py @@ -59,7 +59,7 @@ async def authenticate_user( user = await security_get_user(request, db_session, email) if not user: return False - if not await security_verify_password(password, user.password): + if not security_verify_password(password, user.password): return False return user diff --git a/apps/api/src/security/security.py b/apps/api/src/security/security.py index 0972df02..c3734f38 100644 --- a/apps/api/src/security/security.py +++ b/apps/api/src/security/security.py @@ -17,11 +17,11 @@ ALGORITHM = "HS256" ### 🔒 Passwords Hashing ############################################################## -async def security_hash_password(password: str): +def security_hash_password(password: str): return pbkdf2_sha256.hash(password) -async def security_verify_password(plain_password: str, hashed_password: str): +def security_verify_password(plain_password: str, hashed_password: str): return pbkdf2_sha256.verify(plain_password, hashed_password) diff --git a/apps/api/src/services/install/install.py b/apps/api/src/services/install/install.py index 3b298cda..c54580eb 100644 --- a/apps/api/src/services/install/install.py +++ b/apps/api/src/services/install/install.py @@ -95,7 +95,7 @@ async def update_install_instance( # Install Default roles -async def install_default_elements(db_session: Session): +def install_default_elements(db_session: Session): """ """ # remove all default roles @@ -300,7 +300,7 @@ async def install_default_elements(db_session: Session): # Organization creation -async def install_create_organization( +def install_create_organization( org_object: OrganizationCreate, db_session: Session ): org = Organization.model_validate(org_object) @@ -364,14 +364,14 @@ async def install_create_organization( return org -async def install_create_organization_user( +def install_create_organization_user( user_object: UserCreate, org_slug: str, db_session: Session ): user = User.model_validate(user_object) # Complete the user object user.user_uuid = f"user_{uuid4()}" - user.password = await security_hash_password(user_object.password) + user.password = security_hash_password(user_object.password) user.email_verified = False user.creation_date = str(datetime.now()) user.update_date = str(datetime.now()) diff --git a/apps/api/src/services/users/password_reset.py b/apps/api/src/services/users/password_reset.py index 092d50cc..3d72ed43 100644 --- a/apps/api/src/services/users/password_reset.py +++ b/apps/api/src/services/users/password_reset.py @@ -190,7 +190,7 @@ async def change_password_with_reset_code( ) # Change password - user.password = await security_hash_password(new_password) + user.password = security_hash_password(new_password) db_session.add(user) db_session.commit() diff --git a/apps/api/src/services/users/users.py b/apps/api/src/services/users/users.py index f96c94b6..a0a716f6 100644 --- a/apps/api/src/services/users/users.py +++ b/apps/api/src/services/users/users.py @@ -44,7 +44,7 @@ async def create_user( # Complete the user object user.user_uuid = f"user_{uuid4()}" - user.password = await security_hash_password(user_object.password) + user.password = security_hash_password(user_object.password) user.email_verified = False user.creation_date = str(datetime.now()) user.update_date = str(datetime.now()) @@ -164,7 +164,7 @@ async def create_user_without_org( # Complete the user object user.user_uuid = f"user_{uuid4()}" - user.password = await security_hash_password(user_object.password) + user.password = security_hash_password(user_object.password) user.email_verified = False user.creation_date = str(datetime.now()) user.update_date = str(datetime.now()) @@ -340,13 +340,13 @@ async def update_user_password( # RBAC check await rbac_check(request, current_user, "update", user.user_uuid, db_session) - if not await security_verify_password(form.old_password, user.password): + if not security_verify_password(form.old_password, user.password): raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="Wrong password" ) # Update user - user.password = await security_hash_password(form.new_password) + user.password = security_hash_password(form.new_password) user.update_date = str(datetime.now()) # Update user in database From d5791d99d5c25d5d1feb3b9a3dce1a027aab1309 Mon Sep 17 00:00:00 2001 From: swve Date: Sat, 20 Apr 2024 11:51:01 +0200 Subject: [PATCH 2/5] feat: improve frontend image --- apps/web/Dockerfile | 78 ++- apps/web/app/api/revalidate/route.ts | 19 - apps/web/app/global-error.tsx | 24 - .../Objects/Modals/Feedback/Feedback.tsx | 111 --- apps/web/next.config.js | 21 +- apps/web/package.json | 5 +- apps/web/pnpm-lock.yaml | 642 ++++++++---------- apps/web/sentry.client.config.ts | 34 - apps/web/sentry.server.config.ts | 13 - 9 files changed, 343 insertions(+), 604 deletions(-) delete mode 100644 apps/web/app/api/revalidate/route.ts delete mode 100644 apps/web/app/global-error.tsx delete mode 100644 apps/web/components/Objects/Modals/Feedback/Feedback.tsx delete mode 100644 apps/web/sentry.client.config.ts delete mode 100644 apps/web/sentry.server.config.ts diff --git a/apps/web/Dockerfile b/apps/web/Dockerfile index 9e6311c2..d950b304 100644 --- a/apps/web/Dockerfile +++ b/apps/web/Dockerfile @@ -1,17 +1,71 @@ -# -FROM node:18-alpine +FROM node:18-alpine AS base -# -WORKDIR /usr/learnhouse/front +# Install dependencies only when needed +FROM base AS deps +# Check https://github.com/nodejs/docker-node/tree/b4117f9333da4138b03a546ec926ef50a31506c3#nodealpine to understand why libc6-compat might be needed. +RUN apk add --no-cache libc6-compat +WORKDIR /app -# -COPY package.json /usr/learnhouse/front/package.json +# Install dependencies based on the preferred package manager +COPY package.json yarn.lock* package-lock.json* pnpm-lock.yaml* ./ +RUN \ + if [ -f pnpm-lock.yaml ]; then corepack enable pnpm && pnpm i --frozen-lockfile; \ + else echo "Lockfile not found." && exit 1; \ + fi -# -RUN npm install -# -COPY ./ /usr/learnhouse +# Rebuild the source code only when needed +FROM base AS builder +WORKDIR /app +COPY --from=deps /app/node_modules ./node_modules +COPY . . -# -CMD ["npm", "run", "dev"] \ No newline at end of file +# Next.js collects completely anonymous telemetry data about general usage. +# Learn more here: https://nextjs.org/telemetry +# Uncomment the following line in case you want to disable telemetry during the build. +# ENV NEXT_TELEMETRY_DISABLED 1 + +# Remove .env files from the final image +# This is a good practice to avoid leaking sensitive data +# Learn more about it in the Next.js documentation: https://nextjs.org/docs/basic-features/environment-variables +RUN rm -f .env* + +RUN \ + if [ -f pnpm-lock.yaml ]; then corepack enable pnpm && pnpm run build; \ + else echo "Lockfile not found." && exit 1; \ + fi + +# Production image, copy all the files and run next +FROM base AS runner +WORKDIR /app + +# Install curl +RUN apk add --no-cache curl + +ENV NODE_ENV production +# Uncomment the following line in case you want to disable telemetry during runtime. +# ENV NEXT_TELEMETRY_DISABLED 1 + +RUN addgroup --system --gid 1001 nodejs +RUN adduser --system --uid 1001 nextjs + +COPY --from=builder /app/public ./public + +# Set the correct permission for prerender cache +RUN mkdir .next +RUN chown nextjs:nodejs .next + +# Automatically leverage output traces to reduce image size +# https://nextjs.org/docs/advanced-features/output-file-tracing +COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./ +COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static + +USER nextjs + +EXPOSE 3000 + +ENV PORT 3000 + +# server.js is created by next build from the standalone output +# https://nextjs.org/docs/pages/api-reference/next-config-js/output +CMD HOSTNAME="0.0.0.0" node server.js \ No newline at end of file diff --git a/apps/web/app/api/revalidate/route.ts b/apps/web/app/api/revalidate/route.ts deleted file mode 100644 index e0645121..00000000 --- a/apps/web/app/api/revalidate/route.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { NextRequest, NextResponse } from 'next/server' -import { revalidateTag } from 'next/cache' - -export async function GET(request: NextRequest) { - const tag: any = request.nextUrl.searchParams.get('tag') - revalidateTag(tag) - - return NextResponse.json( - { revalidated: true, now: Date.now(), tag }, - { - status: 200, - headers: { - 'Access-Control-Allow-Origin': '*', - 'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS', - 'Access-Control-Allow-Headers': 'Content-Type, Authorization', - }, - } - ) -} diff --git a/apps/web/app/global-error.tsx b/apps/web/app/global-error.tsx deleted file mode 100644 index 451e8d52..00000000 --- a/apps/web/app/global-error.tsx +++ /dev/null @@ -1,24 +0,0 @@ -'use client' - -import * as Sentry from '@sentry/nextjs' -import NextError from 'next/error' -import { useEffect } from 'react' - -export default function GlobalError({ - error, -}: { - error: Error & { digest?: string } -}) { - useEffect(() => { - Sentry.captureException(error) - }, [error]) - - return ( - - - {/* This is the default Next.js error component but it doesn't allow omitting the statusCode property yet. */} - - - - ) -} diff --git a/apps/web/components/Objects/Modals/Feedback/Feedback.tsx b/apps/web/components/Objects/Modals/Feedback/Feedback.tsx deleted file mode 100644 index 311f0a8d..00000000 --- a/apps/web/components/Objects/Modals/Feedback/Feedback.tsx +++ /dev/null @@ -1,111 +0,0 @@ -import FormLayout, { - ButtonBlack, - Flex, - FormField, - FormLabel, - FormMessage, - Textarea, -} from '@components/StyledElements/Form/Form' -import { BarLoader } from 'react-spinners' -import * as Form from '@radix-ui/react-form' -import React, { useState } from 'react' -import * as Sentry from '@sentry/browser' -import { CheckCircleIcon } from 'lucide-react' -import { useSession } from '@components/Contexts/SessionContext' - -export const FeedbackModal = (user: any) => { - const session = useSession() as any - - const [isSubmitting, setIsSubmitting] = useState(false) - const [view, setView] = useState<'feedbackForm' | 'success'>('feedbackForm') - const [feedbackMessage, setFeedbackMessage] = useState('') - - const handleSubmit = async (e: any) => { - e.preventDefault() - setIsSubmitting(true) - - const user = session.user ? session.user : null - const eventId = Sentry.captureMessage( - `Feedback from ${user ? user.email : 'Anonymous'} - ${feedbackMessage}` - ) - - const userFeedback = { - event_id: eventId, - name: user ? user.full_name : 'Anonymous', - email: user ? user.email : 'Anonymous', - comments: feedbackMessage, - } - Sentry.captureUserFeedback(userFeedback) - setIsSubmitting(false) - setView('success') - } - - const handleFeedbackMessage = (event: React.ChangeEvent) => { - setFeedbackMessage(event.target.value) - } - - if (view == 'feedbackForm') { - return ( - - - - Feedback message - - Please provide learning elements, separated by comma (,) - - - -