diff --git a/Dockerfile b/Dockerfile index 2730dccb9..a605e47e1 100644 --- a/Dockerfile +++ b/Dockerfile @@ -24,12 +24,14 @@ ARG PUBLIC_APPWRITE_ENDPOINT ARG PUBLIC_GROWTH_ENDPOINT ARG PUBLIC_STRIPE_KEY ARG SENTRY_AUTH_TOKEN +ARG PUBLIC_PROJECT_PROFILE ENV PUBLIC_APPWRITE_ENDPOINT=$PUBLIC_APPWRITE_ENDPOINT ENV PUBLIC_GROWTH_ENDPOINT=$PUBLIC_GROWTH_ENDPOINT ENV PUBLIC_CONSOLE_MODE=$PUBLIC_CONSOLE_MODE ENV PUBLIC_STRIPE_KEY=$PUBLIC_STRIPE_KEY ENV SENTRY_AUTH_TOKEN=$SENTRY_AUTH_TOKEN +ENV PUBLIC_PROJECT_PROFILE=$PUBLIC_PROJECT_PROFILE ENV NODE_OPTIONS=--max_old_space_size=8192 RUN pnpm run build @@ -38,5 +40,5 @@ FROM nginx:1.25-alpine EXPOSE 80 -COPY docker/nginx.conf /etc/nginx/conf.d/default.conf +COPY docker/nginx.$PUBLIC_PROJECT_PROFILE.conf /etc/nginx/conf.d/default.conf COPY --from=build /app/build /usr/share/nginx/html/console \ No newline at end of file diff --git a/docker/nginx.conf b/docker/nginx.console.conf similarity index 100% rename from docker/nginx.conf rename to docker/nginx.console.conf diff --git a/docker/nginx.studio.conf b/docker/nginx.studio.conf new file mode 100644 index 000000000..1110b3b06 --- /dev/null +++ b/docker/nginx.studio.conf @@ -0,0 +1,39 @@ +map $sent_http_content_type $expires { + # cache everything for 1 year + default 1y; + # html files shouldn't be cached for single-page applications + text/html off; +} + +server { + listen 80; + server_name localhost; + + # serve compressed file if filename.gz exists + gzip_static on; + + location /studio { + root /usr/share/nginx/html; + index index.html index.htm; + try_files $uri /studio/index.html; + + # Add cache headers + expires $expires; + add_header Pragma public; + add_header Cache-Control "public"; + + # Deny IE browsers from going into quirks mode + add_header X-UA-Compatible "IE=Edge"; + # X-Frame-Options is to prevent from clickJacking attack + add_header X-Frame-Options SAMEORIGIN; + # This header enables the Cross-site scripting (XSS) filter + add_header X-XSS-Protection "1; mode=block;"; + # disable content-type sniffing on some browsers. + add_header X-Content-Type-Options nosniff; + } + + location / { + absolute_redirect off; + return 301 /studio; + } +} \ No newline at end of file diff --git a/package.json b/package.json index 227442430..ada91dd38 100644 --- a/package.json +++ b/package.json @@ -34,6 +34,7 @@ "cron-parser": "^4.9.0", "dayjs": "^1.11.13", "deep-equal": "^2.2.3", + "dotenv": "^16.4.7", "echarts": "^5.6.0", "envfile": "^7.1.0", "ignore": "^6.0.2", @@ -48,6 +49,7 @@ }, "devDependencies": { "@eslint/compat": "^1.2.7", + "@eslint/js": "^9.23.0", "@melt-ui/pp": "^0.3.2", "@melt-ui/svelte": "^0.86.5", "@playwright/test": "^1.51.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ea9cc1732..fdcf03169 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -47,6 +47,9 @@ importers: deep-equal: specifier: ^2.2.3 version: 2.2.3 + dotenv: + specifier: ^16.4.7 + version: 16.4.7 echarts: specifier: ^5.6.0 version: 5.6.0 @@ -84,6 +87,9 @@ importers: '@eslint/compat': specifier: ^1.2.7 version: 1.2.7(eslint@9.23.0) + '@eslint/js': + specifier: ^9.23.0 + version: 9.23.0 '@melt-ui/pp': specifier: ^0.3.2 version: 0.3.2(@melt-ui/svelte@0.86.5(svelte@5.25.3))(svelte@5.25.3) diff --git a/src/app.html b/src/app.html index 18a92b1fa..675d85d59 100644 --- a/src/app.html +++ b/src/app.html @@ -5,9 +5,9 @@ - - - + + + %sveltekit.head% diff --git a/src/hooks.server.ts b/src/hooks.server.ts index e54c183db..8c17e3e98 100644 --- a/src/hooks.server.ts +++ b/src/hooks.server.ts @@ -2,6 +2,7 @@ import { sequence } from '@sveltejs/kit/hooks'; import { handleErrorWithSentry, sentryHandle } from '@sentry/sveltekit'; import * as Sentry from '@sentry/sveltekit'; import { isCloud, isProd } from '$lib/system'; +import type { Handle } from '@sveltejs/kit'; Sentry.init({ enabled: isCloud && isProd, @@ -9,6 +10,26 @@ Sentry.init({ tracesSampleRate: 1.0 }); -export const handle = sequence(sentryHandle()); +const projectProfile = process.env.PUBLIC_PROJECT_PROFILE || 'console'; + +const dynamicHtmlTransform: Handle = async ({ event, resolve }) => { + let response = await resolve(event); + + if (response.headers.get('content-type')?.includes('text/html')) { + let html = await response.text(); + + // Replace '/console/' dynamically with the project profile + html = html.replace(/\/{project_profile}\//g, `/${projectProfile}/`); + + response = new Response(html, { + status: response.status, + headers: response.headers + }); + } + + return response; +}; + +export const handle = sequence(sentryHandle(), dynamicHtmlTransform); export const handleError = handleErrorWithSentry(); diff --git a/src/lib/components/breadcrumbs.svelte b/src/lib/components/breadcrumbs.svelte index 4acd8a6a4..194d0b6e9 100644 --- a/src/lib/components/breadcrumbs.svelte +++ b/src/lib/components/breadcrumbs.svelte @@ -9,7 +9,7 @@ } from '@appwrite.io/pink-icons-svelte'; import { BottomSheet } from '$lib/components'; import { isSmallViewport } from '$lib/stores/viewport'; - import { isCloud } from '$lib/system'; + import { isCloud, isStudio } from '$lib/system'; import { goto } from '$app/navigation'; import { base } from '$app/paths'; import { newOrgModal } from '$lib/stores/organization'; @@ -80,7 +80,7 @@ title: 'Switch Organization', items: organizations.map((organization) => ({ name: organization.name, - href: `/console/organization-${organization?.$id}` + href: `${base}/organization-${organization?.$id}` })) }, bottom: { @@ -101,7 +101,7 @@ items: [ { name: 'Organization overview', - href: `/console/organization-${selectedOrg?.$id}` + href: `${base}/organization-${selectedOrg?.$id}` } ] }, @@ -139,12 +139,12 @@ if (index < 4) { return { name: project.name, - href: `/console/project-${project.$id}/overview` + href: `${base}/project-${project.$id}/overview` }; } else if (index === 4) { return { name: 'All projects', - href: `/console/organization-${selectedOrg?.$id}` + href: `${base}/organization-${selectedOrg?.$id}` }; } return null; @@ -156,7 +156,7 @@ { name: 'Create project', trailingIcon: IconPlus, - href: `/console/organization-${selectedOrg?.$id}?create-project` + href: `${base}/organization-${selectedOrg?.$id}?create-project` } ] }, @@ -167,7 +167,7 @@ { name: 'Create project', trailingIcon: IconPlus, - href: `/console/organization-${selectedOrg?.$id}?create-project` + href: `${base}/organization-${selectedOrg?.$id}?create-project` } ] } @@ -185,7 +185,9 @@
{#if !$isSmallViewport} - / + {#if !isStudio} + / + {/if}
@@ -239,7 +241,7 @@
{organization.name}
@@ -270,7 +272,7 @@
{organization.name}
@@ -315,7 +317,7 @@ {#if index < 4}
- {project.name}
@@ -323,7 +325,7 @@
All projects
@@ -335,7 +337,7 @@ Create project diff --git a/src/lib/components/studio/sidebarOrganization.svelte b/src/lib/components/studio/sidebarOrganization.svelte new file mode 100644 index 000000000..0af4c1ef8 --- /dev/null +++ b/src/lib/components/studio/sidebarOrganization.svelte @@ -0,0 +1,37 @@ + + + + + diff --git a/src/lib/components/studio/sidebarProject.svelte b/src/lib/components/studio/sidebarProject.svelte new file mode 100644 index 000000000..64b2166a3 --- /dev/null +++ b/src/lib/components/studio/sidebarProject.svelte @@ -0,0 +1,35 @@ + + + + + diff --git a/src/lib/layout/container.svelte b/src/lib/layout/container.svelte index abe25a4a9..a104f9f43 100644 --- a/src/lib/layout/container.svelte +++ b/src/lib/layout/container.svelte @@ -1,5 +1,6 @@ -
-
- - - +{#if isStudio} + +{:else} +
+
+ + + +
-
+{/if} diff --git a/src/lib/system.ts b/src/lib/system.ts index 15f745577..8cd3a70f3 100644 --- a/src/lib/system.ts +++ b/src/lib/system.ts @@ -6,11 +6,17 @@ export const enum Mode { SELF_HOSTED = 'self-hosted' } +export const enum Profile { + CONSOLE = 'console', + STUDIO = 'studio' +} + export const VARS = { CONSOLE_MODE: (env.PUBLIC_CONSOLE_MODE as Mode) ?? undefined, APPWRITE_ENDPOINT: env.PUBLIC_APPWRITE_ENDPOINT ?? undefined, GROWTH_ENDPOINT: env.PUBLIC_GROWTH_ENDPOINT ?? undefined, - PUBLIC_STRIPE_KEY: env.PUBLIC_STRIPE_KEY ?? undefined + PUBLIC_STRIPE_KEY: env.PUBLIC_STRIPE_KEY ?? undefined, + PROJECT_PROFILE: (env.PUBLIC_PROJECT_PROFILE as Profile) ?? undefined }; export const ENV = { @@ -21,7 +27,9 @@ export const ENV = { }; export const MODE = VARS.CONSOLE_MODE === Mode.CLOUD ? Mode.CLOUD : Mode.SELF_HOSTED; +export const PROFILE = VARS.PROJECT_PROFILE === Profile.CONSOLE ? Profile.CONSOLE : Profile.STUDIO; export const isCloud = MODE === Mode.CLOUD; +export const isStudio = PROFILE === Profile.STUDIO; export const isSelfHosted = MODE !== Mode.CLOUD; export const isDev = ENV.DEV; export const isProd = ENV.PROD; diff --git a/src/routes/(console)/+layout.svelte b/src/routes/(console)/+layout.svelte index a3175a8bb..d59b404c9 100644 --- a/src/routes/(console)/+layout.svelte +++ b/src/routes/(console)/+layout.svelte @@ -3,6 +3,7 @@ import { BillingPlan, INTERVAL } from '$lib/constants'; import Footer from '$lib/layout/footer.svelte'; import Shell from '$lib/layout/shell.svelte'; + import ShellStudio from '$lib/layout/shellStudio.svelte'; import { app } from '$lib/stores/app'; import { newOrgModal, organization, type Organization } from '$lib/stores/organization'; import { database, checkForDatabaseBackupPolicies } from '$lib/stores/database'; @@ -32,7 +33,7 @@ import { openMigrationWizard } from './(migration-wizard)'; import { project } from './project-[project]/store'; import { feedback } from '$lib/stores/feedback'; - import { hasStripePublicKey, isCloud, VARS } from '$lib/system'; + import { hasStripePublicKey, isCloud, isStudio, VARS } from '$lib/system'; import { stripe } from '$lib/stores/stripe'; import MobileSupportModal from './wizard/support/mobileSupportModal.svelte'; import { showSupportModal } from './wizard/support/store'; @@ -329,6 +330,9 @@ +{#if isStudio} + +{:else}