refactor: remove authentication for internal deployment
Verify and deploy production / verify-and-deploy (push) Successful in 2m33s
Verify and deploy production / verify-and-deploy (push) Successful in 2m33s
This commit is contained in:
+2
-13
@@ -39,17 +39,10 @@ jobs:
|
||||
- name: Deploy persistent production service
|
||||
if: gitea.event_name == 'workflow_dispatch' || (gitea.event_name == 'push' && gitea.ref == 'refs/heads/main')
|
||||
env:
|
||||
STUDIO_USERNAME: studio
|
||||
STUDIO_PASSWORD: ${{ secrets.STUDIO_PASSWORD }}
|
||||
DEPLOY_PORT: ${{ vars.DEPLOY_PORT }}
|
||||
RELEASE_SHA: ${{ gitea.sha }}
|
||||
run: |
|
||||
set -eu
|
||||
if [ -z "$STUDIO_PASSWORD" ]; then
|
||||
echo "STUDIO_PASSWORD repository secret is required for production deployment"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ "$(id -u)" -eq 0 ]; then
|
||||
SUDO=""
|
||||
elif sudo -n true 2>/dev/null; then
|
||||
@@ -79,10 +72,7 @@ jobs:
|
||||
$SUDO chown -R root:root "$APP_ROOT"
|
||||
$SUDO chown -R "$SERVICE_USER:$SERVICE_USER" "$DATA_ROOT"
|
||||
$SUDO ln -sfn "$RELEASE_DIR" "$APP_ROOT/current"
|
||||
|
||||
printf '%s' "$STUDIO_PASSWORD" | $SUDO tee /etc/rednote-tool-studio.password >/dev/null
|
||||
$SUDO chown root:"$SERVICE_USER" /etc/rednote-tool-studio.password
|
||||
$SUDO chmod 640 /etc/rednote-tool-studio.password
|
||||
$SUDO rm -f /etc/rednote-tool-studio.password
|
||||
|
||||
cat > rednote-tool-studio.service <<EOF
|
||||
[Unit]
|
||||
@@ -98,8 +88,7 @@ jobs:
|
||||
Environment=NODE_ENV=production
|
||||
Environment=HOSTNAME=0.0.0.0
|
||||
Environment=PORT=$PORT
|
||||
Environment=STUDIO_USERNAME=$STUDIO_USERNAME
|
||||
ExecStart=/bin/sh -c 'export STUDIO_PASSWORD="\$(cat /etc/rednote-tool-studio.password)"; exec $APP_ROOT/bin/node server.js'
|
||||
ExecStart=$APP_ROOT/bin/node server.js
|
||||
Restart=always
|
||||
RestartSec=5
|
||||
|
||||
|
||||
+1
-1
@@ -26,4 +26,4 @@ Rednote Content Studio 正在改造成小红书内容生成工作台。最高层
|
||||
|
||||
内容草稿和实际生成接口尚未实现。不要把旧版小工具的 build、validate、package 接口当作新的内容生成能力。
|
||||
|
||||
生产环境使用 HTTP Basic Auth。用户名默认为 `studio`,密码由 `STUDIO_PASSWORD` 提供;`GET /api/health` 无需认证。
|
||||
应用面向受控内网运行,不提供登录鉴权。生产环境应通过内网和安全组限制访问范围。
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
- 可复用的内容生成方法配置
|
||||
- revision 乐观并发控制
|
||||
- 下一阶段内容生成流程的界面占位
|
||||
- 生产环境 Basic Auth、Docker 镜像与持久化目录
|
||||
- 面向内网运行的生产部署、Docker 镜像与持久化目录
|
||||
|
||||
实际的选题简报、文案生成、草稿管理、AI 模型接入和导出尚未实现。
|
||||
|
||||
@@ -35,7 +35,7 @@ npm install
|
||||
npm run dev
|
||||
```
|
||||
|
||||
访问 `http://localhost:3000`。开发环境未配置 `STUDIO_PASSWORD` 时允许本地访问;生产环境必须提供密码。
|
||||
访问 `http://localhost:3000`。应用本身不设置登录密码,生产环境应通过内网和安全组限制访问范围。
|
||||
|
||||
## 检查
|
||||
|
||||
|
||||
@@ -1,33 +0,0 @@
|
||||
import { timingSafeEqual } from "node:crypto";
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
|
||||
function sameValue(left: string, right: string) {
|
||||
const leftBuffer = Buffer.from(left);
|
||||
const rightBuffer = Buffer.from(right);
|
||||
return leftBuffer.length === rightBuffer.length && timingSafeEqual(leftBuffer, rightBuffer);
|
||||
}
|
||||
|
||||
export function proxy(request: NextRequest) {
|
||||
if (request.nextUrl.pathname === "/api/health") return NextResponse.next();
|
||||
|
||||
const password = process.env.STUDIO_PASSWORD;
|
||||
const username = process.env.STUDIO_USERNAME || "studio";
|
||||
if (!password) {
|
||||
if (process.env.NODE_ENV !== "production") return NextResponse.next();
|
||||
return NextResponse.json({ error: "STUDIO_PASSWORD is not configured" }, { status: 503 });
|
||||
}
|
||||
|
||||
const authorization = request.headers.get("authorization") || "";
|
||||
const expected = `Basic ${Buffer.from(`${username}:${password}`).toString("base64")}`;
|
||||
if (!sameValue(authorization, expected)) {
|
||||
return new NextResponse("Authentication required", {
|
||||
status: 401,
|
||||
headers: { "WWW-Authenticate": 'Basic realm="Rednote Tool Studio", charset="UTF-8"' },
|
||||
});
|
||||
}
|
||||
return NextResponse.next();
|
||||
}
|
||||
|
||||
export const config = {
|
||||
matcher: ["/((?!_next/static|_next/image|favicon.ico).*)"],
|
||||
};
|
||||
+1
-4
@@ -10,12 +10,9 @@ const volume = `${service}-data`;
|
||||
const sha = (process.env.GITHUB_SHA || process.env.CI_COMMIT_SHA || "dev").slice(0, 12);
|
||||
const image = `${service}:${sha}`;
|
||||
const port = process.env.DEPLOY_PORT || "3000";
|
||||
const password = process.env.STUDIO_PASSWORD;
|
||||
const username = process.env.STUDIO_USERNAME || "studio";
|
||||
const dockerfile = process.env.DEPLOY_DOCKERFILE || "Dockerfile";
|
||||
const usePrebuiltOutput = dockerfile === "Dockerfile.deploy";
|
||||
|
||||
if (!password) throw new Error("STUDIO_PASSWORD secret is required");
|
||||
if (!/^\d{2,5}$/.test(port) || Number(port) > 65535) throw new Error("DEPLOY_PORT must be a valid TCP port");
|
||||
|
||||
async function removeContainer(name) {
|
||||
@@ -98,7 +95,7 @@ try {
|
||||
next = await docker.createContainer({
|
||||
name: candidate,
|
||||
Image: image,
|
||||
Env: [`STUDIO_USERNAME=${username}`, `STUDIO_PASSWORD=${password}`, "NODE_ENV=production"],
|
||||
Env: ["NODE_ENV=production"],
|
||||
ExposedPorts: { "3000/tcp": {} },
|
||||
HostConfig: {
|
||||
RestartPolicy: { Name: "unless-stopped" },
|
||||
|
||||
Reference in New Issue
Block a user