Skip to content

Commit aa5f90e

Browse files
authored
Merge pull request #96 from rebase-network/dev
fix: harden asset uploads and refresh dependency baselines
2 parents 8a6c16d + e42c81d commit aa5f90e

21 files changed

Lines changed: 1142 additions & 2457 deletions

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ Rebase 当前是一个内容驱动的社区网站,加上一套内部管理工
2626
nvm install
2727
nvm use
2828
corepack enable
29-
corepack prepare pnpm@10.6.5 --activate
29+
corepack prepare pnpm@10.34.1 --activate
3030
cp .env.example .env
3131
pnpm install
3232
pnpm local:bootstrap

apps/admin/package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,14 +11,14 @@
1111
},
1212
"dependencies": {
1313
"@rebase/shared": "workspace:*",
14-
"vue": "^3.5.13",
14+
"vue": "^3.5.35",
1515
"vue-router": "^4.5.0"
1616
},
1717
"devDependencies": {
1818
"@types/node": "^22.13.10",
1919
"@vitejs/plugin-vue": "^5.2.3",
2020
"typescript": "^5.8.2",
21-
"vite": "^6.2.1",
21+
"vite": "^6.4.2",
2222
"vue-tsc": "^2.2.8"
2323
}
2424
}

apps/admin/src/lib/api.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,9 +33,14 @@ const adminMessageTranslations = new Map<string, string>([
3333
['asset not found', '未找到媒体文件。'],
3434
['asset is still referenced', '该媒体仍被内容引用,无法删除。'],
3535
['event not found', '未找到活动。'],
36+
['file content does not match its declared mime type', '文件内容与声明的类型不一致,请重新导出后再上传。'],
37+
['file is too large', '文件过大,超过当前上传上限。'],
38+
['file type is not allowed', '当前只允许上传受支持的图片、PDF 或 MP4 文件。'],
3639
['job not found', '未找到招聘信息。'],
3740
['internal server error', '服务端内部错误。'],
41+
['private assets are not supported by the current upload pipeline', '当前上传链路只支持公开媒体,不支持私有媒体。'],
3842
['R2 delete is not configured', '当前环境未配置媒体删除能力。'],
43+
['upload request is too large', '上传请求过大,已被服务端拒绝。'],
3944
]);
4045

