Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,4 @@ PUBLIC_APPWRITE_MULTI_REGION=false
PUBLIC_APPWRITE_ENDPOINT=http://localhost/v1
PUBLIC_STRIPE_KEY=
PUBLIC_GROWTH_ENDPOINT=
PUBLIC_CONSOLE_EMAIL_VERIFICATION=false
PUBLIC_CONSOLE_MOCK_AI_SUGGESTIONS=true
4 changes: 0 additions & 4 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,6 @@ jobs:
"PUBLIC_CONSOLE_MODE=cloud"
"PUBLIC_CONSOLE_FEATURE_FLAGS="
"PUBLIC_APPWRITE_MULTI_REGION=true"
"PUBLIC_CONSOLE_EMAIL_VERIFICATION=true"
"PUBLIC_CONSOLE_MOCK_AI_SUGGESTIONS=false"
"PUBLIC_GROWTH_ENDPOINT=${{ secrets.PUBLIC_GROWTH_ENDPOINT }}"
"PUBLIC_STRIPE_KEY=${{ secrets.PUBLIC_STRIPE_KEY }}"
Expand Down Expand Up @@ -84,7 +83,6 @@ jobs:
"PUBLIC_CONSOLE_MODE=cloud"
"PUBLIC_CONSOLE_FEATURE_FLAGS="
"PUBLIC_APPWRITE_MULTI_REGION=true"
"PUBLIC_CONSOLE_EMAIL_VERIFICATION=false"
"PUBLIC_CONSOLE_MOCK_AI_SUGGESTIONS=false"
"PUBLIC_GROWTH_ENDPOINT=${{ secrets.PUBLIC_GROWTH_ENDPOINT }}"
"PUBLIC_STRIPE_KEY=${{ secrets.PUBLIC_STRIPE_KEY_STAGE }}"
Expand Down Expand Up @@ -124,7 +122,6 @@ jobs:
build-args: |
"PUBLIC_CONSOLE_MODE=self-hosted"
"PUBLIC_APPWRITE_MULTI_REGION=false"
"PUBLIC_CONSOLE_EMAIL_VERIFICATION=false"
"PUBLIC_CONSOLE_MOCK_AI_SUGGESTIONS=true"
"PUBLIC_CONSOLE_FEATURE_FLAGS="
"PUBLIC_GROWTH_ENDPOINT=${{ secrets.PUBLIC_GROWTH_ENDPOINT }}"
Expand Down Expand Up @@ -164,7 +161,6 @@ jobs:
build-args: |
"PUBLIC_CONSOLE_MODE=cloud"
"PUBLIC_APPWRITE_MULTI_REGION=false"
"PUBLIC_CONSOLE_EMAIL_VERIFICATION=false"
"PUBLIC_CONSOLE_MOCK_AI_SUGGESTIONS=false"
"PUBLIC_CONSOLE_FEATURE_FLAGS="
"PUBLIC_STRIPE_KEY=${{ secrets.PUBLIC_STRIPE_KEY_STAGE }}"
Expand Down
1 change: 0 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -222,7 +222,6 @@ Set via `.env` (copy `.env.example`). All prefixed with `PUBLIC_` for SvelteKit:
| `PUBLIC_STRIPE_KEY` | -- | Stripe public key (cloud only) |
| `PUBLIC_GROWTH_ENDPOINT` | -- | Analytics endpoint |
| `PUBLIC_CONSOLE_FEATURE_FLAGS` | -- | Feature flags |
| `PUBLIC_CONSOLE_EMAIL_VERIFICATION` | `false` | Require email verification |
| `PUBLIC_CONSOLE_MOCK_AI_SUGGESTIONS` | `true` | Mock AI in dev |

