feat(demo): add DEMO_MODE environment variable and storage limits

- Introduced a new DEMO_MODE environment variable to toggle demo functionality.
- Updated FileController and ReverseShareService to limit user storage to 200MB when DEMO_MODE is enabled.
- Enhanced StorageService to reflect demo storage limits in disk space calculations.
- Added missing authentication providers in the settings form and server start script for better provider management.
This commit is contained in:
Daniel Luiz Alves
2025-07-15 13:45:46 -03:00
parent 383f26e777
commit 794a2782ac
6 changed files with 222 additions and 161 deletions

View File

@@ -14,6 +14,7 @@ const envSchema = z.object({
S3_FORCE_PATH_STYLE: z.union([z.literal("true"), z.literal("false")]).default("false"),
SECURE_SITE: z.union([z.literal("true"), z.literal("false")]).default("false"),
DATABASE_URL: z.string().optional().default("file:/app/server/prisma/palmr.db"),
DEMO_MODE: z.union([z.literal("true"), z.literal("false")]).default("false"),
});
export const env = envSchema.parse(process.env);

View File

@@ -1,5 +1,6 @@
import { FastifyReply, FastifyRequest } from "fastify";
import { env } from "../../env";
import { prisma } from "../../shared/prisma";
import { ConfigService } from "../config/service";
import { CheckFileInput, CheckFileSchema, RegisterFileInput, RegisterFileSchema, UpdateFileSchema } from "./dto";
@@ -55,7 +56,17 @@ export class FileController {
});
}
const maxTotalStorage = BigInt(await this.configService.getValue("maxTotalStoragePerUser"));
// Check if DEMO_MODE is enabled
const isDemoMode = env.DEMO_MODE === "true";
let maxTotalStorage: bigint;
if (isDemoMode) {
// In demo mode, limit all users to 200MB
maxTotalStorage = BigInt(200 * 1024 * 1024); // 200MB in bytes
} else {
// Normal behavior - use maxTotalStoragePerUser configuration
maxTotalStorage = BigInt(await this.configService.getValue("maxTotalStoragePerUser"));
}
const userFiles = await prisma.file.findMany({
where: { userId },
@@ -127,7 +138,17 @@ export class FileController {
});
}
const maxTotalStorage = BigInt(await this.configService.getValue("maxTotalStoragePerUser"));
// Check if DEMO_MODE is enabled
const isDemoMode = env.DEMO_MODE === "true";
let maxTotalStorage: bigint;
if (isDemoMode) {
// In demo mode, limit all users to 200MB
maxTotalStorage = BigInt(200 * 1024 * 1024); // 200MB in bytes
} else {
// Normal behavior - use maxTotalStoragePerUser configuration
maxTotalStorage = BigInt(await this.configService.getValue("maxTotalStoragePerUser"));
}
const userFiles = await prisma.file.findMany({
where: { userId },

View File

@@ -1,5 +1,6 @@
import { PrismaClient } from "@prisma/client";
import { env } from "../../env";
import { FileService } from "../file/service";
import {
CreateReverseShareInput,
@@ -513,7 +514,18 @@ export class ReverseShareService {
throw new Error(`File size exceeds the maximum allowed size of ${maxSizeMB}MB`);
}
const maxTotalStorage = BigInt(await configService.getValue("maxTotalStoragePerUser"));
// Check if DEMO_MODE is enabled
const isDemoMode = env.DEMO_MODE === "true";
let maxTotalStorage: bigint;
if (isDemoMode) {
// In demo mode, limit all users to 200MB
maxTotalStorage = BigInt(200 * 1024 * 1024); // 200MB in bytes
} else {
// Normal behavior - use maxTotalStoragePerUser configuration
maxTotalStorage = BigInt(await configService.getValue("maxTotalStoragePerUser"));
}
const userFiles = await prisma.file.findMany({
where: { userId: creatorId },
select: { size: true },

View File

@@ -3,14 +3,13 @@ import fs from "node:fs";
import { promisify } from "util";
import { PrismaClient } from "@prisma/client";
import { env } from "../../env";
import { IS_RUNNING_IN_CONTAINER } from "../../utils/container-detection";
import { ConfigService } from "../config/service";
const execAsync = promisify(exec);
const prisma = new PrismaClient();
// TODO: REMOVE LOGGING AFTER TESTING
export class StorageService {
private configService = new ConfigService();
@@ -26,27 +25,23 @@ export class StorageService {
private _parseSize(value: string): number {
if (!value) return 0;
// Remove any whitespace and convert to lowercase
const cleanValue = value.trim().toLowerCase();
// Extract the numeric part
const numericMatch = cleanValue.match(/^(\d+(?:\.\d+)?)/);
if (!numericMatch) return 0;
const numericValue = parseFloat(numericMatch[1]);
if (Number.isNaN(numericValue)) return 0;
// Determine the unit multiplier
if (cleanValue.includes("t")) {
return Math.round(numericValue * 1024 * 1024 * 1024 * 1024); // TB to bytes
return Math.round(numericValue * 1024 * 1024 * 1024 * 1024);
} else if (cleanValue.includes("g")) {
return Math.round(numericValue * 1024 * 1024 * 1024); // GB to bytes
return Math.round(numericValue * 1024 * 1024 * 1024);
} else if (cleanValue.includes("m")) {
return Math.round(numericValue * 1024 * 1024); // MB to bytes
return Math.round(numericValue * 1024 * 1024);
} else if (cleanValue.includes("k")) {
return Math.round(numericValue * 1024); // KB to bytes
return Math.round(numericValue * 1024);
} else {
// Assume bytes if no unit
return Math.round(numericValue);
}
}
@@ -83,10 +78,8 @@ export class StorageService {
}
}
} else {
// Handle different Linux/Unix command formats
const lines = stdout.trim().split("\n");
// Handle findmnt command output
if (command.includes("findmnt")) {
if (lines.length >= 1) {
const parts = lines[0].trim().split(/\s+/);
@@ -96,10 +89,7 @@ export class StorageService {
total = this._parseSize(sizeStr);
}
}
}
// Handle stat -f command output
else if (command.includes("stat -f")) {
// Parse stat -f output (different format)
} else if (command.includes("stat -f")) {
let blockSize = 0;
let totalBlocks = 0;
let freeBlocks = 0;
@@ -117,29 +107,19 @@ export class StorageService {
if (blockSize > 0 && totalBlocks > 0) {
total = totalBlocks * blockSize;
available = freeBlocks * blockSize;
console.log(
`📊 stat -f parsed: blockSize=${blockSize}, totalBlocks=${totalBlocks}, freeBlocks=${freeBlocks}`
);
} else {
console.warn(
`❌ stat -f parsing failed: blockSize=${blockSize}, totalBlocks=${totalBlocks}, freeBlocks=${freeBlocks}`
);
return null;
}
}
// Handle df --output format
else if (command.includes("--output=")) {
} else if (command.includes("--output=")) {
if (lines.length >= 2) {
const parts = lines[1].trim().split(/\s+/);
if (parts.length >= 2) {
const [availStr, sizeStr] = parts;
available = this._safeParseInt(availStr) * 1024; // df --output gives in KB
available = this._safeParseInt(availStr) * 1024;
total = this._safeParseInt(sizeStr) * 1024;
}
}
}
// Handle regular df command output
else {
} else {
if (lines.length >= 2) {
const parts = lines[1].trim().split(/\s+/);
if (parts.length >= 4) {
@@ -148,11 +128,9 @@ export class StorageService {
total = this._safeParseInt(size);
available = this._safeParseInt(avail);
} else if (command.includes("-h")) {
// Handle human-readable format (e.g., "1.5G", "500M")
total = this._parseSize(size);
available = this._parseSize(avail);
} else {
// Default to KB (standard df output)
total = this._safeParseInt(size) * 1024;
available = this._safeParseInt(avail) * 1024;
}
@@ -164,22 +142,16 @@ export class StorageService {
if (total > 0 && available >= 0) {
return { total, available };
} else {
console.warn(`Invalid values parsed: total=${total}, available=${available} for command: ${command}`);
return null;
}
} catch (error) {
console.warn(`Command failed: ${command}`, error);
} catch {
return null;
}
}
/**
* Gets detailed mount information for debugging
*/
private async _getMountInfo(path: string): Promise<{ filesystem: string; mountPoint: string; type: string } | null> {
try {
if (!fs.existsSync("/proc/mounts")) {
console.log("❌ /proc/mounts not found for mount info");
return null;
}
@@ -189,26 +161,11 @@ export class StorageService {
let bestMatch = null;
let bestMatchLength = 0;
console.log(`🔍 Getting mount info for path: ${path}`);
for (const line of lines) {
const parts = line.split(/\s+/);
if (parts.length >= 3) {
const [filesystem, mountPoint, type] = parts;
// Log interesting filesystems for debugging
if (
filesystem.includes("volume") ||
filesystem.includes("mapper") ||
type === "ext4" ||
type === "btrfs" ||
type === "xfs" ||
mountPoint.includes("volume") ||
mountPoint.includes("app")
) {
console.log(`📋 Mount detail: ${filesystem}${mountPoint} (${type})`);
}
if (path.startsWith(mountPoint) && mountPoint.length > bestMatchLength) {
bestMatch = { filesystem, mountPoint, type };
bestMatchLength = mountPoint.length;
@@ -216,25 +173,15 @@ export class StorageService {
}
}
if (bestMatch) {
console.log(`🎯 Selected mount info: ${bestMatch.filesystem}${bestMatch.mountPoint} (${bestMatch.type})`);
}
return bestMatch;
} catch (error) {
console.warn(`Could not get mount info for ${path}:`, error);
} catch {
return null;
}
}
/**
* Detects if a path is a bind mount or mount point by checking /proc/mounts
* Returns the actual filesystem path for bind mounts
*/
private async _detectMountPoint(path: string): Promise<string | null> {
try {
if (!fs.existsSync("/proc/mounts")) {
console.log("❌ /proc/mounts not found, cannot detect mount points");
return null;
}
@@ -244,51 +191,32 @@ export class StorageService {
let bestMatch = null;
let bestMatchLength = 0;
console.log(`🔍 Checking ${lines.length} mount points for path: ${path}`);
for (const line of lines) {
const parts = line.split(/\s+/);
if (parts.length >= 3) {
const [device, mountPoint, filesystem] = parts;
// Log useful mount information for debugging
if (mountPoint.includes("volume") || mountPoint.includes("app") || mountPoint === "/") {
console.log(`📍 Found mount: ${device}${mountPoint} (${filesystem})`);
}
if (path.startsWith(mountPoint) && mountPoint.length > bestMatchLength) {
bestMatch = mountPoint;
bestMatchLength = mountPoint.length;
console.log(`✅ Better match found: ${mountPoint} (length: ${mountPoint.length})`);
}
}
}
if (bestMatch && bestMatch !== "/") {
console.log(`🎯 Selected mount point: ${bestMatch}`);
return bestMatch;
}
console.log("❌ No specific mount point found, using root");
return null;
} catch (error) {
console.warn(`Could not detect mount point for ${path}:`, error);
} catch {
return null;
}
}
/**
* Gets filesystem information for a specific path, with bind mount detection
*/
private async _getFileSystemInfo(
path: string
): Promise<{ total: number; available: number; mountPoint?: string } | null> {
try {
const mountInfo = await this._getMountInfo(path);
if (mountInfo && mountInfo.mountPoint !== "/") {
console.log(`📁 Bind mount detected: ${path}${mountInfo.filesystem} (${mountInfo.type})`);
}
const mountPoint = await this._detectMountPoint(path);
const targetPath = mountPoint || path;
@@ -298,30 +226,21 @@ export class StorageService {
: process.platform === "darwin"
? [`df -k "${targetPath}"`, `df "${targetPath}"`]
: [
// Try different df commands for better compatibility
`df -B1 "${targetPath}"`,
`df -k "${targetPath}"`,
`df "${targetPath}"`,
// Additional commands for Synology NAS and other systems
`df -h "${targetPath}"`,
`df -T "${targetPath}"`,
// Fallback to statfs if available
`stat -f "${targetPath}"`,
// Direct filesystem commands
`findmnt -n -o AVAIL,SIZE "${targetPath}"`,
`findmnt -n -o AVAIL,SIZE,TARGET "${targetPath}"`,
// Alternative df with different formatting
`df -P "${targetPath}"`,
`df --output=avail,size "${targetPath}"`,
];
console.log(`🔍 Trying ${commandsToTry.length} commands for path: ${targetPath}`);
for (const command of commandsToTry) {
console.log(`🔧 Executing command: ${command}`);
const result = await this._tryDiskSpaceCommand(command);
if (result) {
console.log(`✅ Command successful: ${command}`);
return {
...result,
mountPoint: mountPoint || undefined,
@@ -329,17 +248,12 @@ export class StorageService {
}
}
console.warn(`❌ All commands failed for path: ${targetPath}`);
return null;
} catch (error) {
console.warn(`Error getting filesystem info for ${path}:`, error);
} catch {
return null;
}
}
/**
* Dynamically detect Synology volume paths by reading /proc/mounts
*/
private async _detectSynologyVolumes(): Promise<string[]> {
try {
if (!fs.existsSync("/proc/mounts")) {
@@ -355,71 +269,48 @@ export class StorageService {
if (parts.length >= 2) {
const [, mountPoint] = parts;
// Check if this is a Synology volume mount point
if (mountPoint.match(/^\/volume\d+$/)) {
synologyPaths.push(mountPoint);
console.log(`🔍 Found Synology volume: ${mountPoint}`);
}
}
}
return synologyPaths;
} catch (error) {
console.warn("Could not detect Synology volumes:", error);
} catch {
return [];
}
}
private async _getDiskSpaceMultiplePaths(): Promise<{ total: number; available: number } | null> {
// Base paths that work for all systems
const basePaths = IS_RUNNING_IN_CONTAINER
? ["/app/server/uploads", "/app/server/temp-uploads", "/app/server/temp-chunks", "/app/server", "/app", "/"]
: [".", "./uploads", process.cwd()];
// Dynamically detect Synology volume paths
const synologyPaths = await this._detectSynologyVolumes();
// Combine base paths with detected Synology paths
const pathsToTry = [...basePaths, ...synologyPaths];
console.log(`🔍 Attempting disk space detection for ${pathsToTry.length} paths...`);
console.log(`📋 Synology volumes detected: ${synologyPaths.length > 0 ? synologyPaths.join(", ") : "none"}`);
for (const pathToCheck of pathsToTry) {
console.log(`📁 Checking path: ${pathToCheck}`);
if (pathToCheck.includes("uploads") || pathToCheck.includes("temp-")) {
try {
if (!fs.existsSync(pathToCheck)) {
fs.mkdirSync(pathToCheck, { recursive: true });
}
} catch (err) {
console.warn(`Could not create path ${pathToCheck}:`, err);
} catch {
continue;
}
}
if (!fs.existsSync(pathToCheck)) {
console.log(`❌ Path does not exist: ${pathToCheck}`);
continue;
}
// Use the new filesystem detection method
const result = await this._getFileSystemInfo(pathToCheck);
if (result) {
if (result.mountPoint) {
console.log(`✅ Storage resolved via bind mount: ${result.mountPoint}`);
}
console.log(
`✅ Disk space detected for path ${pathToCheck}: ${(result.total / (1024 * 1024 * 1024)).toFixed(2)}GB total, ${(result.available / (1024 * 1024 * 1024)).toFixed(2)}GB available`
);
return { total: result.total, available: result.available };
} else {
console.log(`❌ No filesystem info available for path: ${pathToCheck}`);
}
}
console.error("❌ All disk space detection attempts failed");
return null;
}
@@ -433,52 +324,94 @@ export class StorageService {
uploadAllowed: boolean;
}> {
try {
const isDemoMode = env.DEMO_MODE === "true";
if (isAdmin) {
const diskInfo = await this._getDiskSpaceMultiplePaths();
if (isDemoMode) {
const demoMaxStorage = 200 * 1024 * 1024;
const demoMaxStorageGB = this._ensureNumber(demoMaxStorage / (1024 * 1024 * 1024), 0);
if (!diskInfo) {
console.error("❌ Could not determine disk space - system configuration issue");
throw new Error("Unable to determine actual disk space - system configuration issue");
const userFiles = await prisma.file.findMany({
where: { userId },
select: { size: true },
});
const totalUsedStorage = userFiles.reduce((acc, file) => acc + file.size, BigInt(0));
const usedStorageGB = this._ensureNumber(Number(totalUsedStorage) / (1024 * 1024 * 1024), 0);
const availableStorageGB = this._ensureNumber(demoMaxStorageGB - usedStorageGB, 0);
return {
diskSizeGB: Number(demoMaxStorageGB.toFixed(2)),
diskUsedGB: Number(usedStorageGB.toFixed(2)),
diskAvailableGB: Number(availableStorageGB.toFixed(2)),
uploadAllowed: availableStorageGB > 0,
};
} else {
const diskInfo = await this._getDiskSpaceMultiplePaths();
if (!diskInfo) {
throw new Error("Unable to determine actual disk space - system configuration issue");
}
const { total, available } = diskInfo;
const used = total - available;
const diskSizeGB = this._ensureNumber(total / (1024 * 1024 * 1024), 0);
const diskUsedGB = this._ensureNumber(used / (1024 * 1024 * 1024), 0);
const diskAvailableGB = this._ensureNumber(available / (1024 * 1024 * 1024), 0);
return {
diskSizeGB: Number(diskSizeGB.toFixed(2)),
diskUsedGB: Number(diskUsedGB.toFixed(2)),
diskAvailableGB: Number(diskAvailableGB.toFixed(2)),
uploadAllowed: diskAvailableGB > 0.1,
};
}
const { total, available } = diskInfo;
const used = total - available;
const diskSizeGB = this._ensureNumber(total / (1024 * 1024 * 1024), 0);
const diskUsedGB = this._ensureNumber(used / (1024 * 1024 * 1024), 0);
const diskAvailableGB = this._ensureNumber(available / (1024 * 1024 * 1024), 0);
return {
diskSizeGB: Number(diskSizeGB.toFixed(2)),
diskUsedGB: Number(diskUsedGB.toFixed(2)),
diskAvailableGB: Number(diskAvailableGB.toFixed(2)),
uploadAllowed: diskAvailableGB > 0.1, // At least 100MB free
};
} else if (userId) {
const maxTotalStorage = BigInt(await this.configService.getValue("maxTotalStoragePerUser"));
const maxStorageGB = this._ensureNumber(Number(maxTotalStorage) / (1024 * 1024 * 1024), 10);
if (isDemoMode) {
const demoMaxStorage = 200 * 1024 * 1024;
const demoMaxStorageGB = this._ensureNumber(demoMaxStorage / (1024 * 1024 * 1024), 0);
const userFiles = await prisma.file.findMany({
where: { userId },
select: { size: true },
});
const userFiles = await prisma.file.findMany({
where: { userId },
select: { size: true },
});
const totalUsedStorage = userFiles.reduce((acc, file) => acc + file.size, BigInt(0));
const totalUsedStorage = userFiles.reduce((acc, file) => acc + file.size, BigInt(0));
const usedStorageGB = this._ensureNumber(Number(totalUsedStorage) / (1024 * 1024 * 1024), 0);
const availableStorageGB = this._ensureNumber(demoMaxStorageGB - usedStorageGB, 0);
const usedStorageGB = this._ensureNumber(Number(totalUsedStorage) / (1024 * 1024 * 1024), 0);
const availableStorageGB = this._ensureNumber(maxStorageGB - usedStorageGB, 0);
return {
diskSizeGB: Number(demoMaxStorageGB.toFixed(2)),
diskUsedGB: Number(usedStorageGB.toFixed(2)),
diskAvailableGB: Number(availableStorageGB.toFixed(2)),
uploadAllowed: availableStorageGB > 0,
};
} else {
const maxTotalStorage = BigInt(await this.configService.getValue("maxTotalStoragePerUser"));
const maxStorageGB = this._ensureNumber(Number(maxTotalStorage) / (1024 * 1024 * 1024), 10);
return {
diskSizeGB: Number(maxStorageGB.toFixed(2)),
diskUsedGB: Number(usedStorageGB.toFixed(2)),
diskAvailableGB: Number(availableStorageGB.toFixed(2)),
uploadAllowed: availableStorageGB > 0,
};
const userFiles = await prisma.file.findMany({
where: { userId },
select: { size: true },
});
const totalUsedStorage = userFiles.reduce((acc, file) => acc + file.size, BigInt(0));
const usedStorageGB = this._ensureNumber(Number(totalUsedStorage) / (1024 * 1024 * 1024), 0);
const availableStorageGB = this._ensureNumber(maxStorageGB - usedStorageGB, 0);
return {
diskSizeGB: Number(maxStorageGB.toFixed(2)),
diskUsedGB: Number(usedStorageGB.toFixed(2)),
diskAvailableGB: Number(availableStorageGB.toFixed(2)),
uploadAllowed: availableStorageGB > 0,
};
}
}
throw new Error("User ID is required for non-admin users");
} catch (error) {
console.error("Error getting disk space:", error);
console.error("Error getting disk space:", error);
throw new Error(
`Failed to get disk space information: ${error instanceof Error ? error.message : String(error)}`
);

View File

@@ -104,8 +104,10 @@ export function EditProviderForm({
{ pattern: "linkedin.com", scopes: ["r_liteprofile", "r_emailaddress"] },
{ pattern: "auth0.com", scopes: ["openid", "profile", "email"] },
{ pattern: "okta.com", scopes: ["openid", "profile", "email"] },
{ pattern: "authentik", scopes: ["openid", "profile", "email"] },
{ pattern: "kinde.com", scopes: ["openid", "profile", "email"] },
{ pattern: "zitadel.com", scopes: ["openid", "profile", "email"] },
{ pattern: "pocketid", scopes: ["openid", "profile", "email"] },
];
for (const { pattern, scopes } of providerPatterns) {

View File

@@ -86,6 +86,19 @@ else
return;
}
const expectedProviders = ['google', 'discord', 'github', 'auth0', 'kinde', 'zitadel', 'authentik', 'frontegg', 'pocketid'];
const existingProviders = await prisma.authProvider.findMany({
select: { name: true }
});
const existingProviderNames = existingProviders.map(p => p.name);
const missingProviders = expectedProviders.filter(name => !existingProviderNames.includes(name));
if (missingProviders.length > 0) {
console.log('true');
return;
}
console.log('false');
} catch (error) {
console.log('true');
@@ -117,6 +130,19 @@ else
return;
}
const expectedProviders = ['google', 'discord', 'github', 'auth0', 'kinde', 'zitadel', 'authentik', 'frontegg', 'pocketid'];
const existingProviders = await prisma.authProvider.findMany({
select: { name: true }
});
const existingProviderNames = existingProviders.map(p => p.name);
const missingProviders = expectedProviders.filter(name => !existingProviderNames.includes(name));
if (missingProviders.length > 0) {
console.log('true');
return;
}
console.log('false');
} catch (error) {
console.log('true');
@@ -132,6 +158,72 @@ else
if [ "$NEEDS_SEEDING" = "true" ]; then
echo "🌱 New tables detected or missing data, running seed..."
# Check which providers are missing for better logging
MISSING_PROVIDERS=$(
if [ "$(id -u)" = "0" ]; then
su-exec $TARGET_UID:$TARGET_GID node -e "
const { PrismaClient } = require('@prisma/client');
const prisma = new PrismaClient();
async function checkMissingProviders() {
try {
const expectedProviders = ['google', 'discord', 'github', 'auth0', 'kinde', 'zitadel', 'authentik', 'frontegg', 'pocketid'];
const existingProviders = await prisma.authProvider.findMany({
select: { name: true }
});
const existingProviderNames = existingProviders.map(p => p.name);
const missingProviders = expectedProviders.filter(name => !existingProviderNames.includes(name));
if (missingProviders.length > 0) {
console.log('Missing providers: ' + missingProviders.join(', '));
} else {
console.log('No missing providers');
}
} catch (error) {
console.log('Error checking providers');
} finally {
await prisma.\$disconnect();
}
}
checkMissingProviders();
" 2>/dev/null || echo "Error checking providers"
else
node -e "
const { PrismaClient } = require('@prisma/client');
const prisma = new PrismaClient();
async function checkMissingProviders() {
try {
const expectedProviders = ['google', 'discord', 'github', 'auth0', 'kinde', 'zitadel', 'authentik', 'frontegg', 'pocketid'];
const existingProviders = await prisma.authProvider.findMany({
select: { name: true }
});
const existingProviderNames = existingProviders.map(p => p.name);
const missingProviders = expectedProviders.filter(name => !existingProviderNames.includes(name));
if (missingProviders.length > 0) {
console.log('Missing providers: ' + missingProviders.join(', '));
} else {
console.log('No missing providers');
}
} catch (error) {
console.log('Error checking providers');
} finally {
await prisma.\$disconnect();
}
}
checkMissingProviders();
" 2>/dev/null || echo "Error checking providers"
fi
)
if [ "$MISSING_PROVIDERS" != "No missing providers" ] && [ "$MISSING_PROVIDERS" != "Error checking providers" ]; then
echo "🔍 $MISSING_PROVIDERS"
fi
if [ "$(id -u)" = "0" ]; then
su-exec $TARGET_UID:$TARGET_GID node ./prisma/seed.js
else