mirror of
https://github.com/stack-auth/stack.git
synced 2026-06-04 21:04:37 +08:00
Project logo upload (#817)
<!--
Make sure you've read the CONTRIBUTING.md guidelines:
https://github.com/stack-auth/stack-auth/blob/dev/CONTRIBUTING.md
-->
<!-- ELLIPSIS_HIDDEN -->
----
> [!IMPORTANT]
> Add support for uploading and managing project logos with image
compression and validation in project settings.
>
> - **Behavior**:
> - Added support for uploading and managing project logos (`logoUrl`,
`fullLogoUrl`) in `Project` model.
> - New `LogoUpload` component in `logo-upload.tsx` for image upload
with compression and validation.
> - Projects display and store logo URLs for branding.
> - **Database**:
> - Added `logoUrl` and `fullLogoUrl` columns to `Project` table in
`migration.sql`.
> - Updated `schema.prisma` to include new fields in `Project` model.
> - **Backend**:
> - Updated `createOrUpdateProjectWithLegacyConfig()` in `projects.tsx`
to handle logo uploads.
> - Increased max image upload size to 1 MB in `images.tsx` and
`s3.tsx`.
> - Added `browser-image-compression` dependency in `package.json`.
> - **Frontend**:
> - Integrated `LogoUpload` component in `page-client.tsx` for project
settings.
> - Updated `AdminProject` type in `projects/index.ts` to include logo
URLs.
> - **Tests**:
> - Updated e2e tests in `projects.test.ts` and others to verify logo
upload functionality.
>
> <sup>This description was created by </sup>[<img alt="Ellipsis"
src="https://img.shields.io/badge/Ellipsis-blue?color=175173">](https://www.ellipsis.dev?ref=stack-auth%2Fstack-auth&utm_source=github&utm_medium=referral)<sup>
for 1b0cdbf123. You can
[customize](https://app.ellipsis.dev/stack-auth/settings/summaries) this
summary. It will automatically update as commits are pushed.</sup>
----
<!-- ELLIPSIS_HIDDEN -->
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit
* **New Features**
* Added support for uploading and managing project logos, including both
square and full (with text) logos, in the project settings page.
* Introduced a new logo upload component with image compression, size
validation, and removal functionality.
* Projects now display and store logo URLs, allowing for enhanced
branding and customization.
* **Improvements**
* Increased maximum allowed image upload size to 1 MB for project logos.
* Added clear image size constraints and unified image validation across
the app.
* **Dependencies**
* Added "browser-image-compression" library to support client-side image
compression.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
---------
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
Co-authored-by: Konsti Wohlwend <n2d4xc@gmail.com>
This commit is contained in:
parent
2c237ef572
commit
16d99963fd
@ -0,0 +1,5 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "Project" ADD COLUMN "logoUrl" TEXT;
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "Project" ADD COLUMN "fullLogoUrl" TEXT;
|
||||
@ -19,6 +19,8 @@ model Project {
|
||||
displayName String
|
||||
description String @default("")
|
||||
isProductionMode Boolean
|
||||
logoUrl String?
|
||||
fullLogoUrl String?
|
||||
|
||||
projectConfigOverride Json?
|
||||
|
||||
|
||||
@ -11,7 +11,7 @@ export async function parseBase64Image(input: string, options: {
|
||||
maxHeight?: number,
|
||||
allowTypes?: string[],
|
||||
} = {
|
||||
maxBytes: 1024 * 300,
|
||||
maxBytes: 1_000_000, // 1MB
|
||||
maxWidth: 4096,
|
||||
maxHeight: 4096,
|
||||
allowTypes: ['image/jpeg', 'image/png', 'image/webp'],
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import { uploadAndGetUrl } from "@/s3";
|
||||
import { Prisma } from "@prisma/client";
|
||||
import { KnownErrors } from "@stackframe/stack-shared";
|
||||
import { CompleteConfig, EnvironmentConfigOverrideOverride, ProjectConfigOverrideOverride } from "@stackframe/stack-shared/dist/config/schema";
|
||||
@ -48,6 +49,8 @@ export function getProjectQuery(projectId: string): RawQuery<Promise<Omit<Projec
|
||||
id: row.id,
|
||||
display_name: row.displayName,
|
||||
description: row.description,
|
||||
logo_url: row.logoUrl,
|
||||
full_logo_url: row.fullLogoUrl,
|
||||
created_at_millis: new Date(row.createdAt + "Z").getTime(),
|
||||
is_production_mode: row.isProductionMode,
|
||||
};
|
||||
@ -76,6 +79,16 @@ export async function createOrUpdateProjectWithLegacyConfig(
|
||||
data: ProjectsCrud["Admin"]["Update"],
|
||||
})
|
||||
) {
|
||||
let logoUrl: string | null | undefined;
|
||||
if (options.data.logo_url !== undefined) {
|
||||
logoUrl = await uploadAndGetUrl(options.data.logo_url, "project-logos");
|
||||
}
|
||||
|
||||
let fullLogoUrl: string | null | undefined;
|
||||
if (options.data.full_logo_url !== undefined) {
|
||||
fullLogoUrl = await uploadAndGetUrl(options.data.full_logo_url, "project-logos");
|
||||
}
|
||||
|
||||
const [projectId, branchId] = await retryTransaction(globalPrismaClient, async (tx) => {
|
||||
let project: Prisma.ProjectGetPayload<{}>;
|
||||
let branchId: string;
|
||||
@ -87,6 +100,8 @@ export async function createOrUpdateProjectWithLegacyConfig(
|
||||
displayName: options.data.display_name,
|
||||
description: options.data.description ?? "",
|
||||
isProductionMode: options.data.is_production_mode ?? false,
|
||||
logoUrl,
|
||||
fullLogoUrl,
|
||||
},
|
||||
});
|
||||
|
||||
@ -117,6 +132,8 @@ export async function createOrUpdateProjectWithLegacyConfig(
|
||||
displayName: options.data.display_name,
|
||||
description: options.data.description === null ? "" : options.data.description,
|
||||
isProductionMode: options.data.is_production_mode,
|
||||
logoUrl,
|
||||
fullLogoUrl,
|
||||
},
|
||||
});
|
||||
branchId = options.branchId;
|
||||
|
||||
@ -36,7 +36,7 @@ export function getS3PublicUrl(key: string): string {
|
||||
|
||||
async function uploadBase64Image({
|
||||
input,
|
||||
maxBytes = 1024 * 300,
|
||||
maxBytes = 1_000_000, // 1MB
|
||||
folderName,
|
||||
}: {
|
||||
input: string,
|
||||
@ -85,7 +85,7 @@ export function checkImageString(input: string) {
|
||||
|
||||
export async function uploadAndGetUrl(
|
||||
input: string | null | undefined,
|
||||
folderName: 'user-profile-images' | 'team-profile-images' | 'team-member-profile-images'
|
||||
folderName: 'user-profile-images' | 'team-profile-images' | 'team-member-profile-images' | 'project-logos'
|
||||
) {
|
||||
if (input) {
|
||||
const checkResult = checkImageString(input);
|
||||
@ -97,7 +97,6 @@ export async function uploadAndGetUrl(
|
||||
} else {
|
||||
throw new StatusError(StatusError.BadRequest, "Invalid profile image URL");
|
||||
}
|
||||
|
||||
} else if (input === null) {
|
||||
return null;
|
||||
} else {
|
||||
|
||||
@ -38,6 +38,7 @@
|
||||
"@tanstack/react-table": "^8.20.5",
|
||||
"@vercel/analytics": "^1.2.2",
|
||||
"@vercel/speed-insights": "^1.0.12",
|
||||
"browser-image-compression": "^2.0.2",
|
||||
"canvas-confetti": "^1.9.2",
|
||||
"clsx": "^2.0.0",
|
||||
"dotenv-cli": "^7.3.0",
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
"use client";
|
||||
import { InputField } from "@/components/form-fields";
|
||||
import { StyledLink } from "@/components/link";
|
||||
import { LogoUpload } from "@/components/logo-upload";
|
||||
import { FormSettingCard, SettingCard, SettingSwitch, SettingText } from "@/components/settings";
|
||||
import { getPublicEnvVar } from '@/lib/env';
|
||||
import { Accordion, AccordionContent, AccordionItem, AccordionTrigger, ActionDialog, Alert, Button, Typography } from "@stackframe/stack-ui";
|
||||
@ -63,6 +64,32 @@ export default function PageClient() {
|
||||
)}
|
||||
/>
|
||||
|
||||
<SettingCard title="Project Logo">
|
||||
<LogoUpload
|
||||
label="Logo"
|
||||
value={project.logoUrl}
|
||||
onValueChange={async (logoUrl) => {
|
||||
await project.update({ logoUrl });
|
||||
}}
|
||||
description="Upload a logo for your project. Recommended size: 200x200px"
|
||||
type="logo"
|
||||
/>
|
||||
|
||||
<LogoUpload
|
||||
label="Full Logo"
|
||||
value={project.fullLogoUrl}
|
||||
onValueChange={async (fullLogoUrl) => {
|
||||
await project.update({ fullLogoUrl });
|
||||
}}
|
||||
description="Upload a full logo with text. Recommended size: At least 100px tall, landscape format"
|
||||
type="full-logo"
|
||||
/>
|
||||
|
||||
<Typography variant="secondary" type="footnote">
|
||||
Logo images will be displayed in your application (e.g. login page) and emails. The logo should be a square image, while the full logo can include text and be wider.
|
||||
</Typography>
|
||||
</SettingCard>
|
||||
|
||||
<SettingCard
|
||||
title="API Key Settings"
|
||||
description="Configure which types of API keys are allowed in your project."
|
||||
|
||||
135
apps/dashboard/src/components/logo-upload.tsx
Normal file
135
apps/dashboard/src/components/logo-upload.tsx
Normal file
@ -0,0 +1,135 @@
|
||||
"use client";
|
||||
|
||||
import { fileToBase64 } from '@stackframe/stack-shared/dist/utils/base64';
|
||||
import { runAsynchronouslyWithAlert } from '@stackframe/stack-shared/dist/utils/promises';
|
||||
import { Button, cn, Typography } from '@stackframe/stack-ui';
|
||||
import imageCompression from 'browser-image-compression';
|
||||
import { Upload, X } from 'lucide-react';
|
||||
import { useState } from 'react';
|
||||
|
||||
export async function checkImageUrl(url: string) {
|
||||
try {
|
||||
const res = await fetch(url, { method: 'HEAD' });
|
||||
const buff = await res.blob();
|
||||
return buff.type.startsWith('image/');
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export function LogoUpload(props: {
|
||||
label: string,
|
||||
value?: string | null,
|
||||
onValueChange: (value: string | null) => void | Promise<void>,
|
||||
description?: string,
|
||||
acceptedTypes?: string[],
|
||||
type: 'logo' | 'full-logo',
|
||||
}) {
|
||||
const [uploading, setUploading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
function upload() {
|
||||
const input = document.createElement('input');
|
||||
input.type = 'file';
|
||||
input.accept = props.acceptedTypes?.join(',') || 'image/*';
|
||||
|
||||
input.onchange = (e) => {
|
||||
const file = (e.target as HTMLInputElement).files?.[0];
|
||||
if (!file) return;
|
||||
|
||||
setUploading(true);
|
||||
setError(null);
|
||||
|
||||
runAsynchronouslyWithAlert(async () => {
|
||||
try {
|
||||
// Compress the image first
|
||||
const compressedFile = await imageCompression(file, {
|
||||
maxSizeMB: 1,
|
||||
maxWidthOrHeight: 800,
|
||||
useWebWorker: true,
|
||||
fileType: file.type.startsWith('image/svg') ? file.type : 'image/jpeg',
|
||||
});
|
||||
|
||||
const base64Url = await fileToBase64(compressedFile);
|
||||
|
||||
if (await checkImageUrl(base64Url)) {
|
||||
await props.onValueChange(base64Url);
|
||||
setError(null);
|
||||
} else {
|
||||
setError('Invalid image format');
|
||||
}
|
||||
} catch (err) {
|
||||
setError('Failed to process image');
|
||||
console.error('Logo upload error:', err);
|
||||
} finally {
|
||||
setUploading(false);
|
||||
input.remove();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
input.click();
|
||||
}
|
||||
|
||||
async function remove() {
|
||||
setError(null);
|
||||
await props.onValueChange(null);
|
||||
}
|
||||
|
||||
const logoContainerClasses = props.type === 'full-logo'
|
||||
? "relative h-16 w-48 rounded border overflow-hidden bg-muted"
|
||||
: "relative h-16 w-16 rounded border overflow-hidden bg-muted";
|
||||
|
||||
const placeholderContainerClasses = props.type === 'full-logo'
|
||||
? "h-16 w-48 rounded border-2 border-dashed border-muted-foreground/25 flex items-center justify-center bg-muted/50"
|
||||
: "h-16 w-16 rounded border-2 border-dashed border-muted-foreground/25 flex items-center justify-center bg-muted/50";
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-2">
|
||||
<Typography variant="secondary" type="label">
|
||||
{props.label}
|
||||
</Typography>
|
||||
|
||||
<div className="flex items-center gap-3">
|
||||
{props.value ? (
|
||||
<div className="flex items-center gap-3">
|
||||
<div className={logoContainerClasses}>
|
||||
{/* eslint-disable-next-line @next/next/no-img-element */}
|
||||
<img
|
||||
src={props.value}
|
||||
alt={props.label}
|
||||
className="h-full w-full object-contain"
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={remove}
|
||||
disabled={uploading}
|
||||
>
|
||||
<X className="h-4 w-4 mr-2" />
|
||||
Remove
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex flex-col items-center gap-4">
|
||||
<div className={cn(placeholderContainerClasses, "cursor-pointer", uploading && "opacity-50 pointer-events-none")} onClick={uploading ? undefined : upload}>
|
||||
<Upload className="h-6 w-6 text-muted-foreground" />
|
||||
</div>
|
||||
{props.description && (
|
||||
<Typography variant="secondary" type="footnote" className="mt-2">
|
||||
{props.description}
|
||||
</Typography>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<Typography variant="destructive" type="footnote">
|
||||
{error}
|
||||
</Typography>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -80,8 +80,10 @@ it("should be able to provision a new project if client details are correct", as
|
||||
"created_at_millis": <stripped field 'created_at_millis'>,
|
||||
"description": "Project created by an external integration",
|
||||
"display_name": "Test project",
|
||||
"full_logo_url": null,
|
||||
"id": "<stripped UUID>",
|
||||
"is_production_mode": false,
|
||||
"logo_url": null,
|
||||
},
|
||||
"headers": Headers { <some fields may have been hidden> },
|
||||
}
|
||||
|
||||
@ -126,8 +126,10 @@ it("lists oauth providers", async ({ expect }) => {
|
||||
"created_at_millis": <stripped field 'created_at_millis'>,
|
||||
"description": "",
|
||||
"display_name": "New Project",
|
||||
"full_logo_url": null,
|
||||
"id": "<stripped UUID>",
|
||||
"is_production_mode": false,
|
||||
"logo_url": null,
|
||||
},
|
||||
"headers": Headers { <some fields may have been hidden> },
|
||||
}
|
||||
|
||||
@ -39,8 +39,10 @@ it("get project details", async ({ expect }) => {
|
||||
"created_at_millis": <stripped field 'created_at_millis'>,
|
||||
"description": "",
|
||||
"display_name": "New Project",
|
||||
"full_logo_url": null,
|
||||
"id": "<stripped UUID>",
|
||||
"is_production_mode": false,
|
||||
"logo_url": null,
|
||||
},
|
||||
"headers": Headers { <some fields may have been hidden> },
|
||||
}
|
||||
@ -91,8 +93,10 @@ it("creates and updates the basic project information of a project", async ({ ex
|
||||
"created_at_millis": <stripped field 'created_at_millis'>,
|
||||
"description": "Updated description",
|
||||
"display_name": "Updated Project",
|
||||
"full_logo_url": null,
|
||||
"id": "<stripped UUID>",
|
||||
"is_production_mode": true,
|
||||
"logo_url": null,
|
||||
},
|
||||
"headers": Headers { <some fields may have been hidden> },
|
||||
}
|
||||
@ -159,8 +163,10 @@ it("creates and updates the email config of a project", async ({ expect }) => {
|
||||
"created_at_millis": <stripped field 'created_at_millis'>,
|
||||
"description": "",
|
||||
"display_name": "New Project",
|
||||
"full_logo_url": null,
|
||||
"id": "<stripped UUID>",
|
||||
"is_production_mode": false,
|
||||
"logo_url": null,
|
||||
},
|
||||
"headers": Headers { <some fields may have been hidden> },
|
||||
}
|
||||
|
||||
@ -80,8 +80,10 @@ it("should be able to provision a new project if neon client details are correct
|
||||
"created_at_millis": <stripped field 'created_at_millis'>,
|
||||
"description": "Created with Neon",
|
||||
"display_name": "Test project",
|
||||
"full_logo_url": null,
|
||||
"id": "<stripped UUID>",
|
||||
"is_production_mode": false,
|
||||
"logo_url": null,
|
||||
},
|
||||
"headers": Headers { <some fields may have been hidden> },
|
||||
}
|
||||
|
||||
@ -92,8 +92,10 @@ it("creates a new project", async ({ expect }) => {
|
||||
"created_at_millis": <stripped field 'created_at_millis'>,
|
||||
"description": "",
|
||||
"display_name": "Test Project",
|
||||
"full_logo_url": null,
|
||||
"id": "<stripped UUID>",
|
||||
"is_production_mode": false,
|
||||
"logo_url": null,
|
||||
},
|
||||
"headers": Headers { <some fields may have been hidden> },
|
||||
}
|
||||
@ -142,8 +144,10 @@ it("creates a new project with different configurations", async ({ expect }) =>
|
||||
"created_at_millis": <stripped field 'created_at_millis'>,
|
||||
"description": "Test description",
|
||||
"display_name": "Test Project",
|
||||
"full_logo_url": null,
|
||||
"id": "<stripped UUID>",
|
||||
"is_production_mode": true,
|
||||
"logo_url": null,
|
||||
},
|
||||
"headers": Headers { <some fields may have been hidden> },
|
||||
}
|
||||
@ -195,8 +199,10 @@ it("creates a new project with different configurations", async ({ expect }) =>
|
||||
"created_at_millis": <stripped field 'created_at_millis'>,
|
||||
"description": "",
|
||||
"display_name": "Test Project",
|
||||
"full_logo_url": null,
|
||||
"id": "<stripped UUID>",
|
||||
"is_production_mode": false,
|
||||
"logo_url": null,
|
||||
},
|
||||
"headers": Headers { <some fields may have been hidden> },
|
||||
}
|
||||
@ -239,8 +245,10 @@ it("creates a new project with different configurations", async ({ expect }) =>
|
||||
"created_at_millis": <stripped field 'created_at_millis'>,
|
||||
"description": "",
|
||||
"display_name": "Test Project",
|
||||
"full_logo_url": null,
|
||||
"id": "<stripped UUID>",
|
||||
"is_production_mode": false,
|
||||
"logo_url": null,
|
||||
},
|
||||
"headers": Headers { <some fields may have been hidden> },
|
||||
}
|
||||
@ -297,8 +305,10 @@ it("creates a new project with different configurations", async ({ expect }) =>
|
||||
"created_at_millis": <stripped field 'created_at_millis'>,
|
||||
"description": "",
|
||||
"display_name": "Test Project",
|
||||
"full_logo_url": null,
|
||||
"id": "<stripped UUID>",
|
||||
"is_production_mode": false,
|
||||
"logo_url": null,
|
||||
},
|
||||
"headers": Headers { <some fields may have been hidden> },
|
||||
}
|
||||
@ -357,8 +367,10 @@ it("creates a new project with different configurations", async ({ expect }) =>
|
||||
"created_at_millis": <stripped field 'created_at_millis'>,
|
||||
"description": "",
|
||||
"display_name": "Test Project",
|
||||
"full_logo_url": null,
|
||||
"id": "<stripped UUID>",
|
||||
"is_production_mode": false,
|
||||
"logo_url": null,
|
||||
},
|
||||
"headers": Headers { <some fields may have been hidden> },
|
||||
}
|
||||
@ -400,8 +412,10 @@ it("lists the current projects after creating a new project", async ({ expect })
|
||||
"created_at_millis": <stripped field 'created_at_millis'>,
|
||||
"description": "",
|
||||
"display_name": "New Project",
|
||||
"full_logo_url": null,
|
||||
"id": "<stripped UUID>",
|
||||
"is_production_mode": false,
|
||||
"logo_url": null,
|
||||
},
|
||||
],
|
||||
},
|
||||
@ -449,8 +463,10 @@ it("verifies email_theme update persists", async ({ expect }) => {
|
||||
"created_at_millis": <stripped field 'created_at_millis'>,
|
||||
"description": "",
|
||||
"display_name": "New Project",
|
||||
"full_logo_url": null,
|
||||
"id": "<stripped UUID>",
|
||||
"is_production_mode": false,
|
||||
"logo_url": null,
|
||||
},
|
||||
"headers": Headers { <some fields may have been hidden> },
|
||||
}
|
||||
@ -487,8 +503,10 @@ it("verifies email_theme update persists", async ({ expect }) => {
|
||||
"created_at_millis": <stripped field 'created_at_millis'>,
|
||||
"description": "",
|
||||
"display_name": "New Project",
|
||||
"full_logo_url": null,
|
||||
"id": "<stripped UUID>",
|
||||
"is_production_mode": false,
|
||||
"logo_url": null,
|
||||
},
|
||||
"headers": Headers { <some fields may have been hidden> },
|
||||
}
|
||||
@ -535,8 +553,10 @@ it("updates trusted domains without modifying allow_localhost", async ({ expect
|
||||
"created_at_millis": <stripped field 'created_at_millis'>,
|
||||
"description": "",
|
||||
"display_name": "New Project",
|
||||
"full_logo_url": null,
|
||||
"id": "<stripped UUID>",
|
||||
"is_production_mode": false,
|
||||
"logo_url": null,
|
||||
},
|
||||
"headers": Headers { <some fields may have been hidden> },
|
||||
}
|
||||
@ -586,8 +606,10 @@ it("updates trusted domains without modifying allow_localhost", async ({ expect
|
||||
"created_at_millis": <stripped field 'created_at_millis'>,
|
||||
"description": "",
|
||||
"display_name": "New Project",
|
||||
"full_logo_url": null,
|
||||
"id": "<stripped UUID>",
|
||||
"is_production_mode": false,
|
||||
"logo_url": null,
|
||||
},
|
||||
"headers": Headers { <some fields may have been hidden> },
|
||||
}
|
||||
|
||||
@ -237,8 +237,10 @@ it("can customize default user permissions", async ({ expect }) => {
|
||||
"created_at_millis": <stripped field 'created_at_millis'>,
|
||||
"description": "",
|
||||
"display_name": "New Project",
|
||||
"full_logo_url": null,
|
||||
"id": "<stripped UUID>",
|
||||
"is_production_mode": false,
|
||||
"logo_url": null,
|
||||
},
|
||||
"headers": Headers { <some fields may have been hidden> },
|
||||
}
|
||||
|
||||
@ -97,8 +97,10 @@ it("creates and updates the basic project information of a project", async ({ ex
|
||||
"created_at_millis": <stripped field 'created_at_millis'>,
|
||||
"description": "Updated description",
|
||||
"display_name": "Updated Project",
|
||||
"full_logo_url": null,
|
||||
"id": "<stripped UUID>",
|
||||
"is_production_mode": true,
|
||||
"logo_url": null,
|
||||
},
|
||||
"headers": Headers { <some fields may have been hidden> },
|
||||
}
|
||||
@ -144,8 +146,10 @@ it("updates the basic project configuration", async ({ expect }) => {
|
||||
"created_at_millis": <stripped field 'created_at_millis'>,
|
||||
"description": "",
|
||||
"display_name": "New Project",
|
||||
"full_logo_url": null,
|
||||
"id": "<stripped UUID>",
|
||||
"is_production_mode": false,
|
||||
"logo_url": null,
|
||||
},
|
||||
"headers": Headers { <some fields may have been hidden> },
|
||||
}
|
||||
@ -196,8 +200,10 @@ it("updates the project domains configuration", async ({ expect }) => {
|
||||
"created_at_millis": <stripped field 'created_at_millis'>,
|
||||
"description": "",
|
||||
"display_name": "New Project",
|
||||
"full_logo_url": null,
|
||||
"id": "<stripped UUID>",
|
||||
"is_production_mode": false,
|
||||
"logo_url": null,
|
||||
},
|
||||
"headers": Headers { <some fields may have been hidden> },
|
||||
}
|
||||
@ -254,8 +260,10 @@ it("updates the project domains configuration", async ({ expect }) => {
|
||||
"created_at_millis": <stripped field 'created_at_millis'>,
|
||||
"description": "",
|
||||
"display_name": "New Project",
|
||||
"full_logo_url": null,
|
||||
"id": "<stripped UUID>",
|
||||
"is_production_mode": false,
|
||||
"logo_url": null,
|
||||
},
|
||||
"headers": Headers { <some fields may have been hidden> },
|
||||
}
|
||||
@ -306,8 +314,10 @@ it("should allow insecure HTTP connections if insecureHttp is true", async ({ ex
|
||||
"created_at_millis": <stripped field 'created_at_millis'>,
|
||||
"description": "",
|
||||
"display_name": "New Project",
|
||||
"full_logo_url": null,
|
||||
"id": "<stripped UUID>",
|
||||
"is_production_mode": false,
|
||||
"logo_url": null,
|
||||
},
|
||||
"headers": Headers { <some fields may have been hidden> },
|
||||
}
|
||||
@ -401,8 +411,10 @@ it("updates the project email configuration", async ({ expect }) => {
|
||||
"created_at_millis": <stripped field 'created_at_millis'>,
|
||||
"description": "",
|
||||
"display_name": "New Project",
|
||||
"full_logo_url": null,
|
||||
"id": "<stripped UUID>",
|
||||
"is_production_mode": false,
|
||||
"logo_url": null,
|
||||
},
|
||||
"headers": Headers { <some fields may have been hidden> },
|
||||
}
|
||||
@ -458,8 +470,10 @@ it("updates the project email configuration", async ({ expect }) => {
|
||||
"created_at_millis": <stripped field 'created_at_millis'>,
|
||||
"description": "",
|
||||
"display_name": "New Project",
|
||||
"full_logo_url": null,
|
||||
"id": "<stripped UUID>",
|
||||
"is_production_mode": false,
|
||||
"logo_url": null,
|
||||
},
|
||||
"headers": Headers { <some fields may have been hidden> },
|
||||
}
|
||||
@ -501,8 +515,10 @@ it("updates the project email configuration", async ({ expect }) => {
|
||||
"created_at_millis": <stripped field 'created_at_millis'>,
|
||||
"description": "",
|
||||
"display_name": "New Project",
|
||||
"full_logo_url": null,
|
||||
"id": "<stripped UUID>",
|
||||
"is_production_mode": false,
|
||||
"logo_url": null,
|
||||
},
|
||||
"headers": Headers { <some fields may have been hidden> },
|
||||
}
|
||||
@ -544,8 +560,10 @@ it("updates the project email configuration", async ({ expect }) => {
|
||||
"created_at_millis": <stripped field 'created_at_millis'>,
|
||||
"description": "",
|
||||
"display_name": "New Project",
|
||||
"full_logo_url": null,
|
||||
"id": "<stripped UUID>",
|
||||
"is_production_mode": false,
|
||||
"logo_url": null,
|
||||
},
|
||||
"headers": Headers { <some fields may have been hidden> },
|
||||
}
|
||||
@ -601,8 +619,10 @@ it("updates the project email configuration", async ({ expect }) => {
|
||||
"created_at_millis": <stripped field 'created_at_millis'>,
|
||||
"description": "",
|
||||
"display_name": "New Project",
|
||||
"full_logo_url": null,
|
||||
"id": "<stripped UUID>",
|
||||
"is_production_mode": false,
|
||||
"logo_url": null,
|
||||
},
|
||||
"headers": Headers { <some fields may have been hidden> },
|
||||
}
|
||||
@ -771,8 +791,10 @@ it("updates the project oauth configuration", async ({ expect }) => {
|
||||
"created_at_millis": <stripped field 'created_at_millis'>,
|
||||
"description": "",
|
||||
"display_name": "New Project",
|
||||
"full_logo_url": null,
|
||||
"id": "<stripped UUID>",
|
||||
"is_production_mode": false,
|
||||
"logo_url": null,
|
||||
},
|
||||
"headers": Headers { <some fields may have been hidden> },
|
||||
}
|
||||
@ -821,8 +843,10 @@ it("updates the project oauth configuration", async ({ expect }) => {
|
||||
"created_at_millis": <stripped field 'created_at_millis'>,
|
||||
"description": "",
|
||||
"display_name": "New Project",
|
||||
"full_logo_url": null,
|
||||
"id": "<stripped UUID>",
|
||||
"is_production_mode": false,
|
||||
"logo_url": null,
|
||||
},
|
||||
"headers": Headers { <some fields may have been hidden> },
|
||||
}
|
||||
@ -875,8 +899,10 @@ it("updates the project oauth configuration", async ({ expect }) => {
|
||||
"created_at_millis": <stripped field 'created_at_millis'>,
|
||||
"description": "",
|
||||
"display_name": "New Project",
|
||||
"full_logo_url": null,
|
||||
"id": "<stripped UUID>",
|
||||
"is_production_mode": false,
|
||||
"logo_url": null,
|
||||
},
|
||||
"headers": Headers { <some fields may have been hidden> },
|
||||
}
|
||||
@ -924,8 +950,10 @@ it("updates the project oauth configuration", async ({ expect }) => {
|
||||
"created_at_millis": <stripped field 'created_at_millis'>,
|
||||
"description": "",
|
||||
"display_name": "New Project",
|
||||
"full_logo_url": null,
|
||||
"id": "<stripped UUID>",
|
||||
"is_production_mode": false,
|
||||
"logo_url": null,
|
||||
},
|
||||
"headers": Headers { <some fields may have been hidden> },
|
||||
}
|
||||
@ -988,8 +1016,10 @@ it("updates the project oauth configuration", async ({ expect }) => {
|
||||
"created_at_millis": <stripped field 'created_at_millis'>,
|
||||
"description": "",
|
||||
"display_name": "New Project",
|
||||
"full_logo_url": null,
|
||||
"id": "<stripped UUID>",
|
||||
"is_production_mode": false,
|
||||
"logo_url": null,
|
||||
},
|
||||
"headers": Headers { <some fields may have been hidden> },
|
||||
}
|
||||
@ -1052,8 +1082,10 @@ it("updates the project oauth configuration", async ({ expect }) => {
|
||||
"created_at_millis": <stripped field 'created_at_millis'>,
|
||||
"description": "",
|
||||
"display_name": "New Project",
|
||||
"full_logo_url": null,
|
||||
"id": "<stripped UUID>",
|
||||
"is_production_mode": false,
|
||||
"logo_url": null,
|
||||
},
|
||||
"headers": Headers { <some fields may have been hidden> },
|
||||
}
|
||||
|
||||
@ -255,8 +255,10 @@ it("can customize default team permissions", async ({ expect }) => {
|
||||
"created_at_millis": <stripped field 'created_at_millis'>,
|
||||
"description": "",
|
||||
"display_name": "New Project",
|
||||
"full_logo_url": null,
|
||||
"id": "<stripped UUID>",
|
||||
"is_production_mode": false,
|
||||
"logo_url": null,
|
||||
},
|
||||
"headers": Headers { <some fields may have been hidden> },
|
||||
}
|
||||
|
||||
@ -65,6 +65,8 @@ export const projectsCrudAdminReadSchema = yupObject({
|
||||
id: schemaFields.projectIdSchema.defined(),
|
||||
display_name: schemaFields.projectDisplayNameSchema.defined(),
|
||||
description: schemaFields.projectDescriptionSchema.nonNullable().defined(),
|
||||
logo_url: schemaFields.projectLogoUrlSchema.nullable().optional(),
|
||||
full_logo_url: schemaFields.projectFullLogoUrlSchema.nullable().optional(),
|
||||
created_at_millis: schemaFields.projectCreatedAtMillisSchema.defined(),
|
||||
is_production_mode: schemaFields.projectIsProductionModeSchema.defined(),
|
||||
/** @deprecated */
|
||||
@ -112,6 +114,8 @@ export const projectsCrudClientReadSchema = yupObject({
|
||||
export const projectsCrudAdminUpdateSchema = yupObject({
|
||||
display_name: schemaFields.projectDisplayNameSchema.optional(),
|
||||
description: schemaFields.projectDescriptionSchema.optional(),
|
||||
logo_url: schemaFields.projectLogoUrlSchema.nullable().optional(),
|
||||
full_logo_url: schemaFields.projectFullLogoUrlSchema.nullable().optional(),
|
||||
is_production_mode: schemaFields.projectIsProductionModeSchema.optional(),
|
||||
config: yupObject({
|
||||
sign_up_enabled: schemaFields.projectSignUpEnabledSchema.optional(),
|
||||
|
||||
@ -11,6 +11,8 @@ import { deindent } from "./utils/strings";
|
||||
import { isValidUrl } from "./utils/urls";
|
||||
import { isUuid } from "./utils/uuids";
|
||||
|
||||
const MAX_IMAGE_SIZE_BASE64_BYTES = 1_000_000; // 1MB
|
||||
|
||||
declare module "yup" {
|
||||
// eslint-disable-next-line @typescript-eslint/consistent-type-definitions
|
||||
interface StringSchema<TType, TContext, TDefault, TFlags> {
|
||||
@ -433,6 +435,8 @@ export const adminAuthTypeSchema = yupString().oneOf(['admin']).defined();
|
||||
export const projectIdSchema = yupString().test((v) => v === undefined || v === "internal" || isUuid(v)).meta({ openapiField: { description: _idDescription('project'), exampleValue: 'e0b52f4d-dece-408c-af49-d23061bb0f8d' } });
|
||||
export const projectBranchIdSchema = yupString().nonEmpty().max(255).meta({ openapiField: { description: _idDescription('project branch'), exampleValue: 'main' } });
|
||||
export const projectDisplayNameSchema = yupString().meta({ openapiField: { description: _displayNameDescription('project'), exampleValue: 'MyMusic' } });
|
||||
export const projectLogoUrlSchema = urlSchema.max(MAX_IMAGE_SIZE_BASE64_BYTES).meta({ openapiField: { description: 'URL of the logo for the project. This is usually a close to 1:1 image of the company logo.', exampleValue: 'https://example.com/logo.png' } });
|
||||
export const projectFullLogoUrlSchema = urlSchema.max(MAX_IMAGE_SIZE_BASE64_BYTES).meta({ openapiField: { description: 'URL of the full logo for the project. This is usually a vertical image with the logo and the company name.', exampleValue: 'https://example.com/full-logo.png' } });
|
||||
export const projectDescriptionSchema = yupString().nullable().meta({ openapiField: { description: 'A human readable description of the project', exampleValue: 'A music streaming service' } });
|
||||
export const projectCreatedAtMillisSchema = yupNumber().meta({ openapiField: { description: _createdAtMillisDescription('project'), exampleValue: 1630000000000 } });
|
||||
export const projectIsProductionModeSchema = yupBoolean().meta({ openapiField: { description: 'Whether the project is in production mode', exampleValue: true } });
|
||||
@ -567,7 +571,7 @@ export const primaryEmailAuthEnabledSchema = yupBoolean().meta({ openapiField: {
|
||||
export const primaryEmailVerifiedSchema = yupBoolean().meta({ openapiField: { description: 'Whether the primary email has been verified to belong to this user', exampleValue: true } });
|
||||
export const userDisplayNameSchema = yupString().nullable().meta({ openapiField: { description: _displayNameDescription('user'), exampleValue: 'John Doe' } });
|
||||
export const selectedTeamIdSchema = yupString().uuid().meta({ openapiField: { description: 'ID of the team currently selected by the user', exampleValue: 'team-id' } });
|
||||
export const profileImageUrlSchema = urlSchema.max(1000000).meta({ openapiField: { description: _profileImageUrlDescription('user'), exampleValue: 'https://example.com/image.jpg' } });
|
||||
export const profileImageUrlSchema = urlSchema.max(MAX_IMAGE_SIZE_BASE64_BYTES).meta({ openapiField: { description: _profileImageUrlDescription('user'), exampleValue: 'https://example.com/image.jpg' } });
|
||||
export const signedUpAtMillisSchema = yupNumber().meta({ openapiField: { description: _signedUpAtMillisDescription, exampleValue: 1630000000000 } });
|
||||
export const userClientMetadataSchema = jsonSchema.meta({ openapiField: { description: _clientMetaDataDescription('user'), exampleValue: { key: 'value' } } });
|
||||
export const userClientReadOnlyMetadataSchema = jsonSchema.meta({ openapiField: { description: _clientReadOnlyMetaDataDescription('user'), exampleValue: { key: 'value' } } });
|
||||
|
||||
@ -113,6 +113,8 @@ export class _StackAdminAppImplIncomplete<HasTokenStore extends boolean, Project
|
||||
description: data.description,
|
||||
createdAt: new Date(data.created_at_millis),
|
||||
isProductionMode: data.is_production_mode,
|
||||
logoUrl: data.logo_url,
|
||||
fullLogoUrl: data.full_logo_url,
|
||||
config: {
|
||||
signUpEnabled: data.config.sign_up_enabled,
|
||||
credentialEnabled: data.config.credential_enabled,
|
||||
|
||||
@ -18,6 +18,9 @@ export type AdminProject = {
|
||||
readonly description: string | null,
|
||||
readonly createdAt: Date,
|
||||
readonly isProductionMode: boolean,
|
||||
readonly logoUrl: string | null | undefined,
|
||||
readonly fullLogoUrl: string | null | undefined,
|
||||
|
||||
readonly config: AdminProjectConfig,
|
||||
|
||||
update(this: AdminProject, update: AdminProjectUpdateOptions): Promise<void>,
|
||||
@ -41,6 +44,8 @@ export type AdminProjectUpdateOptions = {
|
||||
displayName?: string,
|
||||
description?: string,
|
||||
isProductionMode?: boolean,
|
||||
logoUrl?: string | null,
|
||||
fullLogoUrl?: string | null,
|
||||
config?: AdminProjectConfigUpdateOptions,
|
||||
};
|
||||
export function adminProjectUpdateOptionsToCrud(options: AdminProjectUpdateOptions): ProjectsCrud["Admin"]["Update"] {
|
||||
@ -48,6 +53,8 @@ export function adminProjectUpdateOptionsToCrud(options: AdminProjectUpdateOptio
|
||||
display_name: options.displayName,
|
||||
description: options.description,
|
||||
is_production_mode: options.isProductionMode,
|
||||
logo_url: options.logoUrl,
|
||||
full_logo_url: options.fullLogoUrl,
|
||||
config: {
|
||||
domains: options.config?.domains?.map((d) => ({
|
||||
domain: d.domain,
|
||||
|
||||
@ -376,6 +376,9 @@ importers:
|
||||
'@vercel/speed-insights':
|
||||
specifier: ^1.0.12
|
||||
version: 1.0.12(next@15.4.1(@babel/core@7.26.0)(@opentelemetry/api@1.9.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react@19.0.0)
|
||||
browser-image-compression:
|
||||
specifier: ^2.0.2
|
||||
version: 2.0.2
|
||||
canvas-confetti:
|
||||
specifier: ^1.9.2
|
||||
version: 1.9.3
|
||||
|
||||
Loading…
Reference in New Issue
Block a user