## Common pitfalls
Expand Down
2 changes: 0 additions & 2 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ ADD ./static /app/static
ARG PUBLIC_CONSOLE_MODE
ARG PUBLIC_CONSOLE_FEATURE_FLAGS
ARG PUBLIC_APPWRITE_MULTI_REGION
ARG PUBLIC_CONSOLE_EMAIL_VERIFICATION
ARG PUBLIC_CONSOLE_MOCK_AI_SUGGESTIONS
ARG PUBLIC_APPWRITE_ENDPOINT
ARG PUBLIC_GROWTH_ENDPOINT
Expand All @@ -31,7 +30,6 @@ ENV PUBLIC_GROWTH_ENDPOINT=$PUBLIC_GROWTH_ENDPOINT
ENV PUBLIC_CONSOLE_MODE=$PUBLIC_CONSOLE_MODE
ENV PUBLIC_CONSOLE_FEATURE_FLAGS=$PUBLIC_CONSOLE_FEATURE_FLAGS
ENV PUBLIC_APPWRITE_MULTI_REGION=$PUBLIC_APPWRITE_MULTI_REGION
ENV PUBLIC_CONSOLE_EMAIL_VERIFICATION=$PUBLIC_CONSOLE_EMAIL_VERIFICATION
ENV PUBLIC_CONSOLE_MOCK_AI_SUGGESTIONS=$PUBLIC_CONSOLE_MOCK_AI_SUGGESTIONS
ENV PUBLIC_STRIPE_KEY=$PUBLIC_STRIPE_KEY
ENV PUBLIC_CONSOLE_FINGERPRINT_KEY=$PUBLIC_CONSOLE_FINGERPRINT_KEY
Expand Down
1 change: 0 additions & 1 deletion build.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,6 @@ async function main() {
logEnv('MULTI REGION', env?.PUBLIC_APPWRITE_MULTI_REGION);
logEnv('APPWRITE ENDPOINT', env?.PUBLIC_APPWRITE_ENDPOINT, 'relative');
logEnv('GROWTH ENDPOINT', env?.PUBLIC_GROWTH_ENDPOINT);
logEnv('CONSOLE EMAIL VERIFICATION', env?.PUBLIC_CONSOLE_EMAIL_VERIFICATION);
logEnv('CONSOLE MOCK AI SUGGESTIONS', env?.PUBLIC_CONSOLE_MOCK_AI_SUGGESTIONS);
log();
logDelimiter();
Expand Down
4 changes: 2 additions & 2 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
},
"dependencies": {
"@ai-sdk/svelte": "^1.1.24",
"@appwrite.io/console": "https://pkg.vc/-/@appwrite/@appwrite.io/console@d223f36",
"@appwrite.io/console": "https://pkg.vc/-/@appwrite/@appwrite.io/console@467cd21",
"@appwrite.io/pink-icons": "0.25.0",
"@appwrite.io/pink-icons-svelte": "https://pkg.vc/-/@appwrite/@appwrite.io/pink-icons-svelte@bfe7ce3",
"@appwrite.io/pink-legacy": "^1.0.3",
Expand Down
5 changes: 4 additions & 1 deletion src/hooks.client.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import * as Sentry from '@sentry/sveltekit';
import { isCloud, isProd } from '$lib/system';
import { AppwriteException } from '@appwrite.io/console';
import { isVerifyEmailRedirectError } from '$lib/helpers/emailVerification';
import type { HandleClientError } from '@sveltejs/kit';

Sentry.init({
Expand All @@ -12,7 +13,9 @@ Sentry.init({
});

export const handleError: HandleClientError = ({ error, message, status }) => {
console.error(error);
if (!isVerifyEmailRedirectError(error)) {
console.error(error);
}

let type;
if (error instanceof AppwriteException) {
Expand Down
28 changes: 28 additions & 0 deletions src/lib/helpers/emailVerification.ts
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This whole file feels like overkill. Some methods for 3 line codes, and isVerifyEmailRedirectError feels too complex. In the end, we just need to recognize 1 exception, we dont need anything crazy here.

Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { AppwriteException } from '@appwrite.io/console';

/** True when access is blocked until the console account email is verified. */
export function isVerifyEmailRedirectError(error: unknown): boolean {
if (error instanceof AppwriteException) {
return (
error.type === 'user_email_not_verified' ||
error.type === 'console_account_verification_required' ||
(error.message?.includes('Console account verification is required') ?? false)
);
}

if (error && typeof error === 'object' && 'message' in error) {
const msg = (error as { message: unknown }).message;
if (typeof msg !== 'string') return false;
const typ =
'type' in error && typeof (error as { type: unknown }).type === 'string'
? (error as { type: string }).type
: undefined;
return (
typ === 'user_email_not_verified' ||
typ === 'console_account_verification_required' ||
msg.includes('Console account verification is required')
);
}

return false;
}
35 changes: 29 additions & 6 deletions src/lib/stores/billing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,40 +78,60 @@ export const showBudgetAlert = derived(
);

function getPlansInfoStore(): BillingPlansMap | null {
return get(plansInfo) ?? get(page).data?.plansInfo ?? null;
return get(plansInfo) ?? get(page).data?.plansInfo ?? new Map();
}

function makeBillingPlan(billingPlanOrId: string | Models.BillingPlan): Models.BillingPlan {
function makeBillingPlan(
billingPlanOrId: string | Models.BillingPlan | null | undefined
): Models.BillingPlan | null {
if (!billingPlanOrId) {
return null;
}

return typeof billingPlanOrId === 'string' ? billingIdToPlan(billingPlanOrId) : billingPlanOrId;
}

export function getRoleLabel(role: string) {
return roles.find((r) => r.value === role)?.label ?? role;
}

export function isStarterPlan(billingPlanOrId: string | Models.BillingPlan): boolean {
export function isStarterPlan(
billingPlanOrId: string | Models.BillingPlan | null | undefined
): boolean {
const billingPlan = makeBillingPlan(billingPlanOrId);
return planHasGroup(billingPlan, BillingPlanGroup.Starter);
}

export function canUpgrade(billingPlanOrId: string | Models.BillingPlan): boolean {
export function canUpgrade(
billingPlanOrId: string | Models.BillingPlan | null | undefined
): boolean {
const billingPlan = makeBillingPlan(billingPlanOrId);
if (!billingPlan?.$id) {
return false;
}

const nextTier = getNextTierBillingPlan(billingPlan.$id);

// defaults back to PRO, so adjust the check!
return billingPlan.$id !== nextTier.$id;
}

export function canDowngrade(billingPlanOrId: string | Models.BillingPlan): boolean {
export function canDowngrade(
billingPlanOrId: string | Models.BillingPlan | null | undefined
): boolean {
const billingPlan = makeBillingPlan(billingPlanOrId);
if (!billingPlan?.$id) {
return false;
}

const nextTier = getPreviousTierBillingPlan(billingPlan.$id);

// defaults back to Starter, so adjust the check!
return billingPlan.$id !== nextTier.$id;
}

export function planHasGroup(
billingPlanOrId: string | Models.BillingPlan,
billingPlanOrId: string | Models.BillingPlan | null | undefined,
group: BillingPlanGroup
): boolean {
const billingPlan = makeBillingPlan(billingPlanOrId);
Expand Down Expand Up @@ -567,6 +587,9 @@ export function checkForMarkedForDeletion(org: Models.Organization) {

export async function checkForMissingPaymentMethod() {
const starterPlan = getBasePlanFromGroup(BillingPlanGroup.Starter);
if (!starterPlan?.$id) {
return;
}

const orgs = await sdk.forConsole.organizations.list({
queries: [
Expand Down
1 change: 0 additions & 1 deletion src/lib/system.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ export const VARS = {
APPWRITE_ENDPOINT: env.PUBLIC_APPWRITE_ENDPOINT ?? undefined,
GROWTH_ENDPOINT: env.PUBLIC_GROWTH_ENDPOINT ?? undefined,
PUBLIC_STRIPE_KEY: env.PUBLIC_STRIPE_KEY ?? undefined,
EMAIL_VERIFICATION: env.PUBLIC_CONSOLE_EMAIL_VERIFICATION === 'true',
MOCK_AI_SUGGESTIONS: (env.PUBLIC_CONSOLE_MOCK_AI_SUGGESTIONS ?? 'true') === 'true'
};

Expand Down
13 changes: 11 additions & 2 deletions src/routes/(console)/+error.svelte
Original file line number Diff line number Diff line change
@@ -1,9 +1,18 @@
<script>
import { base } from '$app/paths';
<script lang="ts">
import { goto } from '$app/navigation';
import { base, resolve } from '$app/paths';
import { page } from '$app/state';
import { Button } from '$lib/elements/forms';
import { isVerifyEmailRedirectError } from '$lib/helpers/emailVerification';
import { Container } from '$lib/layout';
import { Typography } from '@appwrite.io/pink-svelte';

$effect(() => {
const verifyEmailPath = resolve('/verify-email');
if (isVerifyEmailRedirectError(page.error) && page.url.pathname !== verifyEmailPath) {
goto(verifyEmailPath, { replaceState: true });
}
});
</script>

<Container>
Expand Down
70 changes: 65 additions & 5 deletions src/routes/(console)/+layout.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,71 @@
import { sdk } from '$lib/stores/sdk';
import { isCloud } from '$lib/system';
import type { LayoutLoad } from './$types';
import type { Account } from '$lib/stores/user';
import { Dependencies } from '$lib/constants';
import { Platform, Query } from '@appwrite.io/console';
import { Platform, Query, type Models } from '@appwrite.io/console';
import { makePlansMap } from '$lib/helpers/billing';
import { plansInfo as plansInfoStore } from '$lib/stores/billing';
import { normalizeConsoleVariables } from '$lib/helpers/domains';
import { syncServerTime } from '$lib/helpers/fingerprint';
import { redirect } from '@sveltejs/kit';
import { resolve } from '$app/paths';
import { isVerifyEmailRedirectError } from '$lib/helpers/emailVerification';

export const load: LayoutLoad = async ({ depends, parent }) => {
const { organizations, plansInfo } = await parent();
export const load: LayoutLoad = async ({ depends, parent, url }) => {
const parentData = await parent();
const { organizations, plansInfo } = parentData;
const account = parentData.account as Account | undefined;

const { endpoint, project } = sdk.forConsole.client.config;
const verifyEmailUrl = resolve('/verify-email');

// While unverified, several console APIs (not only teams) may return 401; avoid failing the layout.
if (url.pathname === verifyEmailUrl && account && !account.emailVerification) {
depends(Dependencies.RUNTIMES);
depends(Dependencies.CONSOLE_VARIABLES);
depends(Dependencies.ORGANIZATION);

const [preferences, rawConsoleVariables, versionData] = await Promise.all([
sdk.forConsole.account.getPrefs().catch(() => ({}) as Models.DefaultPreferences),
sdk.forConsole.console.variables().catch(() => ({}) as Models.ConsoleVariables),
fetch(`${endpoint}/health/version`, {
headers: { 'X-Appwrite-Project': project as string }
})
.then(async (response) => {
const dateHeader = response.headers.get('Date');
const parsed = dateHeader ? new Date(dateHeader).getTime() : NaN;
if (Number.isFinite(parsed)) {
syncServerTime(Math.floor(parsed / 1000));
}
return response.json() as { version?: string };
})
.catch(() => null)
]);

const consoleVariables = normalizeConsoleVariables(rawConsoleVariables);

plansInfoStore.set(plansInfo ?? null);

return {
roles: [],
scopes: [],
preferences,
currentOrgId: undefined,
organizations,
consoleVariables,
allProjectsCount: 0,
plansInfo: plansInfo ?? null,
version: versionData?.version ?? null
};
}

depends(Dependencies.RUNTIMES);
depends(Dependencies.CONSOLE_VARIABLES);
depends(Dependencies.ORGANIZATION);

const { endpoint, project } = sdk.forConsole.client.config;
const shouldRedirectToVerifyEmail = (error: unknown) =>
isVerifyEmailRedirectError(error) && url.pathname !== verifyEmailUrl;

const plansArrayPromise =
plansInfo || !isCloud
Expand All @@ -38,7 +88,13 @@ export const load: LayoutLoad = async ({ depends, parent }) => {
return response.json() as { version?: string };
}),
sdk.forConsole.console.variables()
]);
]).catch((error) => {
if (shouldRedirectToVerifyEmail(error)) {
redirect(303, verifyEmailUrl);
}

throw error;
});

const consoleVariables = normalizeConsoleVariables(rawConsoleVariables);

Expand All @@ -65,6 +121,10 @@ export const load: LayoutLoad = async ({ depends, parent }) => {
})
).total;
} catch (e) {
if (shouldRedirectToVerifyEmail(e)) {
redirect(303, verifyEmailUrl);
}

projectsCount = 0;
}
}
Expand Down
6 changes: 1 addition & 5 deletions src/routes/(console)/verify-email/+page.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,10 @@ import type { PageLoad } from './$types';
import { Dependencies } from '$lib/constants';
import { sdk } from '$lib/stores/sdk';
import { addNotification } from '$lib/stores/notifications';
import { VARS } from '$lib/system';

export const load: PageLoad = async ({ parent, depends, url }) => {
if (!VARS.EMAIL_VERIFICATION) {
redirect(303, resolve('/'));
}

const { account } = await parent();

depends(Dependencies.ACCOUNT);

const user = url.searchParams.get('userId') ?? null;
Expand Down
Loading
Loading