add sign in page
This commit is contained in:
154
scripts/deploy-front-oss.mjs
Executable file
154
scripts/deploy-front-oss.mjs
Executable file
@@ -0,0 +1,154 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import fs from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
import process from 'node:process';
|
||||
import {spawnSync} from 'node:child_process';
|
||||
|
||||
import {
|
||||
buildObjectKey,
|
||||
createAuthorizationHeader,
|
||||
encodeObjectKey,
|
||||
getCacheControl,
|
||||
getContentType,
|
||||
listFiles,
|
||||
normalizeEndpoint,
|
||||
parseSimpleEnv,
|
||||
} from './oss-deploy-lib.mjs';
|
||||
|
||||
const repoRoot = process.cwd();
|
||||
const frontDir = path.join(repoRoot, 'front');
|
||||
const distDir = path.join(frontDir, 'dist');
|
||||
const envFilePath = path.join(repoRoot, '.env.oss.local');
|
||||
|
||||
function parseArgs(argv) {
|
||||
return {
|
||||
dryRun: argv.includes('--dry-run'),
|
||||
skipBuild: argv.includes('--skip-build'),
|
||||
};
|
||||
}
|
||||
|
||||
async function loadEnvFileIfPresent() {
|
||||
try {
|
||||
const raw = await fs.readFile(envFilePath, 'utf-8');
|
||||
const values = parseSimpleEnv(raw);
|
||||
for (const [key, value] of Object.entries(values)) {
|
||||
if (!process.env[key]) {
|
||||
process.env[key] = value;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
if (error && typeof error === 'object' && 'code' in error && error.code === 'ENOENT') {
|
||||
return;
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
function requireEnv(name) {
|
||||
const value = process.env[name];
|
||||
if (!value) {
|
||||
throw new Error(`Missing required environment variable: ${name}`);
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
function runBuild() {
|
||||
const result = spawnSync('npm', ['run', 'build'], {
|
||||
cwd: frontDir,
|
||||
stdio: 'inherit',
|
||||
shell: process.platform === 'win32',
|
||||
});
|
||||
|
||||
if (result.status !== 0) {
|
||||
throw new Error('Frontend build failed');
|
||||
}
|
||||
}
|
||||
|
||||
async function uploadFile({
|
||||
bucket,
|
||||
endpoint,
|
||||
objectKey,
|
||||
filePath,
|
||||
accessKeyId,
|
||||
accessKeySecret,
|
||||
}) {
|
||||
const body = await fs.readFile(filePath);
|
||||
const contentType = getContentType(objectKey);
|
||||
const date = new Date().toUTCString();
|
||||
const url = `https://${bucket}.${normalizeEndpoint(endpoint)}/${encodeObjectKey(objectKey)}`;
|
||||
const authorization = createAuthorizationHeader({
|
||||
method: 'PUT',
|
||||
bucket,
|
||||
objectKey,
|
||||
contentType,
|
||||
date,
|
||||
accessKeyId,
|
||||
accessKeySecret,
|
||||
});
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
Authorization: authorization,
|
||||
'Cache-Control': getCacheControl(objectKey),
|
||||
'Content-Length': String(body.byteLength),
|
||||
'Content-Type': contentType,
|
||||
Date: date,
|
||||
},
|
||||
body,
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const text = await response.text();
|
||||
throw new Error(`Upload failed for ${objectKey}: ${response.status} ${response.statusText}\n${text}`);
|
||||
}
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const {dryRun, skipBuild} = parseArgs(process.argv.slice(2));
|
||||
|
||||
await loadEnvFileIfPresent();
|
||||
|
||||
const accessKeyId = requireEnv('YOYUZH_OSS_ACCESS_KEY_ID');
|
||||
const accessKeySecret = requireEnv('YOYUZH_OSS_ACCESS_KEY_SECRET');
|
||||
const endpoint = requireEnv('YOYUZH_OSS_ENDPOINT');
|
||||
const bucket = requireEnv('YOYUZH_OSS_BUCKET');
|
||||
const remotePrefix = process.env.YOYUZH_OSS_PREFIX || '';
|
||||
|
||||
if (!skipBuild) {
|
||||
runBuild();
|
||||
}
|
||||
|
||||
const files = await listFiles(distDir);
|
||||
if (files.length === 0) {
|
||||
throw new Error('No files found in front/dist. Run the frontend build first.');
|
||||
}
|
||||
|
||||
for (const filePath of files) {
|
||||
const relativePath = path.relative(distDir, filePath).split(path.sep).join('/');
|
||||
const objectKey = buildObjectKey(remotePrefix, relativePath);
|
||||
|
||||
if (dryRun) {
|
||||
console.log(`[dry-run] upload ${relativePath} -> ${objectKey}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
await uploadFile({
|
||||
bucket,
|
||||
endpoint,
|
||||
objectKey,
|
||||
filePath,
|
||||
accessKeyId,
|
||||
accessKeySecret,
|
||||
});
|
||||
console.log(`uploaded ${objectKey}`);
|
||||
}
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
console.error(error instanceof Error ? error.message : String(error));
|
||||
process.exitCode = 1;
|
||||
});
|
||||
126
scripts/oss-deploy-lib.mjs
Normal file
126
scripts/oss-deploy-lib.mjs
Normal file
@@ -0,0 +1,126 @@
|
||||
import crypto from 'node:crypto';
|
||||
import fs from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
|
||||
const CONTENT_TYPES = new Map([
|
||||
['.css', 'text/css; charset=utf-8'],
|
||||
['.html', 'text/html; charset=utf-8'],
|
||||
['.ico', 'image/x-icon'],
|
||||
['.jpeg', 'image/jpeg'],
|
||||
['.jpg', 'image/jpeg'],
|
||||
['.js', 'text/javascript; charset=utf-8'],
|
||||
['.json', 'application/json; charset=utf-8'],
|
||||
['.map', 'application/json; charset=utf-8'],
|
||||
['.png', 'image/png'],
|
||||
['.svg', 'image/svg+xml'],
|
||||
['.txt', 'text/plain; charset=utf-8'],
|
||||
['.webmanifest', 'application/manifest+json; charset=utf-8'],
|
||||
]);
|
||||
|
||||
export function normalizeEndpoint(endpoint) {
|
||||
return endpoint.replace(/^https?:\/\//, '').replace(/\/+$/, '');
|
||||
}
|
||||
|
||||
export function buildObjectKey(prefix, relativePath) {
|
||||
const cleanPrefix = prefix.replace(/^\/+|\/+$/g, '');
|
||||
const cleanRelativePath = relativePath.replace(/^\/+/, '');
|
||||
return cleanPrefix ? `${cleanPrefix}/${cleanRelativePath}` : cleanRelativePath;
|
||||
}
|
||||
|
||||
export function getCacheControl(relativePath) {
|
||||
if (relativePath === 'index.html') {
|
||||
return 'no-cache';
|
||||
}
|
||||
|
||||
if (relativePath.startsWith('assets/')) {
|
||||
return 'public,max-age=31536000,immutable';
|
||||
}
|
||||
|
||||
return 'public,max-age=300';
|
||||
}
|
||||
|
||||
export function getContentType(relativePath) {
|
||||
const ext = path.extname(relativePath).toLowerCase();
|
||||
return CONTENT_TYPES.get(ext) || 'application/octet-stream';
|
||||
}
|
||||
|
||||
export function createAuthorizationHeader({
|
||||
method,
|
||||
bucket,
|
||||
objectKey,
|
||||
contentType,
|
||||
date,
|
||||
accessKeyId,
|
||||
accessKeySecret,
|
||||
}) {
|
||||
const stringToSign = [
|
||||
method.toUpperCase(),
|
||||
'',
|
||||
contentType,
|
||||
date,
|
||||
`/${bucket}/${objectKey}`,
|
||||
].join('\n');
|
||||
|
||||
const signature = crypto
|
||||
.createHmac('sha1', accessKeySecret)
|
||||
.update(stringToSign)
|
||||
.digest('base64');
|
||||
|
||||
return `OSS ${accessKeyId}:${signature}`;
|
||||
}
|
||||
|
||||
export function encodeObjectKey(objectKey) {
|
||||
return objectKey
|
||||
.split('/')
|
||||
.map((part) => encodeURIComponent(part))
|
||||
.join('/');
|
||||
}
|
||||
|
||||
export async function listFiles(rootDir) {
|
||||
const entries = await fs.readdir(rootDir, {withFileTypes: true});
|
||||
const files = [];
|
||||
|
||||
for (const entry of entries) {
|
||||
const absolutePath = path.join(rootDir, entry.name);
|
||||
if (entry.isDirectory()) {
|
||||
files.push(...(await listFiles(absolutePath)));
|
||||
continue;
|
||||
}
|
||||
|
||||
if (entry.isFile()) {
|
||||
files.push(absolutePath);
|
||||
}
|
||||
}
|
||||
|
||||
return files.sort();
|
||||
}
|
||||
|
||||
export function parseSimpleEnv(rawText) {
|
||||
const parsed = {};
|
||||
|
||||
for (const line of rawText.split(/\r?\n/)) {
|
||||
const trimmed = line.trim();
|
||||
if (!trimmed || trimmed.startsWith('#')) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const separatorIndex = trimmed.indexOf('=');
|
||||
if (separatorIndex === -1) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const key = trimmed.slice(0, separatorIndex).trim();
|
||||
let value = trimmed.slice(separatorIndex + 1).trim();
|
||||
|
||||
if (
|
||||
(value.startsWith('"') && value.endsWith('"')) ||
|
||||
(value.startsWith("'") && value.endsWith("'"))
|
||||
) {
|
||||
value = value.slice(1, -1);
|
||||
}
|
||||
|
||||
parsed[key] = value;
|
||||
}
|
||||
|
||||
return parsed;
|
||||
}
|
||||
46
scripts/oss-deploy-lib.test.mjs
Normal file
46
scripts/oss-deploy-lib.test.mjs
Normal file
@@ -0,0 +1,46 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
|
||||
import {
|
||||
buildObjectKey,
|
||||
createAuthorizationHeader,
|
||||
getCacheControl,
|
||||
getContentType,
|
||||
normalizeEndpoint,
|
||||
} from './oss-deploy-lib.mjs';
|
||||
|
||||
test('normalizeEndpoint strips scheme and trailing slashes', () => {
|
||||
assert.equal(normalizeEndpoint('https://oss-ap-northeast-1.aliyuncs.com/'), 'oss-ap-northeast-1.aliyuncs.com');
|
||||
});
|
||||
|
||||
test('buildObjectKey joins optional prefix with relative path', () => {
|
||||
assert.equal(buildObjectKey('', 'assets/index.js'), 'assets/index.js');
|
||||
assert.equal(buildObjectKey('portal', 'assets/index.js'), 'portal/assets/index.js');
|
||||
});
|
||||
|
||||
test('getCacheControl keeps index uncached and assets immutable', () => {
|
||||
assert.equal(getCacheControl('index.html'), 'no-cache');
|
||||
assert.equal(getCacheControl('assets/index.js'), 'public,max-age=31536000,immutable');
|
||||
assert.equal(getCacheControl('race/index.html'), 'public,max-age=300');
|
||||
});
|
||||
|
||||
test('getContentType resolves common frontend asset types', () => {
|
||||
assert.equal(getContentType('index.html'), 'text/html; charset=utf-8');
|
||||
assert.equal(getContentType('assets/app.css'), 'text/css; charset=utf-8');
|
||||
assert.equal(getContentType('assets/app.js'), 'text/javascript; charset=utf-8');
|
||||
assert.equal(getContentType('favicon.png'), 'image/png');
|
||||
});
|
||||
|
||||
test('createAuthorizationHeader is stable for a known request', () => {
|
||||
const header = createAuthorizationHeader({
|
||||
method: 'PUT',
|
||||
bucket: 'demo-bucket',
|
||||
objectKey: 'assets/index.js',
|
||||
contentType: 'text/javascript; charset=utf-8',
|
||||
date: 'Tue, 17 Mar 2026 12:00:00 GMT',
|
||||
accessKeyId: 'test-id',
|
||||
accessKeySecret: 'test-secret',
|
||||
});
|
||||
|
||||
assert.equal(header, 'OSS test-id:JgyH7mTiSILGGWsnXJwg4KIBRO4=');
|
||||
});
|
||||
Reference in New Issue
Block a user