4146
const localizeAdminMessage = (message: string) => {

apps/admin/src/lib/format.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -166,7 +166,7 @@ export const formatAssetVisibility = (value: 'public' | 'private') => {
166166
case 'public':
167167
return '公开';
168168
case 'private':
169-
return '私有';
169+
return '私有(历史值)';
170170
default:
171171
return value;
172172
}

apps/admin/src/views/AssetsPage.vue

Lines changed: 100 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { formatAssetStatus, formatAssetVisibility, formatDateTime, formatFileSiz
99
1010
type AssetStatus = (typeof assetStatusValues)[number];
1111
type AssetVisibility = 'public' | 'private';
12+
type AssetVisibilityFilter = 'all' | AssetVisibility;
1213
1314
interface AssetFormState {
1415
storageProvider: string;
@@ -29,7 +30,6 @@ interface AssetFormState {
2930
3031
interface UploadFormState {
3132
folder: string;
32-
visibility: AssetVisibility;
3333
altText: string;
3434
assetType: string;
3535
}
@@ -42,7 +42,7 @@ interface UploadQueueItem {
4242
4343
type AssetWorkspaceMode = 'upload' | 'edit';
4444
45-
const visibilityOptions: AssetVisibility[] = ['public', 'private'];
45+
const filterVisibilityOptions: AssetVisibilityFilter[] = ['all', 'public', 'private'];
4646
4747
const createBlankForm = (bucket = 'rebase-media'): AssetFormState => ({
4848
storageProvider: 'r2',
@@ -63,7 +63,6 @@ const createBlankForm = (bucket = 'rebase-media'): AssetFormState => ({
6363
6464
const createUploadForm = (): UploadFormState => ({
6565
folder: 'media',
66-
visibility: 'public',
6766
altText: '',
6867
assetType: '',
6968
});
@@ -92,7 +91,7 @@ const upload = reactive<UploadFormState>(createUploadForm());
9291
const filters = reactive({
9392
query: '',
9493
status: 'all',
95-
visibility: 'all',
94+
visibility: 'all' as AssetVisibilityFilter,
9695
});
9796
9897
const selectedAsset = computed(() => rows.value.find((row) => row.id === selectedAssetId.value) ?? null);
@@ -151,6 +150,13 @@ const uploadStatusMessage = computed(() => {
151150
return uploadConfig.value.message;
152151
}
153152
});
153+
const uploadMimeSummary = computed(() => {
154+
const values = uploadConfig.value?.acceptedMimeTypes ?? [];
155+
return values.length > 0 ? values.join('') : '尚未配置';
156+
});
157+
const uploadLimitSummary = computed(() => (uploadConfig.value ? formatFileSize(uploadConfig.value.maxUploadBytes) : '尚未配置'));
158+
const legacyPrivateVisibility = computed(() => (selectedAsset.value?.visibility ?? form.visibility) === 'private');
159+
const editorVisibilityOptions = computed<AssetVisibility[]>(() => (legacyPrivateVisibility.value ? ['private', 'public'] : ['public']));
154160
const uploadProgressMessage = computed(() => {
155161
if (!uploading.value || !uploadProgressName.value) {
156162
return '';
@@ -379,15 +385,32 @@ const loadPage = async (nextSelectedId: string | null = selectedAssetId.value, p
379385
380386
const onFilePicked = (event: Event) => {
381387
const input = event.target as HTMLInputElement;
382-
setUploadFiles(Array.from(input.files ?? []));
388+
const files = Array.from(input.files ?? []);
389+
const acceptedFiles: File[] = [];
390+
const rejections: string[] = [];
391+
392+
for (const file of files) {
393+
const failure = validateSelectedUploadFile(file);
394+
if (failure) {
395+
rejections.push(`${file.name}:${failure}`);
396+
continue;
397+
}
398+
399+
acceptedFiles.push(file);
400+
}
401+
402+
setUploadFiles(acceptedFiles);
403+
if (rejections.length > 0) {
404+
errorMessage.value = `以下文件未加入上传队列:${summarizeUploadSelectionRejections(rejections)}`;
405+
}
383406
input.value = '';
384407
};
385408
386409
const buildUploadPayload = (file: File) => {
387410
const payload = new FormData();
388411
payload.append('file', file);
389412
payload.append('folder', upload.folder.trim());
390-
payload.append('visibility', upload.visibility);
413+
payload.append('visibility', 'public');
391414
392415
const altText = upload.altText.trim();
393416
if (altText) {
@@ -411,6 +434,40 @@ const summarizeUploadFailures = (failures: string[]) => {
411434
return `${visible};另有 ${failures.length - 3} 个文件失败`;
412435
};
413436
437+
const summarizeUploadSelectionRejections = (failures: string[]) => {
438+
const visible = failures.slice(0, 3).join('');
439+
if (failures.length <= 3) {
440+
return visible;
441+
}
442+
443+
return `${visible};另有 ${failures.length - 3} 个文件不符合限制`;
444+
};
445+
446+
const formatAssetVisibilityOption = (value: AssetVisibility) => {
447+
if (value === 'private') {
448+
return '私有(历史值,不再支持写入)';
449+
}
450+
451+
return '公开';
452+
};
453+
454+
const validateSelectedUploadFile = (file: File) => {
455+
const config = uploadConfig.value;
456+
if (!config) {
457+
return null;
458+
}
459+
460+
if (file.size > config.maxUploadBytes) {
461+
return `超过 ${formatFileSize(config.maxUploadBytes)}`;
462+
}
463+
464+
if (!config.acceptedMimeTypes.includes(file.type)) {
465+
return '文件类型不受支持';
466+
}
467+
468+
return null;
469+
};
470+
414471
const uploadAsset = async () => {
415472
if (uploadFileCount.value === 0) {
416473
errorMessage.value = '请先选择至少一个要上传的文件。';
@@ -644,7 +701,13 @@ const goToAssetPage = async (nextPage: number) => {
644701
<span>可见性</span>
645702
<select v-model="filters.visibility">
646703
<option value="all">全部可见性</option>
647-
<option v-for="visibility in visibilityOptions" :key="visibility" :value="visibility">{{ formatAssetVisibility(visibility) }}</option>
704+
<option
705+
v-for="visibility in filterVisibilityOptions.filter((value) => value !== 'all')"
706+
:key="visibility"
707+
:value="visibility"
708+
>
709+
{{ formatAssetVisibility(visibility) }}
710+
</option>
648711
</select>
649712
</label>
650713
</div>
@@ -762,6 +825,14 @@ const goToAssetPage = async (nextPage: number) => {
762825
<dt>对象目录</dt>
763826
<dd class="muted">{{ upload.folder || 'media' }}</dd>
764827
</div>
828+
<div class="summary-item">
829+
<dt>大小上限</dt>
830+
<dd class="muted">{{ uploadLimitSummary }}</dd>
831+
</div>
832+
<div class="summary-item">
833+
<dt>允许类型</dt>
834+
<dd class="muted">{{ uploadMimeSummary }}</dd>
835+
</div>
765836
</dl>
766837

767838
<div v-if="uploadPreviewSrc" class="summary-item summary-asset">
@@ -814,9 +885,8 @@ const goToAssetPage = async (nextPage: number) => {
814885
<div class="field-grid field-grid-2">
815886
<label class="field">
816887
<span>可见性</span>
817-
<select v-model="upload.visibility">
818-
<option v-for="visibility in visibilityOptions" :key="visibility" :value="visibility">{{ formatAssetVisibility(visibility) }}</option>
819-
</select>
888+
<input value="公开" type="text" readonly />
889+
<small>当前上传链路只支持公开媒体;历史 private 值不再允许新写入。</small>
820890
</label>
821891
<label class="field">
822892
<span>{{ uploadHasMultipleFiles ? '统一 Alt 文案(可选)' : 'Alt 文案' }}</span>
@@ -889,6 +959,9 @@ const goToAssetPage = async (nextPage: number) => {
889959
<h3>访问与路径</h3>
890960
<div class="panel-meta">{{ formatAssetVisibility(form.visibility) }}</div>
891961
</div>
962+
<div v-if="legacyPrivateVisibility" class="field-warning">
963+
这个媒体记录带有历史 `private` 值,但当前 R2 写入链路并不提供真正的私有访问控制。继续维护前请先改为公开,或等待后续引入私有桶 / 签名 URL。
964+
</div>
892965
<div class="field-grid field-grid-2 field-grid-compact">
893966
<label class="field">
894967
<span>存储提供方</span>
@@ -963,7 +1036,14 @@ const goToAssetPage = async (nextPage: number) => {
9631036
<label class="field">
9641037
<span>可见性</span>
9651038
<select v-model="form.visibility">
966-
<option v-for="visibility in visibilityOptions" :key="visibility" :value="visibility">{{ formatAssetVisibility(visibility) }}</option>
1039+
<option
1040+
v-for="visibility in editorVisibilityOptions"
1041+
:key="visibility"
1042+
:value="visibility"
1043+
:disabled="visibility === 'private'"
1044+
>
1045+
{{ formatAssetVisibilityOption(visibility) }}
1046+
</option>
9671047
</select>
9681048
</label>
9691049
<label class="field">
@@ -1005,6 +1085,15 @@ const goToAssetPage = async (nextPage: number) => {
10051085
</template>
10061086

10071087
<style scoped>
1088+
.field-warning {
1089+
padding: 0.72rem 0.78rem;
1090+
border: 1px solid rgba(167, 52, 32, 0.18);
1091+
border-radius: 12px;
1092+
background: rgba(167, 52, 32, 0.06);
1093+
color: var(--danger);
1094+
line-height: 1.5;
1095+
}
1096+
10081097
.asset-table th:first-child,
10091098
.asset-table td:first-child {
10101099
width: 5.4rem;

apps/api/package.json

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,13 @@
1111
"bootstrap-admin": "node --env-file=../../.env --import tsx src/scripts/bootstrap-admin.ts"
1212
},
1313
"dependencies": {
14-
"@aws-sdk/client-s3": "^3.1024.0",
15-
"@hono/node-server": "^1.19.1",
14+
"@aws-sdk/client-s3": "^3.1064.0",
15+
"@hono/node-server": "^1.19.13",
1616
"@rebase/db": "workspace:*",
1717
"@rebase/shared": "workspace:*",
18-
"better-auth": "^1.3.12",
19-
"drizzle-orm": "^0.41.0",
20-
"hono": "^4.12.0",
18+
"better-auth": "^1.6.15",
19+
"drizzle-orm": "^0.45.2",
20+
"hono": "^4.12.24",
2121
"image-size": "^2.0.2"
2222
},
2323
"devDependencies": {

0 commit comments

Comments
 (0)