feat(auth): 添加认证模块和图片批注功能(项目初始化)
- 实现用户登录、登出、密码修改等认证功能 - 添加会话管理和权限控制中间件 - 创建图片批注组件和相关API路由 - 实现批注的增删改查功能 - 添加Docker和Git忽略配置文件 - 创建系统架构文档和开发约定说明 - 集成认证模块到前端应用路由中
This commit is contained in:
90
api/storage.ts
Normal file
90
api/storage.ts
Normal file
@@ -0,0 +1,90 @@
|
||||
import COS from 'cos-nodejs-sdk-v5';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { randomUUID } from 'crypto';
|
||||
import { database } from './database.js';
|
||||
import { decryptSecret } from './configCrypto.js';
|
||||
|
||||
export interface StorageConfigRecord {
|
||||
id: number;
|
||||
region: string;
|
||||
bucket: string;
|
||||
public_base_url: string;
|
||||
cdn_domain: string;
|
||||
path_prefix: string;
|
||||
secret_id_encrypted: string;
|
||||
secret_key_encrypted: string;
|
||||
}
|
||||
|
||||
export interface StoredUpload {
|
||||
url: string;
|
||||
storageProvider: 'local' | 'tencent_cos';
|
||||
storageKey: string;
|
||||
}
|
||||
|
||||
export async function getActiveStorageConfig(): Promise<StorageConfigRecord | undefined> {
|
||||
return database.one<StorageConfigRecord>(`
|
||||
SELECT id, region, bucket, public_base_url, cdn_domain, path_prefix,
|
||||
secret_id_encrypted, secret_key_encrypted
|
||||
FROM storage_configs WHERE status = 'active' ORDER BY id DESC LIMIT 1
|
||||
`);
|
||||
}
|
||||
|
||||
function createClient(config: StorageConfigRecord) {
|
||||
return new COS({
|
||||
SecretId: decryptSecret(config.secret_id_encrypted),
|
||||
SecretKey: decryptSecret(config.secret_key_encrypted),
|
||||
});
|
||||
}
|
||||
|
||||
function cleanPrefix(value: string): string {
|
||||
return value.trim().replace(/^\/+|\/+$/g, '').replace(/\/{2,}/g, '/');
|
||||
}
|
||||
|
||||
function objectUrl(config: StorageConfigRecord, key: string): string {
|
||||
const base = (config.cdn_domain || config.public_base_url || `https://${config.bucket}.cos.${config.region}.myqcloud.com`).replace(/\/+$/, '');
|
||||
return `${base}/${key.split('/').map(encodeURIComponent).join('/')}`;
|
||||
}
|
||||
|
||||
function safeError(error: unknown): string {
|
||||
if (!error || typeof error !== 'object') return 'COS 连接失败';
|
||||
const item = error as { code?: string; statusCode?: number; message?: string };
|
||||
return [item.code, item.statusCode, item.message].filter(Boolean).join(' · ').slice(0, 300) || 'COS 连接失败';
|
||||
}
|
||||
|
||||
export async function testStorageConfig(config: StorageConfigRecord): Promise<string> {
|
||||
const cos = createClient(config);
|
||||
const prefix = cleanPrefix(config.path_prefix);
|
||||
const key = `${prefix ? `${prefix}/` : ''}.delivery-desk-check/${randomUUID()}.txt`;
|
||||
try {
|
||||
await cos.putObject({ Bucket: config.bucket, Region: config.region, Key: key, Body: Buffer.from('delivery-desk storage check'), ContentType: 'text/plain' });
|
||||
await cos.getObject({ Bucket: config.bucket, Region: config.region, Key: key });
|
||||
await cos.deleteObject({ Bucket: config.bucket, Region: config.region, Key: key });
|
||||
return '上传、读取和删除测试均已通过';
|
||||
} catch (error) {
|
||||
try { await cos.deleteObject({ Bucket: config.bucket, Region: config.region, Key: key }); } catch { /* best-effort cleanup */ }
|
||||
throw new Error(safeError(error));
|
||||
}
|
||||
}
|
||||
|
||||
export async function storeUploadedFile(file: { filename: string; originalname?: string; mimetype?: string; path: string }): Promise<StoredUpload> {
|
||||
const config = await getActiveStorageConfig();
|
||||
if (!config) return { url: file.filename, storageProvider: 'local', storageKey: file.filename };
|
||||
|
||||
const prefix = cleanPrefix(config.path_prefix);
|
||||
const now = new Date();
|
||||
const extension = path.extname(file.originalname || file.filename).toLowerCase() || '.jpg';
|
||||
const month = `${now.getUTCFullYear()}/${String(now.getUTCMonth() + 1).padStart(2, '0')}`;
|
||||
const key = `${prefix ? `${prefix}/` : ''}originals/${month}/${randomUUID()}${extension}`;
|
||||
const cos = createClient(config);
|
||||
await cos.putObject({
|
||||
Bucket: config.bucket,
|
||||
Region: config.region,
|
||||
Key: key,
|
||||
Body: fs.createReadStream(file.path),
|
||||
ContentLength: fs.statSync(file.path).size,
|
||||
ContentType: file.mimetype || 'application/octet-stream',
|
||||
});
|
||||
fs.unlinkSync(file.path);
|
||||
return { url: objectUrl(config, key), storageProvider: 'tencent_cos', storageKey: key };
|
||||
}
|
||||
Reference in New Issue
Block a user