diff --git a/.dockerignore b/.dockerignore
index 4efc3dc2..5865c997 100644
--- a/.dockerignore
+++ b/.dockerignore
@@ -1,18 +1,165 @@
-Dockerfile
+# Docker files
+Dockerfile*
.dockerignore
+
+# Git
.git
.gitignore
.gitattributes
+.gitmodules
+
+# Documentation
README.md
-.npmrc
+CHANGELOG.md
+CONTRIBUTING.md
+CODE_OF_CONDUCT.md
+LICENSE
+*.md
+
+# IDE and Editor files
+.vscode
+.idea
+*.swp
+*.swo
+*~
+
+# OS generated files
+.DS_Store
+.DS_Store?
+._*
+.Spotlight-V100
+.Trashes
+ehthumbs.db
+Thumbs.db
+
+# Node.js
+node_modules
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+.npm
+.yarn-integrity
+.pnpm-debug.log*
+
+# Build outputs
+**/dist
+**/build
+**/.next
+**/.svelte-kit
+**/coverage
+**/.nyc_output
+**/test-results
+**/playwright-report
+**/playwright/.cache
+
+# Dependencies
+**/node_modules
+**/.pnpm-store
+.pnpm-store/*
+
+# Environment files
+**/.env
+**/.env.local
+**/.env.development
+**/.env.test
+**/.env.production
+**/.env.staging
+
+# Logs
+logs
+*.log
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+lerna-debug.log*
+
+# Runtime data
+pids
+*.pid
+*.seed
+*.pid.lock
+
+# Coverage directory used by tools like istanbul
+coverage
+*.lcov
+
+# nyc test coverage
+.nyc_output
+
+# Dependency directories
+jspm_packages/
+
+# Optional npm cache directory
+.npm
+
+# Optional eslint cache
+.eslintcache
+
+# Microbundle cache
+.rpt2_cache/
+.rts2_cache_cjs/
+.rts2_cache_es/
+.rts2_cache_umd/
+
+# Optional REPL history
+.node_repl_history
+
+# Output of 'npm pack'
+*.tgz
+
+# Yarn Integrity file
+.yarn-integrity
+
+# parcel-bundler cache (https://parceljs.org/)
+.cache
+.parcel-cache
+
+# Next.js build output
+.next
+
+# Nuxt.js build / generate output
+.nuxt
+
+# Gatsby files
+.cache/
+public
+
+# Storybook build outputs
+.out
+.storybook-out
+
+# Temporary folders
+tmp/
+temp/
+
+# Editor directories and files
+.vscode/*
+!.vscode/extensions.json
+.idea
+*.suo
+*.ntvs*
+*.njsproj
+*.sln
+*.sw?
+
+# Turbo
+**/.turbo
+
+# Testing
+**/cypress
+**/cypress.config.js
+**/cypress.config.ts
+**/test-results
+**/playwright-report
+**/playwright/.cache
+
+# Package managers (keep pnpm-lock.yaml for builds)
+package-lock.json
+yarn.lock
+
+# Config files that aren't needed in container
.prettierrc
.eslintrc.cjs
.graphqlrc
.editorconfig
-.vscode
-node_modules
-build
-package
-**/.env
-
-.pnpm-store/*
\ No newline at end of file
+.npmrc
\ No newline at end of file
diff --git a/.github/workflows/docker-publish.yml b/.github/workflows/docker-publish.yml
new file mode 100644
index 00000000..65753dbb
--- /dev/null
+++ b/.github/workflows/docker-publish.yml
@@ -0,0 +1,71 @@
+name: Publish Docker Images
+
+on:
+ push:
+ branches:
+ - main
+ tags:
+ - "v*"
+ workflow_dispatch:
+ inputs:
+ version:
+ description: "Version tag (e.g., v1.0.0)"
+ required: false
+ default: "latest"
+
+env:
+ DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME || 'classroomio' }}
+
+jobs:
+ build-and-push:
+ runs-on: ubuntu-latest
+ strategy:
+ matrix:
+ include:
+ - name: api
+ dockerfile: ./docker/Dockerfile.api
+ image: api
+ - name: dashboard
+ dockerfile: ./docker/Dockerfile.dashboard
+ image: dashboard
+
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v4
+
+ - name: Set up Docker Buildx
+ uses: docker/setup-buildx-action@v3
+
+ - name: Log in to Docker Hub
+ uses: docker/login-action@v3
+ with:
+ username: ${{ secrets.DOCKERHUB_USERNAME }}
+ password: ${{ secrets.DOCKERHUB_TOKEN }}
+
+ - name: Extract metadata (tags, labels)
+ id: meta
+ uses: docker/metadata-action@v5
+ with:
+ images: ${{ env.DOCKERHUB_USERNAME }}/${{ matrix.image }}
+ tags: |
+ type=ref,event=branch
+ type=ref,event=pr
+ type=semver,pattern={{version}}
+ type=semver,pattern={{major}}.{{minor}}
+ type=semver,pattern={{major}}
+ type=raw,value=latest,enable={{is_default_branch}}
+
+ - name: Build and push ${{ matrix.name }}
+ uses: docker/build-push-action@v5
+ with:
+ context: .
+ file: ${{ matrix.dockerfile }}
+ push: true
+ tags: ${{ steps.meta.outputs.tags }}
+ labels: ${{ steps.meta.outputs.labels }}
+ cache-from: type=registry,ref=${{ env.DOCKERHUB_USERNAME }}/${{ matrix.image }}:buildcache
+ cache-to: type=registry,ref=${{ env.DOCKERHUB_USERNAME }}/${{ matrix.image }}:buildcache,mode=max
+ platforms: linux/amd64
+
+ - name: Image digest
+ run: echo ${{ steps.docker_build.outputs.digest }}
diff --git a/apps/api/Dockerfile b/apps/api/Dockerfile
deleted file mode 100644
index fb59e5c3..00000000
--- a/apps/api/Dockerfile
+++ /dev/null
@@ -1,85 +0,0 @@
-FROM node:slim AS app
-
-# Add build arguments
-ARG PUBLIC_SUPABASE_ANON_KEY
-ARG PUBLIC_SUPABASE_URL
-ARG CLOUDFLARE_BUCKET_DOMAIN
-ARG CLOUDFLARE_ACCESS_KEY
-ARG CLOUDFLARE_SECRET_ACCESS_KEY
-ARG CLOUDFLARE_ACCOUNT_ID
-ARG CLOUDFLARE_BUCKET_ID
-ARG SMTP_HOST
-ARG SMTP_USER
-ARG SMTP_PASSWORD
-ARG SMTP_PORT
-ARG SMTP_SENDER
-ARG ZOHO_TOKEN
-ARG CLOUDFLARE_RENDERING_API_KEY
-ARG OPENAPI_URL
-
-RUN mkdir /app
-WORKDIR /app
-
-# Install pnpm and healthcheck tools
-RUN npm install -g pnpm && \
- apt-get update && \
- apt-get install -y curl wget && \
- rm -rf /var/lib/apt/lists/*
-
-# Install chrome stable from sources, then remove it. Why? The
-# npm install of `puppeteer` brings its own bundle chromium build,
-# and puppeteer releases are only guaranteed to work with that version.
-#
-# But the bundled chromium implicitly needs a bunch of shared libs on
-# the host. It's a little tedious to find and maintain that list; but
-# the official apt distro of `google-chrome-stable` should bring the right
-# set along. So do that, but immediately uninstall (to free up layer space).
-#
-# This is a little brittle, since the puppeteer chrome could in theory diverge
-# from the official apt chrome's shared lib deps. But it works..
-# RUN apt-get update \
-# && apt-get install curl gnupg -y \
-# && curl --location --silent https://dl-ssl.google.com/linux/linux_signing_key.pub | apt-key add - \
-# && sh -c 'echo "deb [arch=amd64] http://dl.google.com/linux/chrome/deb/ stable main" >> /etc/apt/sources.list.d/google.list' \
-# && apt-get update \
-# && apt-get install google-chrome-stable -y --no-install-recommends \
-# && apt-get remove google-chrome-stable -y \
-# && rm -rf /var/lib/apt/lists/*
-
-# Add emoji fonts.
-# Source: https://gist.github.com/win0err/9d8c7f0feabdfe8a4c9787b02c79ac51
-# RUN mkdir ~/.fonts/ && \
-# wget https://github.com/samuelngs/apple-emoji-linux/releases/download/ios-15.4/AppleColorEmoji.ttf -O ~/.fonts/AppleColorEmoji.ttf
-
-COPY ./package.json /app
-RUN pnpm install
-COPY . /app
-
-# Set environment variables from build args
-ENV NODE_ENV=production \
- PUBLIC_SUPABASE_ANON_KEY=$PUBLIC_SUPABASE_ANON_KEY \
- PUBLIC_SUPABASE_URL=$PUBLIC_SUPABASE_URL \
- CLOUDFLARE_BUCKET_DOMAIN=$CLOUDFLARE_BUCKET_DOMAIN \
- CLOUDFLARE_ACCESS_KEY=$CLOUDFLARE_ACCESS_KEY \
- CLOUDFLARE_SECRET_ACCESS_KEY=$CLOUDFLARE_SECRET_ACCESS_KEY \
- CLOUDFLARE_ACCOUNT_ID=$CLOUDFLARE_ACCOUNT_ID \
- CLOUDFLARE_BUCKET_ID=$CLOUDFLARE_BUCKET_ID \
- CLOUDFLARE_RENDERING_API_KEY=$CLOUDFLARE_RENDERING_API_KEY \
- SMTP_HOST=$SMTP_HOST \
- SMTP_USER=$SMTP_USER \
- SMTP_PASSWORD=$SMTP_PASSWORD \
- SMTP_PORT=$SMTP_PORT \
- SMTP_SENDER=$SMTP_SENDER \
- ZOHO_TOKEN=$ZOHO_TOKEN \
- OPENAPI_URL=$OPENAPI_URL \
- PORT=3002
-
-RUN pnpm build
-
-EXPOSE 3002
-
-# Add healthcheck
-HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
- CMD curl -f http://localhost:3002 || exit 1
-
-CMD ["pnpm", "start"]
diff --git a/apps/backend/.dockerignore b/apps/backend/.dockerignore
deleted file mode 100644
index 5171c540..00000000
--- a/apps/backend/.dockerignore
+++ /dev/null
@@ -1,2 +0,0 @@
-node_modules
-npm-debug.log
\ No newline at end of file
diff --git a/apps/backend/.env.example b/apps/backend/.env.example
deleted file mode 100644
index 0f24be11..00000000
--- a/apps/backend/.env.example
+++ /dev/null
@@ -1,20 +0,0 @@
-## Supabase - DB
-PUBLIC_SUPABASE_ANON_KEY=
-PUBLIC_SUPABASE_URL=
-
-## Cloudflare - Video uploads
-# Not required - https://cdn.example.com
-CLOUDFLARE_BUCKET_DOMAIN=
-
-CLOUDFLARE_ACCESS_KEY=
-CLOUDFLARE_SECRET_ACCESS_KEY=
-CLOUDFLARE_ACCOUNT_ID=
-
-# Email
-SMTP_HOST=
-SMTP_USER=
-SMTP_PASSWORD=
-SMTP_PORT=
-
-# Sentry DNS
-SENTRY_DNS=
\ No newline at end of file
diff --git a/apps/backend/Dockerfile b/apps/backend/Dockerfile
deleted file mode 100644
index dff48ecc..00000000
--- a/apps/backend/Dockerfile
+++ /dev/null
@@ -1,71 +0,0 @@
-FROM node:slim AS app
-
-# Add build arguments
-ARG PUBLIC_SUPABASE_ANON_KEY
-ARG PUBLIC_SUPABASE_URL
-ARG CLOUDFLARE_BUCKET_DOMAIN
-ARG CLOUDFLARE_ACCESS_KEY
-ARG CLOUDFLARE_SECRET_ACCESS_KEY
-ARG CLOUDFLARE_ACCOUNT_ID
-ARG CLOUDFLARE_BUCKET_ID
-ARG SMTP_HOST
-ARG SMTP_USER
-ARG SMTP_PASSWORD
-ARG SMTP_PORT
-ARG SMTP_SENDER
-
-RUN mkdir /app
-WORKDIR /app
-
-# Install pnpm
-RUN npm install -g pnpm
-
-# Install chrome stable from sources, then remove it. Why? The
-# npm install of `puppeteer` brings its own bundle chromium build,
-# and puppeteer releases are only guaranteed to work with that version.
-#
-# But the bundled chromium implicitly needs a bunch of shared libs on
-# the host. It's a little tedious to find and maintain that list; but
-# the official apt distro of `google-chrome-stable` should bring the right
-# set along. So do that, but immediately uninstall (to free up layer space).
-#
-# This is a little brittle, since the puppeteer chrome could in theory diverge
-# from the official apt chrome's shared lib deps. But it works..
-# RUN apt-get update \
-# && apt-get install curl gnupg -y \
-# && curl --location --silent https://dl-ssl.google.com/linux/linux_signing_key.pub | apt-key add - \
-# && sh -c 'echo "deb [arch=amd64] http://dl.google.com/linux/chrome/deb/ stable main" >> /etc/apt/sources.list.d/google.list' \
-# && apt-get update \
-# && apt-get install google-chrome-stable -y --no-install-recommends \
-# && apt-get remove google-chrome-stable -y \
-# && rm -rf /var/lib/apt/lists/*
-
-# Add emoji fonts.
-# Source: https://gist.github.com/win0err/9d8c7f0feabdfe8a4c9787b02c79ac51
-# RUN mkdir ~/.fonts/ && \
-# wget https://github.com/samuelngs/apple-emoji-linux/releases/download/ios-15.4/AppleColorEmoji.ttf -O ~/.fonts/AppleColorEmoji.ttf
-
-COPY ./package.json /app
-RUN pnpm install
-COPY . /app
-
-# Set environment variables from build args
-ENV NODE_ENV=production \
- PUBLIC_SUPABASE_ANON_KEY=$PUBLIC_SUPABASE_ANON_KEY \
- PUBLIC_SUPABASE_URL=$PUBLIC_SUPABASE_URL \
- CLOUDFLARE_BUCKET_DOMAIN=$CLOUDFLARE_BUCKET_DOMAIN \
- CLOUDFLARE_ACCESS_KEY=$CLOUDFLARE_ACCESS_KEY \
- CLOUDFLARE_SECRET_ACCESS_KEY=$CLOUDFLARE_SECRET_ACCESS_KEY \
- CLOUDFLARE_ACCOUNT_ID=$CLOUDFLARE_ACCOUNT_ID \
- CLOUDFLARE_BUCKET_ID=$CLOUDFLARE_BUCKET_ID \
- SMTP_HOST=$SMTP_HOST \
- SMTP_USER=$SMTP_USER \
- SMTP_PASSWORD=$SMTP_PASSWORD \
- SMTP_PORT=$SMTP_PORT \
- PORT=3002
-
-RUN pnpm build
-
-EXPOSE 3002
-
-CMD ["pnpm", "start"]
diff --git a/apps/backend/certificate.html b/apps/backend/certificate.html
deleted file mode 100644
index 26392dbf..00000000
--- a/apps/backend/certificate.html
+++ /dev/null
@@ -1,431 +0,0 @@
-
-
-
-
-
-
-
-
-
- Certificate
-
-
-
-
-
-
-
-
- ${orgName}
-
-
-
-
This certificate is awarded to
- ${studentName}
-
-
-
-
has succesfully completed training on
- ${courseName}
-
-
-
-
-
${dateIssued}
-
Date Issued
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/apps/backend/course.html b/apps/backend/course.html
deleted file mode 100644
index ca9560e8..00000000
--- a/apps/backend/course.html
+++ /dev/null
@@ -1,182 +0,0 @@
-
-
-
-
-
-
-
-
-
-
- Lesson Note
-
-
-
-
-
-
- React js for Beginners
- By TheSpace
-
-
-
-
-
-
- 32
-
- The Yams
- By The Space
-
-
-
-
- Lesson Note: Introduction and Counselling
- Course: The Ultimate Guide to relocating to the UK via Study Path
- Lesson Overview:
- This lesson serves as the introduction to the course "The Ultimate Guide to relocating to the UK via
- Study Path." In this session, we will provide an overview of the course and discuss the importance of
- effective counseling in the relocation process.
- Learning Objectives:
- By the end of this lesson, students should be able to:
-
- Understand the goals and structure of the course.
- Recognize the significance of counseling in the relocation process.
- Acquire knowledge about the necessary steps involved in relocating to the UK through the study path.
-
- Lesson Content:
-
-
- Introduction to the Course
-
- Briefly introduce the course title and explain its relevance to students' interests.
- Highlight the main topics and modules covered throughout the course.
-
-
-
- Importance of Counseling
-
- Define the role of counseling in the relocation process.
- Discuss how counseling can assist students in making informed decisions regarding their studies and
- life in the UK.
- Explain the benefits of seeking professional guidance during the relocation journey.
-
-
-
- Steps for Relocating to the UK via Study Path
-
- Provide an overview of the necessary steps involved in relocating to the UK for study purposes.
- Discuss the importance of thorough planning, research, and documentation in the relocation process.
-
- Highlight key points such as visa applications, university admissions, accommodation arrangements, and
- financial considerations.
-
-
-
- Q&A and Discussion Time
-
- Allocate time for students to ask questions, provide comments, or share personal experiences.
- Encourage active participation and foster a supportive learning environment.
-
-
-
- Conclusion:
- In this lesson, we introduced the course "The Ultimate Guide to relocating to the UK via Study
- Path" and emphasized the significance of counseling in the relocation process. We explored the essential
- steps involved in relocating to the UK and highlighted the importance of thorough planning and research.
- Students should now have a better understanding of the course objectives and overall expectations. In the next
- session, we will delve deeper into the specific requirements for studying in the UK and provide detailed
- guidance on university admissions.
-
-
-
-
- Extra Resources
-
-
- Video Link:
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/apps/backend/fly.toml b/apps/backend/fly.toml
deleted file mode 100644
index 009283bc..00000000
--- a/apps/backend/fly.toml
+++ /dev/null
@@ -1,18 +0,0 @@
-# fly.toml app configuration file generated for classroomio-server on 2023-07-27T21:01:09+01:00
-#
-# See https://fly.io/docs/reference/configuration/ for information about how to use this file.
-#
-
-app = "classroomio-server"
-primary_region = "lhr"
-
-[build]
- dockerfile = "Dockerfile"
-
-[http_service]
- internal_port = 3002
- force_https = true
- auto_stop_machines = true
- auto_start_machines = true
- min_machines_running = 0
- processes = ["app"]
diff --git a/apps/backend/lesson.html b/apps/backend/lesson.html
deleted file mode 100644
index b31ff14c..00000000
--- a/apps/backend/lesson.html
+++ /dev/null
@@ -1,186 +0,0 @@
-
-
-
-
-
-
-
-
-
-
- Lesson Note
-
-
-
-
-
-
- React js for Beginners
- By TheSpace
-
-
-
-
-
-
- 32
-
- The Yams
- By The Space
-
-
-
-
- Lesson Note: Introduction and Counselling
- Course: The Ultimate Guide to relocating to the UK via Study Path
- Lesson Overview:
- This lesson serves as the introduction to the course "The Ultimate Guide to relocating to the UK via
- Study Path." In this session, we will provide an overview of the course and discuss the importance of
- effective counseling in the relocation process.
- Learning Objectives:
- By the end of this lesson, students should be able to:
-
- Understand the goals and structure of the course.
- Recognize the significance of counseling in the relocation process.
- Acquire knowledge about the necessary steps involved in relocating to the UK through the study path.
-
- Lesson Content:
-
-
- Introduction to the Course
-
- Briefly introduce the course title and explain its relevance to students' interests.
- Highlight the main topics and modules covered throughout the course.
-
-
-
- Importance of Counseling
-
- Define the role of counseling in the relocation process.
- Discuss how counseling can assist students in making informed decisions regarding their studies and
- life in the UK.
- Explain the benefits of seeking professional guidance during the relocation journey.
-
-
-
- Steps for Relocating to the UK via Study Path
-
- Provide an overview of the necessary steps involved in relocating to the UK for study purposes.
- Discuss the importance of thorough planning, research, and documentation in the relocation process.
-
- Highlight key points such as visa applications, university admissions, accommodation arrangements, and
- financial considerations.
-
-
-
- Q&A and Discussion Time
-
- Allocate time for students to ask questions, provide comments, or share personal experiences.
- Encourage active participation and foster a supportive learning environment.
-
-
-
- Conclusion:
- In this lesson, we introduced the course "The Ultimate Guide to relocating to the UK via Study
- Path" and emphasized the significance of counseling in the relocation process. We explored the essential
- steps involved in relocating to the UK and highlighted the importance of thorough planning and research.
- Students should now have a better understanding of the course objectives and overall expectations. In the next
- session, we will delve deeper into the specific requirements for studying in the UK and provide detailed
- guidance on university admissions.
-
-
-
-
- Extra Resources
-
-
- Video Link:
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/apps/backend/src/config/env.ts b/apps/backend/src/config/env.ts
deleted file mode 100644
index 0e18d88c..00000000
--- a/apps/backend/src/config/env.ts
+++ /dev/null
@@ -1,20 +0,0 @@
-import { z } from 'zod';
-
-const envSchema = z.object({
- PORT: z.string().optional(),
- NODE_ENV: z.enum(['development', 'production', 'test']).default('development'),
- CLOUDFLARE_BUCKET_DOMAIN: z.string().optional(),
- CLOUDFLARE_ACCESS_KEY: z.string().optional(),
- CLOUDFLARE_SECRET_ACCESS_KEY: z.string().optional(),
- CLOUDFLARE_ACCOUNT_ID: z.string().optional(),
- PUBLIC_SUPABASE_URL: z.string(),
- PUBLIC_SUPABASE_ANON_KEY: z.string(),
- ZOHO_TOKEN: z.string().optional(),
- SMTP_HOST: z.string().optional(),
- SMTP_PORT: z.string().optional(),
- SMTP_USER: z.string().optional(),
- SMTP_PASSWORD: z.string().optional(),
- SMTP_SENDER: z.string().optional()
-});
-
-export const env = envSchema.parse(process.env);
diff --git a/apps/backend/src/constants/index.ts b/apps/backend/src/constants/index.ts
deleted file mode 100644
index 9db00bc9..00000000
--- a/apps/backend/src/constants/index.ts
+++ /dev/null
@@ -1,25 +0,0 @@
-import { env } from '$src/config/env';
-
-const {
- CLOUDFLARE_BUCKET_DOMAIN,
- CLOUDFLARE_ACCESS_KEY,
- CLOUDFLARE_SECRET_ACCESS_KEY,
- CLOUDFLARE_ACCOUNT_ID
-} = env;
-
-export const CLOUDFLARE = {
- CONFIGS: {
- BUCKET_DOMAIN: CLOUDFLARE_BUCKET_DOMAIN!,
- ACCESS_KEY: CLOUDFLARE_ACCESS_KEY!,
- SECRET_ACCESS_KEY: CLOUDFLARE_SECRET_ACCESS_KEY!,
- ACCOUNT_ID: CLOUDFLARE_ACCOUNT_ID!
- },
- R2: {
- BUCKET: 'videos',
- BUCKET_DOMAIN: CLOUDFLARE_ACCOUNT_ID
- ? `https://${CLOUDFLARE_ACCOUNT_ID}.r2.cloudflarestorage.com`
- : CLOUDFLARE_BUCKET_DOMAIN!,
- PRESIGN_EXPIRATION_TIME: 60 * 60, // 1 hour
- DOWNLOAD_EXPIRATION_TIME: 60 * 60 * 3 // 3 hours
- }
-} as const;
diff --git a/apps/backend/src/middlewares/auth.ts b/apps/backend/src/middlewares/auth.ts
deleted file mode 100644
index 4c6d9aa2..00000000
--- a/apps/backend/src/middlewares/auth.ts
+++ /dev/null
@@ -1,44 +0,0 @@
-import { Context, Next } from 'hono';
-
-import { validateUser } from '$src/utils/auth/validate-user';
-
-export const authMiddleware = async (c: Context, next: Next) => {
- try {
- const authHeader = c.req.header('authorization');
-
- if (!authHeader) {
- return c.json(
- {
- success: false,
- message: 'No authorization header provided'
- },
- 401
- );
- }
-
- // Extract the token from "Bearer "
- const token = authHeader.split(' ')[1];
-
- if (!token) {
- return c.json(
- {
- success: false,
- message: 'Invalid authorization header format'
- },
- 401
- );
- }
-
- const user = await validateUser(token);
- c.set('user', user); // Attach the user to the context
- await next();
- } catch (error) {
- return c.json(
- {
- success: false,
- message: 'Unauthorized'
- },
- 401
- );
- }
-};
diff --git a/apps/backend/src/routes/course/katex.ts b/apps/backend/src/routes/course/katex.ts
deleted file mode 100644
index 49f8d70e..00000000
--- a/apps/backend/src/routes/course/katex.ts
+++ /dev/null
@@ -1,41 +0,0 @@
-import { Context, Hono } from 'hono';
-
-import katex from 'katex';
-import { z } from 'zod';
-
-export const katexRouter = new Hono();
-
-katexRouter.get('/', async (c: Context) => {
- try {
- const original = c.req.raw.url;
-
- const latexString = original.split('?')[1].replaceAll('+', '+').replaceAll('&space;', '');
-
- const html = katex.renderToString(latexString, {
- output: 'mathml',
- throwOnError: false
- });
-
- return c.html(html);
- } catch (error) {
- if (error instanceof z.ZodError) {
- return c.json(
- {
- success: false,
- error: 'Validation error',
- details: error.errors
- },
- 400
- );
- }
-
- console.error('Error rendering LaTeX:', error);
- return c.json(
- {
- success: false,
- error: error instanceof Error ? error.message : 'Unknown error occurred'
- },
- 500
- );
- }
-});
diff --git a/apps/backend/src/routes/mail.ts b/apps/backend/src/routes/mail.ts
deleted file mode 100644
index 539886db..00000000
--- a/apps/backend/src/routes/mail.ts
+++ /dev/null
@@ -1,70 +0,0 @@
-import { Context, Hono } from 'hono';
-import { sendWithNodemailer, sendWithZoho } from '$src/services/mail';
-
-import { ZSendEmailValidation } from '$src/types/mail';
-import { env } from '$src/config/env';
-import { z } from 'zod';
-
-export const mailRouter = new Hono();
-
-mailRouter.post('/send', async (c: Context) => {
- try {
- const data = await c.req.json();
- const validatedData = ZSendEmailValidation.parse(data);
-
- const results = await Promise.all(
- validatedData.map(async (emailData) => {
- try {
- const res = env.ZOHO_TOKEN
- ? await sendWithZoho(emailData)
- : await sendWithNodemailer(emailData);
-
- console.log('Email status:', res);
- return res;
- } catch (error) {
- console.error('Error sending email:', error);
- return {
- success: false,
- error: error instanceof Error ? error.message : 'Unknown error occurred',
- details: error
- };
- }
- })
- );
-
- const hasErrors = results.some((result) => !result.success);
- if (hasErrors) {
- return c.json(
- {
- success: false,
- error: 'Some emails failed to send',
- details: results
- },
- 500
- );
- }
-
- return c.json({ success: true, details: results });
- } catch (error) {
- if (error instanceof z.ZodError) {
- return c.json(
- {
- success: false,
- error: 'Validation error',
- details: error.errors
- },
- 400
- );
- }
-
- console.error('Error sending emails:', error);
- return c.json(
- {
- success: false,
- error: error instanceof Error ? error.message : 'Unknown error occurred',
- details: error
- },
- 500
- );
- }
-});
diff --git a/apps/backend/src/services/mail.ts b/apps/backend/src/services/mail.ts
deleted file mode 100644
index 2d46422e..00000000
--- a/apps/backend/src/services/mail.ts
+++ /dev/null
@@ -1,97 +0,0 @@
-import { extractNameAndEmail, withEmailTemplate } from '$src/utils/mail';
-import { nodemailerTransporter, zohoClient } from '$src/utils/email';
-
-import { Hono } from 'hono';
-import type { TSendEmailValidation } from '$src/types/mail';
-import type { Transporter } from 'nodemailer';
-import { env } from '$src/config/env';
-
-export const mailRouter = new Hono();
-
-interface EmailResponse {
- success: boolean;
- error?: string;
- details?: unknown;
-}
-
-let transporter: Transporter | null;
-
-nodemailerTransporter().then((t: Transporter | null) => {
- transporter = t;
-});
-
-export async function sendWithNodemailer(
- emailData: TSendEmailValidation[0]
-): Promise {
- const { from, to, subject, content, replyTo } = emailData;
-
- if (!transporter) {
- return {
- success: false,
- error: 'Email transporter not initialized'
- };
- }
-
- try {
- const result = await transporter.sendMail({
- from: from || env.SMTP_SENDER,
- to,
- subject,
- replyTo,
- html: withEmailTemplate(content)
- });
-
- return {
- success: true,
- details: result
- };
- } catch (error) {
- return {
- success: false,
- error: error instanceof Error ? error.message : 'Unknown error occurred',
- details: error
- };
- }
-}
-
-export async function sendWithZoho(emailData: TSendEmailValidation[0]): Promise {
- const { from, to, subject, content } = emailData;
-
- const fromData = extractNameAndEmail(from || '');
-
- if (!fromData) {
- return {
- success: false,
- error: 'Invalid from data'
- };
- }
-
- try {
- const result = await zohoClient.sendMail({
- from: {
- address: fromData.email,
- name: fromData.name
- },
- to: [
- {
- email_address: {
- address: to
- }
- }
- ],
- subject,
- htmlbody: content
- });
-
- return {
- success: true,
- details: result
- };
- } catch (error) {
- return {
- success: false,
- error: error instanceof Error ? error.message : 'Unknown error occurred',
- details: error
- };
- }
-}
diff --git a/apps/backend/src/types/course/index.ts b/apps/backend/src/types/course/index.ts
deleted file mode 100644
index 4ccb7062..00000000
--- a/apps/backend/src/types/course/index.ts
+++ /dev/null
@@ -1,39 +0,0 @@
-import { ALLOWED_CONTENT_TYPES, MAX_FILE_SIZE } from '$src/constants/upload';
-
-import { z } from 'zod';
-
-export * from './lesson';
-
-export const ZCourseDownloadContent = z.object({
- courseTitle: z.string(),
- orgName: z.string(),
- orgTheme: z.string(),
- lessons: z.array(
- z.object({
- lessonTitle: z.string(),
- lessonNumber: z.string(),
- lessonNote: z.string(),
- slideUrl: z.string().optional(),
- video: z.array(z.string()).optional()
- })
- )
-});
-
-export type TCourseDownloadContent = z.infer;
-
-export const ZCoursePresignUrlUpload = z.object({
- fileName: z.string().min(1, 'File name is required'),
- fileType: z.enum(ALLOWED_CONTENT_TYPES, {
- errorMap: () => ({
- message: `Invalid content type. Allowed types: ${ALLOWED_CONTENT_TYPES.join(', ')}`
- })
- })
-});
-
-export type TCoursePresignUrlUpload = z.infer;
-
-export const ZCourseDownloadPresignedUrl = z.object({
- keys: z.array(z.string()).min(1, 'File names array is required')
-});
-
-export type TCourseDownloadPresignedUrl = z.infer;
diff --git a/apps/backend/src/types/course/lesson.ts b/apps/backend/src/types/course/lesson.ts
deleted file mode 100644
index 66f24905..00000000
--- a/apps/backend/src/types/course/lesson.ts
+++ /dev/null
@@ -1,25 +0,0 @@
-import { z } from 'zod';
-
-export const ZLessonDownloadContent = z.object({
- title: z.string(),
- number: z.string(),
- orgName: z.string(),
- note: z.string(),
- slideUrl: z.string().optional(),
- video: z.array(z.string()).optional(),
- courseTitle: z.string()
-});
-
-export type TLessonDownloadContent = z.infer;
-
-export const ZCertificateDownload = z.object({
- theme: z.string().min(1),
- studentName: z.string().min(1),
- courseName: z.string().min(1),
- courseDescription: z.string().min(1),
- orgName: z.string().min(1),
- orgLogoUrl: z.string().url().optional(),
- facilitator: z.string().optional()
-});
-
-export type TCertificateDownload = z.infer;
diff --git a/apps/backend/src/types/mail.ts b/apps/backend/src/types/mail.ts
deleted file mode 100644
index 78116183..00000000
--- a/apps/backend/src/types/mail.ts
+++ /dev/null
@@ -1,13 +0,0 @@
-import { z } from 'zod';
-
-export const ZSendEmailValidation = z.array(
- z.object({
- from: z.string().optional(),
- to: z.string().email(),
- subject: z.string().min(1),
- content: z.string().min(1),
- replyTo: z.string().email().optional()
- })
-);
-
-export type TSendEmailValidation = z.infer;
diff --git a/apps/backend/src/utils/auth/validate-user.ts b/apps/backend/src/utils/auth/validate-user.ts
deleted file mode 100644
index 9ea20f2f..00000000
--- a/apps/backend/src/utils/auth/validate-user.ts
+++ /dev/null
@@ -1,21 +0,0 @@
-import { User } from '@supabase/supabase-js';
-import { getSupabase } from '../supabase.js';
-
-const supabase = getSupabase();
-
-export async function validateUser(accessToken: string): Promise {
- let user: User | null = null;
-
- try {
- const { data } = await supabase.auth.getUser(accessToken);
- user = data.user;
- } catch (error) {
- console.error(error);
- }
-
- if (!user) {
- throw new Error('Unauthenticated user');
- }
-
- return user;
-}
diff --git a/apps/backend/src/utils/certificate.ts b/apps/backend/src/utils/certificate.ts
deleted file mode 100644
index b9a40eb9..00000000
--- a/apps/backend/src/utils/certificate.ts
+++ /dev/null
@@ -1,124 +0,0 @@
-import type { TCertificateDownload } from '$src/types/course/lesson';
-import { getCloudflarePdfBuffer } from '$src/utils/cloudflare';
-
-export interface CertificateData {
- theme: string;
- studentName: string;
- courseName: string;
- courseDescription: string;
- orgLogoUrl: string;
- orgName: string;
- facilitator?: string;
-}
-
-export interface CertificateGenerationResult {
- success: boolean;
- buffer?: Buffer;
- error?: string;
-}
-
-export const generateCertificate = async (data: TCertificateDownload) => {
- const { html, styles } = generateCertificateHtml(data);
- return getCloudflarePdfBuffer(html, styles);
-};
-
-function generateCertificateHtml(data: TCertificateDownload): { html: string; styles: string } {
- const { studentName, courseName, courseDescription, orgLogoUrl, orgName, facilitator } = data;
-
- const html = `
-
-
-
- Certificate of Completion
-
-
-
-
-
-
${studentName}
-
${courseName}
-
${courseDescription}
-
-
-
-
-
- `;
-
- const styles = `
- body {
- font-family: 'Arial', sans-serif;
- margin: 0;
- padding: 0;
- background-color: #f5f5f5;
- }
- .certificate {
- width: 1123px;
- height: 794px;
- margin: 0 auto;
- background-color: white;
- position: relative;
- box-shadow: 0 0 10px rgba(0,0,0,0.1);
- }
- .header {
- text-align: center;
- padding: 20px;
- }
- .logo {
- max-width: 200px;
- margin-bottom: 20px;
- }
- .title {
- font-size: 36px;
- color: #333;
- margin-bottom: 10px;
- }
- .content {
- text-align: center;
- padding: 40px;
- }
- .student-name {
- font-size: 48px;
- color: #2c3e50;
- margin-bottom: 20px;
- }
- .course-name {
- font-size: 24px;
- color: #34495e;
- margin-bottom: 10px;
- }
- .description {
- font-size: 18px;
- color: #7f8c8d;
- margin-bottom: 30px;
- }
- .footer {
- position: absolute;
- bottom: 40px;
- width: 100%;
- text-align: center;
- }
- .signature {
- margin-top: 20px;
- }
- .date {
- margin-top: 10px;
- color: #95a5a6;
- }
- `;
- const pdf = {
- html: html,
- styles: styles
- };
-
- return pdf;
-}
diff --git a/apps/backend/src/utils/cloudflare.ts b/apps/backend/src/utils/cloudflare.ts
deleted file mode 100644
index 9d480803..00000000
--- a/apps/backend/src/utils/cloudflare.ts
+++ /dev/null
@@ -1,27 +0,0 @@
-import { CLOUDFLARE } from '$src/constants';
-
-export const getCloudflarePdfBuffer = async (html: string, styles?: string) => {
- console.log('Generating PDF with Cloudflare API...');
- try {
- const pdfResponse = await fetch(
- `https://api.cloudflare.com/client/v4/accounts/${CLOUDFLARE.CONFIGS.ACCOUNT_ID}/browser-rendering/pdf`,
- {
- method: 'POST',
- headers: {
- 'Content-Type': 'application/json',
- Authorization: `Bearer ${CLOUDFLARE.CONFIGS.ACCESS_KEY}`
- },
- body: JSON.stringify({
- html: html,
- addStyleTag: [{ content: `${styles}` }]
- })
- }
- );
-
- console.log('PDF response status:', pdfResponse.status);
- return pdfResponse;
- } catch (error) {
- console.error('Error generating PDF:', error);
- throw new Error(error instanceof Error ? error.message : 'Failed to generate PDF');
- }
-};
diff --git a/apps/backend/src/utils/course.ts b/apps/backend/src/utils/course.ts
deleted file mode 100644
index 200b2261..00000000
--- a/apps/backend/src/utils/course.ts
+++ /dev/null
@@ -1,170 +0,0 @@
-import type { TCourseDownloadContent } from '$src/types/course';
-import { marked } from 'marked';
-
-function getHtmlTemplate(body: string): string {
- return `
-
-
-
-
-
-
-
-
-
- Lesson Note
-
-
-
-
- ${body}
-
-
- `;
-}
-
-type TLessonBody = TCourseDownloadContent['lessons'][0];
-
-function getLessonBody(
- { lessonTitle, lessonNumber, lessonNote, slideUrl, video }: TLessonBody,
- orgName: string,
- orgTheme: string
-): string {
- const noteHtml = marked.parse(lessonNote);
- const showExtraResources = slideUrl || (video && video.length > 0);
- return `
-
-
- ${lessonNumber}
-
- ${lessonTitle}
- By ${orgName}
-
-
-
-
-
-
- ${
- showExtraResources
- ? `
-
- Extra Resources
- ${
- slideUrl
- ? ``
- : ''
- }
- ${
- video && video.length > 0
- ? `
- Video Link:
-
-
- ${video
- .map(
- (videoData, index) => `
- ${videoData}
- `
- )
- .join('')}
-
-
- `
- : ''
- }
-
- `
- : ''
- }
-
-
- `;
-}
-
-function getCourseBody({
- courseTitle,
- orgName,
- orgTheme,
- lessons
-}: TCourseDownloadContent): string {
- return `
-
- ${courseTitle}
- ${orgName}
-
-
- ${lessons.map((lesson) => getLessonBody(lesson, orgName, orgTheme)).join('')}
-
- `;
-}
-
-async function generateSinglePdfFromHtml(
- htmlContent: string,
- courseTitle: string,
- orgTheme: string
-): Promise {
- throw 'Download disabled';
-}
-
-export async function generateCoursePdf(params: TCourseDownloadContent): Promise {
- const body = getCourseBody(params);
- const html = getHtmlTemplate(body);
- console.log(html);
- return await generateSinglePdfFromHtml(html, params.courseTitle, params.orgTheme);
-}
diff --git a/apps/backend/src/utils/email.ts b/apps/backend/src/utils/email.ts
deleted file mode 100644
index 3a52bf81..00000000
--- a/apps/backend/src/utils/email.ts
+++ /dev/null
@@ -1,35 +0,0 @@
-import { SendMailClient } from 'zeptomail';
-import type { Transporter } from 'nodemailer';
-import { env } from '$src/config/env';
-import nodemailer from 'nodemailer';
-
-export const zohoClient = new SendMailClient({
- url: 'api.zeptomail.eu/',
- token: env.ZOHO_TOKEN!
-});
-
-export const nodemailerTransporter = async (): Promise => {
- if (!env.SMTP_HOST || !env.SMTP_PORT || !env.SMTP_USER || !env.SMTP_PASSWORD) {
- console.error('SMTP configuration missing');
- return null;
- }
-
- try {
- const transporter = nodemailer.createTransport({
- host: env.SMTP_HOST,
- port: parseInt(env.SMTP_PORT || '465', 10),
- secure: true,
- auth: {
- user: env.SMTP_USER,
- pass: env.SMTP_PASSWORD
- }
- });
-
- await transporter.verify();
-
- return transporter;
- } catch (error) {
- console.error('Transporter error:', error);
- return null;
- }
-};
diff --git a/apps/backend/src/utils/genUniqueId.ts b/apps/backend/src/utils/genUniqueId.ts
deleted file mode 100644
index b45b91f3..00000000
--- a/apps/backend/src/utils/genUniqueId.ts
+++ /dev/null
@@ -1,3 +0,0 @@
-export default function genUniqueId(): string {
- return Date.now().toString(36) + Math.random().toString(36).substr(2);
-}
diff --git a/apps/backend/src/utils/lesson.ts b/apps/backend/src/utils/lesson.ts
deleted file mode 100644
index 4f4c6241..00000000
--- a/apps/backend/src/utils/lesson.ts
+++ /dev/null
@@ -1,161 +0,0 @@
-import type { TLessonDownloadContent } from '$src/types/course/lesson';
-import { marked } from 'marked';
-
-function getHtmlTemplate(body: string): string {
- return `
-
-
-
-
-
-
-
-
-
- Lesson Note
-
-
-
-
- ${body}
-
-
- `;
-}
-
-function getLessonBody({
- title,
- number,
- orgName,
- note,
- slideUrl,
- video,
- courseTitle
-}: TLessonDownloadContent): string {
- const noteHtml = marked.parse(note);
- const showExtraResources = slideUrl || (video && video.length > 0);
- return `
-
-
-
- ${number}
-
- ${title}
- By ${orgName}
-
-
-
-
-
-
- ${
- showExtraResources
- ? `
-
- Extra Resources
- ${
- slideUrl
- ? ``
- : ''
- }
- ${
- video && video.length > 0
- ? `
- Video Link:
-
-
- ${video
- .map(
- (videoData, index) => `
- ${videoData}
- `
- )
- .join('')}
-
-
- `
- : ''
- }
-
- `
- : ''
- }
-
-
-
-
- `;
-}
-
-async function generateSinglePdfFromHtml(
- htmlContent: string,
- courseTitle: string
-): Promise {
- throw 'Download disabled';
-}
-
-export async function generateLessonPdf(params: TLessonDownloadContent): Promise {
- const body = getLessonBody(params);
- const html = getHtmlTemplate(body);
- console.log(html);
- return await generateSinglePdfFromHtml(html, params.courseTitle);
-}
diff --git a/apps/backend/src/utils/mail.ts b/apps/backend/src/utils/mail.ts
deleted file mode 100644
index 99525b6e..00000000
--- a/apps/backend/src/utils/mail.ts
+++ /dev/null
@@ -1,213 +0,0 @@
-interface FromData {
- name: string;
- email: string;
-}
-
-// format: "ClassroomIO Developers (via ClassroomIO.com)"
-export function extractNameAndEmail(str: string): FromData | undefined {
- // Use regular expressions to match the name and email
- const regex = /"(.*?)"\s+<\s*(.*?)@(.*?)\s*>/;
- const match = str.match(regex);
-
- if (match) {
- // Extract the name and email from the match groups
- const name = match[1];
- const email = match[2] + '@' + match[3];
- return { name, email };
- } else {
- // Return undefined if the format doesn't match
- return { name: str, email: str };
- }
-}
-
-export const withEmailTemplate = (content: string): string =>
- `
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- ${content}
-
-
-
-
-
-
-`;
diff --git a/apps/backend/src/utils/s3.ts b/apps/backend/src/utils/s3.ts
deleted file mode 100644
index 56b56d05..00000000
--- a/apps/backend/src/utils/s3.ts
+++ /dev/null
@@ -1,56 +0,0 @@
-import {
- DeleteObjectCommand,
- DeleteObjectCommandInput,
- GetObjectCommand,
- GetObjectCommandInput,
- PutObjectCommand,
- PutObjectCommandInput,
- S3Client
-} from '@aws-sdk/client-s3';
-
-import { CLOUDFLARE } from '$src/constants';
-import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
-
-export type GetSignedUrlParameters = Parameters;
-
-export const s3Client = new S3Client({
- region: 'auto',
- endpoint: `https://${CLOUDFLARE.CONFIGS.ACCOUNT_ID}.r2.cloudflarestorage.com`,
- credentials: {
- accessKeyId: CLOUDFLARE.CONFIGS.ACCESS_KEY,
- secretAccessKey: CLOUDFLARE.CONFIGS.SECRET_ACCESS_KEY
- }
-});
-
-export async function uploadToS3(
- params: PutObjectCommandInput
-): Promise<{ success: boolean; error?: string }> {
- try {
- await s3Client.send(new PutObjectCommand(params));
- return { success: true };
- } catch (error) {
- return { success: false, error: error instanceof Error ? error.message : 'Unknown error' };
- }
-}
-
-export async function getFromS3(
- params: GetObjectCommandInput
-): Promise<{ success: boolean; data?: any; error?: string }> {
- try {
- const data = await s3Client.send(new GetObjectCommand(params));
- return { success: true, data };
- } catch (error) {
- return { success: false, error: error instanceof Error ? error.message : 'Unknown error' };
- }
-}
-
-export async function deleteFromS3(
- params: DeleteObjectCommandInput
-): Promise<{ success: boolean; error?: string }> {
- try {
- await s3Client.send(new DeleteObjectCommand(params));
- return { success: true };
- } catch (error) {
- return { success: false, error: error instanceof Error ? error.message : 'Unknown error' };
- }
-}
diff --git a/apps/backend/src/utils/supabase.ts b/apps/backend/src/utils/supabase.ts
deleted file mode 100644
index cb0e461f..00000000
--- a/apps/backend/src/utils/supabase.ts
+++ /dev/null
@@ -1,36 +0,0 @@
-import { SupabaseClient, createClient } from '@supabase/supabase-js';
-
-import { env } from '$src/config/env';
-
-const { PUBLIC_SUPABASE_ANON_KEY, PUBLIC_SUPABASE_URL } = env;
-
-let supabase: SupabaseClient | null = null;
-
-interface SupabaseConfig {
- supabaseConfig: {
- url: string;
- anonKey: string;
- };
-}
-
-export function getSupabase(config?: SupabaseConfig): SupabaseClient {
- if (supabase) return supabase;
-
- const finalConfig = config ?? {
- supabaseConfig: {
- url: PUBLIC_SUPABASE_URL,
- anonKey: PUBLIC_SUPABASE_ANON_KEY
- }
- };
-
- try {
- supabase = createClient(finalConfig.supabaseConfig.url, finalConfig.supabaseConfig.anonKey, {
- auth: { persistSession: false }
- });
- } catch (error) {
- console.error('Error starting supabase', error);
- throw error;
- }
-
- return supabase;
-}
diff --git a/apps/backend/src/utils/upload.ts b/apps/backend/src/utils/upload.ts
deleted file mode 100644
index 99cd0198..00000000
--- a/apps/backend/src/utils/upload.ts
+++ /dev/null
@@ -1,24 +0,0 @@
-import { nanoid } from 'nanoid';
-import path from 'path';
-
-/**
- * Safely extracts the file extension from a filename.
- * Returns an empty string if no extension is found.
- */
-export function getExtension(filename: string): string {
- if (!filename || typeof filename !== 'string') return '';
- const ext = path.extname(filename).split('.');
- return ext.length > 1 ? ext[ext.length - 1] : '';
-}
-
-/**
- * Generates a unique file key for storage, preserving the file extension.
- * Throws an error if the filename is invalid or has no extension.
- */
-export function generateFileKey(fileName: string): string {
- const ext = getExtension(fileName);
- if (!ext) {
- throw new Error('Invalid file name or missing file extension');
- }
- return `${nanoid()}.${ext}`;
-}
diff --git a/apps/backend/vitest.config.ts b/apps/backend/vitest.config.ts
deleted file mode 100644
index f29f8b2a..00000000
--- a/apps/backend/vitest.config.ts
+++ /dev/null
@@ -1,13 +0,0 @@
-import { defineConfig } from 'vitest/config';
-
-export default defineConfig({
- test: {
- globals: true,
- environment: 'node',
- coverage: {
- provider: 'v8',
- reporter: ['text', 'json', 'html'],
- exclude: ['node_modules/', 'dist/']
- }
- }
-});
diff --git a/apps/dashboard/Dockerfile b/apps/dashboard/Dockerfile
deleted file mode 100644
index e97737b2..00000000
--- a/apps/dashboard/Dockerfile
+++ /dev/null
@@ -1,81 +0,0 @@
-# ----------- Build Stage -----------
-FROM node:18.18.0-alpine AS builder
-
-# Add build arguments
-ARG PUBLIC_SUPABASE_URL
-ARG PUBLIC_SUPABASE_ANON_KEY
-ARG PRIVATE_SUPABASE_SERVICE_ROLE
-ARG PRIVATE_APP_HOST
-ARG PRIVATE_APP_SUBDOMAINS
-ARG UNSPLASH_API_KEY
-ARG OPENAI_API_KEY
-ARG PUBLIC_SERVER_URL
-ARG PUBLIC_IP_REGISTRY_KEY
-
-WORKDIR /cio
-
-# Install pnpm
-RUN corepack enable && corepack prepare pnpm@latest --activate
-
-# Copy monorepo files
-COPY pnpm-lock.yaml package.json pnpm-workspace.yaml ./
-COPY apps/dashboard/package.json apps/dashboard/
-COPY packages/tsconfig ./packages/tsconfig
-COPY packages/shared ./packages/shared
-
-# Install all dependencies (monorepo context)
-RUN pnpm install
-
-# Copy the rest of the dashboard app
-COPY apps/dashboard ./apps/dashboard
-
-# Set env vars for SvelteKit to use Node adapter
-ENV PUBLIC_IS_SELFHOSTED=true \
- PUBLIC_SUPABASE_URL=$PUBLIC_SUPABASE_URL \
- PUBLIC_SUPABASE_ANON_KEY=$PUBLIC_SUPABASE_ANON_KEY \
- PRIVATE_SUPABASE_SERVICE_ROLE=$PRIVATE_SUPABASE_SERVICE_ROLE \
- PRIVATE_APP_HOST=$PRIVATE_APP_HOST \
- PRIVATE_APP_SUBDOMAINS=$PRIVATE_APP_SUBDOMAINS \
- UNSPLASH_API_KEY=$UNSPLASH_API_KEY \
- OPENAI_API_KEY=$OPENAI_API_KEY \
- PUBLIC_SERVER_URL=$PUBLIC_SERVER_URL \
- PUBLIC_IP_REGISTRY_KEY=$PUBLIC_IP_REGISTRY_KEY
-
-# Build the dashboard app
-WORKDIR /cio/apps/dashboard
-RUN pnpm install
-RUN NODE_OPTIONS=--max-old-space-size=4096 pnpm build
-
-# ----------- Production Stage -----------
-FROM node:18.18.0-alpine AS runner
-
-WORKDIR /app
-
-# Install pnpm
-RUN corepack enable && corepack prepare pnpm@latest --activate
-
-# Copy only production node_modules for dashboard
-COPY --from=builder ./cio/node_modules ./node_modules
-COPY --from=builder ./cio/package.json ./package.json
-COPY --from=builder ./cio/apps/dashboard/node_modules ./apps/dashboard/node_modules
-COPY --from=builder ./cio/apps/dashboard/package.json ./apps/dashboard/package.json
-COPY --from=builder ./cio/apps/dashboard/build ./apps/dashboard/build
-
-# Expose the port (default for SvelteKit Node adapter)
-EXPOSE 3000
-
-# Set environment variables for production
-ENV NODE_ENV=production \
- PUBLIC_IS_SELFHOSTED=true \
- PUBLIC_SUPABASE_URL=$PUBLIC_SUPABASE_URL \
- PUBLIC_SUPABASE_ANON_KEY=$PUBLIC_SUPABASE_ANON_KEY \
- PRIVATE_SUPABASE_SERVICE_ROLE=$PRIVATE_SUPABASE_SERVICE_ROLE \
- PRIVATE_APP_HOST=$PRIVATE_APP_HOST \
- PRIVATE_APP_SUBDOMAINS=$PRIVATE_APP_SUBDOMAINS \
- UNSPLASH_API_KEY=$UNSPLASH_API_KEY \
- OPENAI_API_KEY=$OPENAI_API_KEY \
- PUBLIC_SERVER_URL=$PUBLIC_SERVER_URL \
- PUBLIC_IP_REGISTRY_KEY=$PUBLIC_IP_REGISTRY_KEY
-
-# Start the app
-CMD ["pnpm", "dashboard:start"]
\ No newline at end of file
diff --git a/apps/dashboard/src/routes/+layout.server.ts b/apps/dashboard/src/routes/+layout.server.ts
index 0f22797b..aa6b499a 100644
--- a/apps/dashboard/src/routes/+layout.server.ts
+++ b/apps/dashboard/src/routes/+layout.server.ts
@@ -1,8 +1,8 @@
import { getSupabase, supabase } from '$lib/utils/functions/supabase';
import type { CurrentOrg } from '$lib/utils/types/org';
-import { PUBLIC_IS_SELFHOSTED } from '$env/static/public';
import type { MetaTagsProps } from 'svelte-meta-tags';
+import { PUBLIC_IS_SELFHOSTED } from '$env/static/public';
import { blockedSubdomain } from '$lib/utils/constants/app';
import { dev } from '$app/environment';
import { env } from '$env/dynamic/private';
diff --git a/apps/docs/pages/quickstart/self-hosting.mdx b/apps/docs/pages/quickstart/self-hosting.mdx
index 0c82065f..6e38e64f 100644
--- a/apps/docs/pages/quickstart/self-hosting.mdx
+++ b/apps/docs/pages/quickstart/self-hosting.mdx
@@ -140,8 +140,8 @@ docker compose up --build
The services will be available at:
-- Dashboard: http://localhost:3000
-- Backend: http://localhost:3002
+- Dashboard: http://localhost:3082
+- Backend: http://localhost:3081
### Service Configuration
@@ -149,7 +149,7 @@ The docker-compose file sets up two services:
#### Backend Service
-- Runs on port 3002
+- Runs on port 3081
- Uses Node.js slim image
- Mounts the api code directory
- Requires environment variables for:
@@ -159,7 +159,7 @@ The docker-compose file sets up two services:
#### Dashboard Service
-- Runs on port 3000
+- Runs on port 3082
- Uses Node.js Alpine image
- Uses pnpm for package management
- Requires environment variables for:
diff --git a/docker/.dockerignore b/docker/.dockerignore
new file mode 100644
index 00000000..5c017482
--- /dev/null
+++ b/docker/.dockerignore
@@ -0,0 +1,4 @@
+# This .dockerignore is a symlink reference
+# The actual .dockerignore is in the root directory
+# Docker will use the root .dockerignore when building from the docker/ directory
+
diff --git a/docker/Dockerfile.api b/docker/Dockerfile.api
new file mode 100644
index 00000000..6d8cd06d
--- /dev/null
+++ b/docker/Dockerfile.api
@@ -0,0 +1,31 @@
+FROM node:20.19.3-slim AS app
+
+RUN mkdir /app
+WORKDIR /app
+
+# Install pnpm, turbo and healthcheck tools
+RUN corepack enable && corepack prepare pnpm@latest --activate
+RUN npm install -g turbo@1.10.16
+
+COPY package.json ./
+COPY pnpm-lock.yaml ./
+COPY pnpm-workspace.yaml ./
+COPY turbo.json ./
+COPY packages/tsconfig ./packages/tsconfig
+COPY packages/shared ./packages/shared
+
+# Copy the api package.json
+COPY apps/api/package.json ./apps/api/package.json
+
+RUN pnpm install
+
+# Copy app source
+COPY . .
+
+RUN pnpm build --filter @cio/api
+
+EXPOSE 3081
+
+RUN ls -la
+
+CMD ["pnpm", "api:start"]
diff --git a/docker/Dockerfile.dashboard b/docker/Dockerfile.dashboard
new file mode 100644
index 00000000..c1e1fe2e
--- /dev/null
+++ b/docker/Dockerfile.dashboard
@@ -0,0 +1,60 @@
+# ----------- Build Stage -----------
+FROM node:20.19.3-alpine AS builder
+
+WORKDIR /cio
+# Install pnpm globally so it's available in the final container
+RUN npm install -g pnpm
+
+# Install pnpm
+RUN corepack enable && corepack prepare pnpm@latest --activate
+
+# Copy everything (filtered by .dockerignore)
+COPY package.json ./
+COPY pnpm-lock.yaml ./
+COPY pnpm-workspace.yaml ./
+COPY turbo.json ./
+COPY packages/tsconfig ./packages/tsconfig
+COPY packages/shared ./packages/shared
+
+# Copy the api package.json
+COPY apps/dashboard ./apps/dashboard
+COPY apps/api ./apps/api
+
+# Install all dependencies (monorepo context)
+RUN pnpm install
+
+RUN ls -la
+
+# Build the dashboard app
+RUN NODE_OPTIONS="--max-old-space-size=3072" PUBLIC_IS_SELFHOSTED=true pnpm build --filter @cio/dashboard
+
+# ----------- Production Stage -----------
+FROM node:20.19.3-alpine AS runner
+
+WORKDIR /app
+
+# Install pnpm and turbo before switching to non-root user
+RUN corepack enable && corepack prepare pnpm@latest --activate
+RUN npm install -g turbo@1.10.16
+
+# Don't run production as root
+RUN addgroup --system --gid 1001 nodejs
+RUN adduser --system --uid 1001 cio-user
+USER cio-user
+
+# Copy the entire dashboard app (including build output)
+COPY --from=builder --chown=cio-user:nodejs ./cio/apps/dashboard ./apps/dashboard
+COPY --from=builder --chown=cio-user:nodejs ./cio/node_modules ./node_modules
+COPY --from=builder --chown=cio-user:nodejs ./cio/package.json ./package.json
+COPY --from=builder --chown=cio-user:nodejs ./cio/turbo.json ./turbo.json
+COPY --from=builder --chown=cio-user:nodejs ./cio/pnpm-workspace.yaml ./pnpm-workspace.yaml
+
+# Expose the port (default for SvelteKit Node adapter)
+EXPOSE 3082
+
+# Set environment variables for production
+ENV NODE_ENV=production \
+ PUBLIC_IS_SELFHOSTED=true
+
+# Start the app
+CMD ["pnpm", "dashboard:start"]
\ No newline at end of file
diff --git a/docker-compose.yaml b/docker/docker-compose.yaml
similarity index 50%
rename from docker-compose.yaml
rename to docker/docker-compose.yaml
index 1be1f9e6..9fe3eef5 100644
--- a/docker-compose.yaml
+++ b/docker/docker-compose.yaml
@@ -8,28 +8,9 @@ version: "3.8"
services:
api:
- build:
- context: ./apps/api
- dockerfile: Dockerfile
- args:
- - PUBLIC_SUPABASE_ANON_KEY=${PUBLIC_SUPABASE_ANON_KEY}
- - PUBLIC_SUPABASE_URL=${PUBLIC_SUPABASE_URL}
- - CLOUDFLARE_BUCKET_DOMAIN=${CLOUDFLARE_BUCKET_DOMAIN}
- - CLOUDFLARE_ACCESS_KEY=${CLOUDFLARE_ACCESS_KEY}
- - CLOUDFLARE_SECRET_ACCESS_KEY=${CLOUDFLARE_SECRET_ACCESS_KEY}
- - CLOUDFLARE_ACCOUNT_ID=${CLOUDFLARE_ACCOUNT_ID}
- - CLOUDFLARE_BUCKET_ID=${CLOUDFLARE_BUCKET_ID}
- - SMTP_HOST=${SMTP_HOST}
- - SMTP_USER=${SMTP_USER}
- - SMTP_PASSWORD=${SMTP_PASSWORD}
- - SMTP_PORT=${SMTP_PORT}
+ image: classroomio/api:latest
ports:
- - "3002:3002"
- volumes:
- # Node modules (optional - for faster development)
- - ./apps/api/node_modules:/app/node_modules
- # Build output directory (if you want to access built files)
- - ./apps/api/dist:/app/dist
+ - "3081:3081"
environment:
- NODE_ENV=production
# Supabase - Database
@@ -46,27 +27,13 @@ services:
- SMTP_PASSWORD=${SMTP_PASSWORD}
- SMTP_PORT=${SMTP_PORT}
- SMTP_SENDER=${SMTP_SENDER}
+ # Redis
+ - REDIS_URL=${REDIS_URL}
dashboard:
- build:
- context: .
- dockerfile: ./apps/dashboard/Dockerfile
- args:
- - PUBLIC_SUPABASE_URL=${PUBLIC_SUPABASE_URL}
- - PUBLIC_SUPABASE_ANON_KEY=${PUBLIC_SUPABASE_ANON_KEY}
- - PRIVATE_SUPABASE_SERVICE_ROLE=${PRIVATE_SUPABASE_SERVICE_ROLE}
- - PRIVATE_APP_HOST=${PRIVATE_APP_HOST}
- - PRIVATE_APP_SUBDOMAINS=${PRIVATE_APP_SUBDOMAINS}
- - UNSPLASH_API_KEY=${UNSPLASH_API_KEY}
- - OPENAI_API_KEY=${OPENAI_API_KEY}
- - PUBLIC_SERVER_URL=${PUBLIC_SERVER_URL}
- - PUBLIC_IP_REGISTRY_KEY=${PUBLIC_IP_REGISTRY_KEY}
+ image: classroomio/dashboard:latest
ports:
- - "3000:3000"
- volumes:
- - .:/app
- - /app/node_modules
- - /app/apps/dashboard/node_modules
+ - "3082:3082"
environment:
- PUBLIC_IS_SELFHOSTED=true
# Supabase - Database
diff --git a/docker/docker-push.sh b/docker/docker-push.sh
new file mode 100755
index 00000000..3666f240
--- /dev/null
+++ b/docker/docker-push.sh
@@ -0,0 +1,62 @@
+#!/bin/bash
+
+# Docker Hub Push Script for ClassroomIO
+# This script builds and pushes the API and Dashboard images to Docker Hub
+
+set -e # Exit on error
+
+# Configuration
+DOCKERHUB_USERNAME="${DOCKERHUB_USERNAME:-classroomio}" # Change this to your Docker Hub username
+VERSION="${VERSION:-latest}" # Can be overridden with VERSION env var
+
+# Colors for output
+GREEN='\033[0;32m'
+BLUE='\033[0;34m'
+RED='\033[0;31m'
+NC='\033[0m' # No Color
+
+echo -e "${BLUE}================================${NC}"
+echo -e "${BLUE}ClassroomIO Docker Hub Publisher${NC}"
+echo -e "${BLUE}================================${NC}"
+echo ""
+
+# Build and push API
+echo -e "${GREEN}Building API image...${NC}"
+docker build \
+ -f docker/Dockerfile.api \
+ -t ${DOCKERHUB_USERNAME}/api:${VERSION} \
+ -t ${DOCKERHUB_USERNAME}/api:latest \
+ .
+
+echo -e "${GREEN}Pushing API image to Docker Hub...${NC}"
+docker push ${DOCKERHUB_USERNAME}/api:${VERSION}
+docker push ${DOCKERHUB_USERNAME}/api:latest
+
+echo -e "${GREEN}✓ API image published successfully!${NC}"
+echo ""
+
+# Build and push Dashboard
+echo -e "${GREEN}Building Dashboard image...${NC}"
+docker build \
+ -f docker/Dockerfile.dashboard \
+ -t ${DOCKERHUB_USERNAME}/dashboard:${VERSION} \
+ -t ${DOCKERHUB_USERNAME}/dashboard:latest \
+ .
+
+echo -e "${GREEN}Pushing Dashboard image to Docker Hub...${NC}"
+docker push ${DOCKERHUB_USERNAME}/dashboard:${VERSION}
+docker push ${DOCKERHUB_USERNAME}/dashboard:latest
+
+echo -e "${GREEN}✓ Dashboard image published successfully!${NC}"
+
+# Summary
+echo -e "${BLUE}================================${NC}"
+echo -e "${GREEN}All images published successfully!${NC}"
+echo -e "${BLUE}================================${NC}"
+echo ""
+echo "API Image: ${DOCKERHUB_USERNAME}/api:${VERSION}"
+echo "Dashboard Image: ${DOCKERHUB_USERNAME}/dashboard:${VERSION}"
+echo ""
+echo "To pull these images:"
+echo " docker pull ${DOCKERHUB_USERNAME}/api:${VERSION}"
+echo " docker pull ${DOCKERHUB_USERNAME}/dashboard:${VERSION}
diff --git a/docker/docs/README.md b/docker/docs/README.md
new file mode 100644
index 00000000..5db22022
--- /dev/null
+++ b/docker/docs/README.md
@@ -0,0 +1,215 @@
+# Publishing ClassroomIO Docker Images to Docker Hub
+
+This guide explains how to publish the ClassroomIO API and Dashboard Docker images to Docker Hub.
+
+## Prerequisites
+
+1. **Docker Hub Account**: Create an account at https://hub.docker.com if you don't have one
+2. **Docker Installed**: Make sure Docker is installed and running on your machine
+3. **Docker Hub Access Token**: Create a personal access token at https://hub.docker.com/settings/security
+
+## Method 1: Manual Publishing (Local)
+
+### Step 1: Login to Docker Hub
+
+```bash
+docker login
+# Enter your Docker Hub username and password/token when prompted
+```
+
+### Step 2: Build and Push Images
+
+Use the provided script to build and push both images:
+
+```bash
+# Default (uses 'classroomio' as username and 'latest' as version)
+./docker/docker-push.sh
+
+# Custom username
+DOCKERHUB_USERNAME=your-username ./docker/docker-push.sh
+
+# Specific version
+VERSION=v0.1.0 ./docker/docker-push.sh
+
+# Both custom username and version
+DOCKERHUB_USERNAME=classroomio VERSION=v0.1.0 ./docker/docker-push.sh
+```
+
+### Step 3: Verify Images
+
+Check your Docker Hub repository at:
+- https://hub.docker.com/r/classroomio/api
+- https://hub.docker.com/r/classroomio/dashboard
+
+## Method 2: GitHub Actions (Automated)
+
+### Setup (One-time)
+
+1. **Add Docker Hub Secrets to GitHub**:
+ - Go to your GitHub repository → Settings → Secrets and variables → Actions
+ - Add two secrets:
+ - `DOCKERHUB_USERNAME`: Your Docker Hub username
+ - `DOCKERHUB_TOKEN`: Your Docker Hub access token
+
+2. **Push the workflow file** (already created at `.github/workflows/docker-publish.yml`)
+
+### Publishing via GitHub Actions
+
+The workflow will automatically publish images when:
+
+1. **Push to main/master branch**: Creates a `latest` tag
+2. **Create a version tag**: Creates versioned tags
+3. **Manual trigger**: Use the Actions tab in GitHub
+
+#### Create a version tag and push:
+
+```bash
+git tag v0.1.0
+git push origin v0.1.0
+```
+
+#### Manual trigger from GitHub:
+
+1. Go to Actions tab in your repository
+2. Select "Publish Docker Images" workflow
+3. Click "Run workflow"
+4. Enter optional version tag (or leave default 'latest')
+
+## Method 3: Individual Image Building
+
+### Build API Image
+
+```bash
+docker build -f docker/Dockerfile.api -t classroomio/api:latest .
+docker push classroomio/api:latest
+```
+
+### Build Dashboard Image
+
+```bash
+docker build -f docker/Dockerfile.dashboard -t classroomio/dashboard:latest .
+docker push classroomio/dashboard:latest
+```
+
+## Using Published Images
+
+### Pull Images
+
+```bash
+docker pull classroomio/api:latest
+docker pull classroomio/dashboard:latest
+```
+
+### Update docker-compose.yml
+
+Instead of building locally, use the published images:
+
+```yaml
+services:
+ api:
+ image: classroomio/api:latest
+ # Remove the 'build' section
+ ports:
+ - "3081:3081"
+ environment:
+ # ... your environment variables
+
+ dashboard:
+ image: classroomio/dashboard:latest
+ # Remove the 'build' section
+ ports:
+ - "3082:3082"
+ environment:
+ # ... your environment variables
+```
+
+## Versioning Strategy
+
+### Recommended Tags:
+
+- `latest`: Most recent stable build
+- `v0.1.0`: Specific semantic version
+- `v1.0`: Minor version
+- `v1`: Major version
+- `main`: Latest main branch build
+- `dev`: Development builds
+
+### Example: Publishing a new version
+
+```bash
+# Build with multiple tags
+docker build -f docker/Dockerfile.api \
+ -t classroomio/api:latest \
+ -t classroomio/api:v1.2.0 \
+ -t classroomio/api:v1.2 \
+ -t classroomio/api:v1 \
+ .
+
+# Push all tags
+docker push classroomio/api:latest
+docker push classroomio/api:v1.2.0
+docker push classroomio/api:v1.2
+docker push classroomio/api:v1
+```
+
+## Image Sizes and Build Times
+
+- **API Image**: ~400-600 MB (Node.js Alpine + dependencies)
+- **Dashboard Image**: ~600-800 MB (Node.js Alpine + SvelteKit build)
+- **Build Time**: 5-10 minutes per image (depending on your machine)
+
+## Multi-Architecture Support
+
+The GitHub Actions workflow builds for both `linux/amd64` and `linux/arm64` platforms, making the images compatible with:
+- x86_64 servers
+- ARM-based servers (like AWS Graviton)
+- Apple Silicon Macs (M1/M2/M3)
+
+## Troubleshooting
+
+### "unauthorized: authentication required"
+- Run `docker login` again
+- Check your Docker Hub token hasn't expired
+
+### "denied: requested access to the resource is denied"
+- Make sure you're pushing to your own username
+- Repository might need to be created first on Docker Hub
+
+### Build failures
+- Check Dockerfile syntax
+- Ensure all build arguments are provided
+- Check available disk space: `docker system df`
+
+### Clean up old images
+```bash
+# Remove dangling images
+docker image prune
+
+# Remove all unused images
+docker image prune -a
+```
+
+## Security Best Practices
+
+1. **Never commit secrets** to the repository
+2. **Use Docker Hub access tokens** instead of passwords
+3. **Enable 2FA** on your Docker Hub account
+4. **Scan images** for vulnerabilities:
+ ```bash
+ docker scan classroomio/api:latest
+ ```
+5. **Use specific version tags** in production, not `latest`
+
+## Support
+
+For issues with publishing, please check:
+- Docker Hub status: https://status.docker.com
+- GitHub Actions logs in your repository
+- Docker daemon logs: `docker info`
+
+## References
+
+- [Docker Hub Documentation](https://docs.docker.com/docker-hub/)
+- [GitHub Actions for Docker](https://docs.github.com/en/actions/publishing-packages/publishing-docker-images)
+- [Docker Build Documentation](https://docs.docker.com/engine/reference/commandline/build/)
+
diff --git a/docker/docs/USAGE.md b/docker/docs/USAGE.md
new file mode 100644
index 00000000..23738f18
--- /dev/null
+++ b/docker/docs/USAGE.md
@@ -0,0 +1,141 @@
+# Docker Usage Guide
+
+This directory contains all Docker-related files for the ClassroomIO project.
+
+## 📁 Directory Structure
+
+```
+docker/
+├── README.md # Full publishing guide
+├── USAGE.md # This file - quick usage guide
+├── commands.md # Quick command reference
+├── docker-compose.yaml # Docker Compose configuration
+├── docker-push.sh # Script to publish images to Docker Hub
+├── Dockerfile.api # API service Dockerfile
+└── Dockerfile.dashboard # Dashboard service Dockerfile
+```
+
+## 🚀 Quick Start
+
+### Using Docker Compose (Recommended)
+
+```bash
+# From the project root
+cd docker
+docker-compose up -d
+
+# Or from project root
+docker-compose -f docker/docker-compose.yaml up -d
+```
+
+### Building Individual Images
+
+```bash
+# From project root
+docker build -f docker/Dockerfile.api -t api .
+docker build -f docker/Dockerfile.dashboard -t dashboard .
+```
+
+## 📦 Publishing to Docker Hub
+
+```bash
+# From project root
+./docker/docker-push.sh
+
+# With custom options
+DOCKERHUB_USERNAME=your-username VERSION=v1.0.0 ./docker/docker-push.sh
+```
+
+## 🔧 Common Commands
+
+### Start Services
+```bash
+cd docker && docker-compose up -d
+```
+
+### Stop Services
+```bash
+cd docker && docker-compose down
+```
+
+### View Logs
+```bash
+cd docker && docker-compose logs -f
+```
+
+### Rebuild Services
+```bash
+cd docker && docker-compose up -d --build
+```
+
+## 📚 Documentation
+
+- **README.md**: Complete Docker Hub publishing guide
+- **commands.md**: Quick command reference
+- **GitHub Actions**: Automated publishing via `.github/workflows/docker-publish.yml`
+
+## 🌍 Environment Variables
+
+Copy `.env.example` to `.env` in the project root and configure:
+
+```bash
+cp ../.env.example ../.env
+```
+
+Required variables are listed in `docker-compose.yaml`.
+
+## 🔗 Published Images
+
+- **API**: `classroomio/api` or `classroomio/api`
+- **Dashboard**: `classroomio/dashboard` or `classroomio/dashboard`
+
+Pull from Docker Hub:
+```bash
+docker pull classroomio/api:latest
+docker pull classroomio/dashboard:latest
+```
+
+## 💡 Tips
+
+1. **Always run builds from project root** - the context needs to be the entire monorepo
+2. **Use docker-compose** for local development
+3. **Use published images** for production deployments
+4. **Check logs** if services fail to start: `docker-compose logs -f`
+
+## 🐛 Troubleshooting
+
+### Services won't start
+```bash
+# Check logs
+docker-compose logs -f
+
+# Rebuild from scratch
+docker-compose down
+docker-compose build --no-cache
+docker-compose up -d
+```
+
+### Port conflicts
+```bash
+# Check if ports are in use
+lsof -i :3081 # API
+lsof -i :3082 # Dashboard
+
+# Change ports in docker-compose.yaml if needed
+```
+
+### Build failures
+```bash
+# Clean up Docker
+docker system prune -a
+
+# Try building again
+docker-compose build --no-cache
+```
+
+## 📞 Support
+
+For more detailed information, see:
+- `README.md` - Full Docker Hub publishing guide
+- `commands.md` - Quick command reference
+- Project main README for general setup
diff --git a/docker/docs/commands.md b/docker/docs/commands.md
new file mode 100644
index 00000000..c8b88f5f
--- /dev/null
+++ b/docker/docs/commands.md
@@ -0,0 +1,90 @@
+# Quick Docker Commands Reference
+
+## Login to Docker Hub
+```bash
+docker login
+```
+
+## Build Images Locally
+
+### API
+```bash
+docker build -f ./docker/Dockerfile.api -t classroomio/api:latest .
+```
+
+### Dashboard
+```bash
+docker build -f ./docker/Dockerfile.dashboard -t classroomio/dashboard:latest .
+```
+
+## Push to Docker Hub
+
+### API
+```bash
+docker push classroomio/api:latest
+```
+
+### Dashboard
+```bash
+docker push classroomio/dashboard:latest
+```
+
+## All in One Script
+```bash
+# Make script executable (first time only)
+chmod +x docker-push.sh
+
+# Run the script
+./docker-push.sh
+
+# With custom username
+DOCKERHUB_USERNAME=your-username ./docker-push.sh
+
+# With version tag
+VERSION=v1.0.0 ./docker-push.sh
+```
+
+## Pull Published Images
+```bash
+docker pull classroomio/api:latest
+docker pull classroomio/dashboard:latest
+```
+
+## Run Published Images
+```bash
+# API
+docker run -d -p 3081:3081 \
+ -e PUBLIC_SUPABASE_ANON_KEY=your_key \
+ -e PUBLIC_SUPABASE_URL=your_url \
+ classroomio/api:latest
+
+# Dashboard
+docker run -d -p 3082:3082 \
+ -e PUBLIC_SUPABASE_ANON_KEY=your_key \
+ -e PUBLIC_SUPABASE_URL=your_url \
+ classroomio/dashboard:latest
+```
+
+## Useful Commands
+
+### Check local images
+```bash
+docker images | grep classroomio
+```
+
+### Remove local images
+```bash
+docker rmi classroomio/api:latest
+docker rmi classroomio/dashboard:latest
+```
+
+### View image details
+```bash
+docker inspect classroomio/api:latest
+```
+
+### Check image size
+```bash
+docker images classroomio/api:latest --format "table {{.Repository}}\t{{.Tag}}\t{{.Size}}"
+```
+
diff --git a/package.json b/package.json
index 48f485c9..e08d353e 100644
--- a/package.json
+++ b/package.json
@@ -3,14 +3,15 @@
"version": "0.1.1",
"scripts": {
"build": "turbo run build",
+ "start": "turbo run start",
"dev": "turbo prepare --concurrency 13 && turbo run dev",
"lint": "turbo run lint",
"clean": "turbo clean && rm -rf node_modules",
"format": "prettier . --write",
"release": "standard-version",
"ci": "cypress run",
- "dashboard:build": "cd apps/dashboard && pnpm i && PUBLIC_IS_SELFHOSTED=true NODE_OPTIONS='--max-old-space-size=20384' pnpm build",
- "dashboard:start": "cd apps/dashboard && node build",
+ "dashboard:start": "PORT=3082 pnpm start --filter @cio/dashboard",
+ "api:start": "PORT=3081 pnpm start --filter @cio/api",
"supabase:push": "pnpx supabase link --project-ref $PROJECT_ID && pnpx supabase db push --include-all"
},
"license": "MIT",
diff --git a/turbo.json b/turbo.json
index 0cec25d5..a6f4535a 100644
--- a/turbo.json
+++ b/turbo.json
@@ -9,6 +9,10 @@
"dependsOn": ["^build"],
"outputs": [".next/**", "!.next/cache/**", ".svelte-kit/**", ".vercel/**", "dist/**"]
},
+ "start": {
+ "cache": false,
+ "persistent": true
+ },
"lint": {
"dependsOn": ["^build"]
},