fix: Resolve bcrypt architecture and duplicate resource creation issues
Backend fixes: - Fix bcrypt exec format error for ARM64 architecture - Add architecture detection for MinIO client download - Install build tools and rebuild bcrypt from source - Add postinstall script to automatically rebuild bcrypt - Rebuild bcrypt after copying files to ensure correct binary Frontend fixes: - Handle duplicate resource creation in QuickStartWizard - Add error handling for "already exists" scenarios - Prevent multiple executions with isCreating flag - Allow wizard to complete even if resources already exist These changes fix: 1. Backend crash on ARM64 due to bcrypt binary mismatch 2. 500 errors when resources already exist in QuickStartWizard 3. Race conditions when clicking Complete button multiple times 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -11,7 +11,8 @@
|
|||||||
"lint": "eslint src/",
|
"lint": "eslint src/",
|
||||||
"lint:fix": "eslint src/ --fix",
|
"lint:fix": "eslint src/ --fix",
|
||||||
"hash-password": "node scripts/hash-password.js",
|
"hash-password": "node scripts/hash-password.js",
|
||||||
"generate-password": "node generate-password.js"
|
"generate-password": "node generate-password.js",
|
||||||
|
"postinstall": "npm rebuild bcrypt --build-from-source"
|
||||||
},
|
},
|
||||||
"keywords": ["minio", "api", "backend"],
|
"keywords": ["minio", "api", "backend"],
|
||||||
"author": "",
|
"author": "",
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
FROM node:20-alpine
|
FROM node:20-alpine
|
||||||
|
|
||||||
# Install MinIO client
|
# Install MinIO client (detect architecture)
|
||||||
RUN wget https://dl.min.io/client/mc/release/linux-amd64/mc && \
|
RUN ARCH=$(uname -m | sed 's/x86_64/amd64/g' | sed 's/aarch64/arm64/g') && \
|
||||||
|
wget https://dl.min.io/client/mc/release/linux-${ARCH}/mc && \
|
||||||
chmod +x mc && \
|
chmod +x mc && \
|
||||||
mv mc /usr/local/bin/
|
mv mc /usr/local/bin/
|
||||||
|
|
||||||
@@ -11,12 +12,19 @@ WORKDIR /app
|
|||||||
# Copy package files
|
# Copy package files
|
||||||
COPY package*.json ./
|
COPY package*.json ./
|
||||||
|
|
||||||
# Install production dependencies
|
# Install build tools for native dependencies (keep them)
|
||||||
|
RUN apk add --no-cache python3 make g++ \
|
||||||
|
&& rm -rf /app/node_modules
|
||||||
|
|
||||||
|
# Install production dependencies (will trigger postinstall)
|
||||||
RUN npm ci --only=production
|
RUN npm ci --only=production
|
||||||
|
|
||||||
# Copy application files
|
# Copy application files
|
||||||
COPY . .
|
COPY . .
|
||||||
|
|
||||||
|
# Rebuild bcrypt after copying files
|
||||||
|
RUN npm rebuild bcrypt --build-from-source
|
||||||
|
|
||||||
# Create non-root user
|
# Create non-root user
|
||||||
RUN addgroup -g 1001 -S nodejs && \
|
RUN addgroup -g 1001 -S nodejs && \
|
||||||
adduser -S nodejs -u 1001
|
adduser -S nodejs -u 1001
|
||||||
|
|||||||
@@ -73,6 +73,7 @@ const QuickStartWizard: React.FC<QuickStartWizardProps> = ({
|
|||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [error, setError] = useState('');
|
const [error, setError] = useState('');
|
||||||
const [completedSetup, setCompletedSetup] = useState<any>(null);
|
const [completedSetup, setCompletedSetup] = useState<any>(null);
|
||||||
|
const [isCreating, setIsCreating] = useState(false);
|
||||||
|
|
||||||
const steps = [
|
const steps = [
|
||||||
t('quickWizard:steps.createBucket'),
|
t('quickWizard:steps.createBucket'),
|
||||||
@@ -125,57 +126,102 @@ const QuickStartWizard: React.FC<QuickStartWizardProps> = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleComplete = async () => {
|
const handleComplete = async () => {
|
||||||
|
// Prevent multiple executions
|
||||||
|
if (isCreating) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsCreating(true);
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
setError('');
|
setError('');
|
||||||
|
|
||||||
|
const results = {
|
||||||
|
bucket: setupData.bucketName,
|
||||||
|
user: setupData.userName,
|
||||||
|
password: setupData.userPassword,
|
||||||
|
policy: `${setupData.bucketName}-${setupData.policyType}-policy`,
|
||||||
|
};
|
||||||
|
|
||||||
|
let bucketCreated = false;
|
||||||
|
let userCreated = false;
|
||||||
|
let policyCreated = false;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Step 1: Create bucket
|
// Step 1: Create bucket (skip if already exists)
|
||||||
await api.post('/buckets', { bucketName: setupData.bucketName }, {
|
try {
|
||||||
timeout: 30000, // 30 seconds timeout
|
await api.post('/buckets', { bucketName: setupData.bucketName }, {
|
||||||
});
|
timeout: 30000, // 30 seconds timeout
|
||||||
|
});
|
||||||
|
bucketCreated = true;
|
||||||
|
} catch (err: any) {
|
||||||
|
if (err.response?.data?.error?.includes('already own it') ||
|
||||||
|
err.response?.data?.error?.includes('BucketAlreadyOwnedByYou')) {
|
||||||
|
// Bucket already exists, that's ok
|
||||||
|
bucketCreated = true;
|
||||||
|
} else {
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Step 2: Create user
|
// Step 2: Create user (skip if already exists)
|
||||||
await userService.createUser({
|
try {
|
||||||
accessKey: setupData.userName,
|
await userService.createUser({
|
||||||
secretKey: setupData.userPassword,
|
accessKey: setupData.userName,
|
||||||
});
|
secretKey: setupData.userPassword,
|
||||||
|
});
|
||||||
|
userCreated = true;
|
||||||
|
} catch (err: any) {
|
||||||
|
if (err.response?.data?.error?.includes('already exists') ||
|
||||||
|
err.response?.data?.error?.includes('UserAlreadyExists')) {
|
||||||
|
// User already exists, that's ok
|
||||||
|
userCreated = true;
|
||||||
|
} else {
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Step 3: Create policy
|
// Step 3: Create policy (skip if already exists)
|
||||||
const policyName = `${setupData.bucketName}-${setupData.policyType}-policy`;
|
const policyName = results.policy;
|
||||||
|
|
||||||
// Map policy types to template keys
|
try {
|
||||||
const templateKey = setupData.policyType === 'readwrite'
|
// Map policy types to template keys
|
||||||
? 'bucketFullAccess'
|
const templateKey = setupData.policyType === 'readwrite'
|
||||||
: setupData.policyType === 'readonly'
|
? 'bucketFullAccess'
|
||||||
? 'bucketReadOnly'
|
: setupData.policyType === 'readonly'
|
||||||
: 'bucketWriteOnly';
|
? 'bucketReadOnly'
|
||||||
|
: 'bucketWriteOnly';
|
||||||
|
|
||||||
const policyJson = policyService.generatePolicyFromTemplate(
|
const policyJson = policyService.generatePolicyFromTemplate(
|
||||||
templateKey,
|
templateKey,
|
||||||
setupData.bucketName
|
setupData.bucketName
|
||||||
);
|
);
|
||||||
|
|
||||||
await policyService.createPolicy({
|
await policyService.createPolicy({
|
||||||
name: policyName,
|
name: policyName,
|
||||||
policy: policyJson,
|
policy: policyJson,
|
||||||
});
|
});
|
||||||
|
policyCreated = true;
|
||||||
|
} catch (err: any) {
|
||||||
|
if (err.response?.data?.error?.includes('already exists') ||
|
||||||
|
err.response?.data?.error?.includes('PolicyAlreadyExists')) {
|
||||||
|
// Policy already exists, that's ok
|
||||||
|
policyCreated = true;
|
||||||
|
} else {
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Step 4: Attach policy to user
|
// Step 4: Attach policy to user (always try this)
|
||||||
await policyService.attachPolicy({
|
await policyService.attachPolicy({
|
||||||
policyName: policyName,
|
policyName: policyName,
|
||||||
userName: setupData.userName,
|
userName: setupData.userName,
|
||||||
});
|
});
|
||||||
|
|
||||||
setCompletedSetup({
|
setCompletedSetup(results);
|
||||||
bucket: setupData.bucketName,
|
|
||||||
user: setupData.userName,
|
|
||||||
password: setupData.userPassword,
|
|
||||||
policy: policyName,
|
|
||||||
});
|
|
||||||
|
|
||||||
setActiveStep(steps.length);
|
setActiveStep(steps.length);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError(err instanceof Error ? err.message : t('quickWizard:errors.creationFailed'));
|
setError(err instanceof Error ? err.message : t('quickWizard:errors.creationFailed'));
|
||||||
|
setIsCreating(false);
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
@@ -199,6 +245,7 @@ const QuickStartWizard: React.FC<QuickStartWizardProps> = ({
|
|||||||
});
|
});
|
||||||
setError('');
|
setError('');
|
||||||
setCompletedSetup(null);
|
setCompletedSetup(null);
|
||||||
|
setIsCreating(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user