支持开发环境局域网登录测试
This commit is contained in:
@@ -22,6 +22,11 @@ class LoginRequest(BaseModel):
|
|||||||
ciphertext: str = Field(min_length=1)
|
ciphertext: str = Field(min_length=1)
|
||||||
|
|
||||||
|
|
||||||
|
class DevLoginRequest(BaseModel):
|
||||||
|
email: str = Field(min_length=1)
|
||||||
|
password: str = Field(min_length=1)
|
||||||
|
|
||||||
|
|
||||||
class LoginKeyResponse(BaseModel):
|
class LoginKeyResponse(BaseModel):
|
||||||
key_id: str
|
key_id: str
|
||||||
public_key: str
|
public_key: str
|
||||||
@@ -84,6 +89,29 @@ async def authenticate_encrypted_password(payload: LoginRequest, db: AsyncSessio
|
|||||||
return db_user
|
return db_user
|
||||||
|
|
||||||
|
|
||||||
|
async def authenticate_plain_password(payload: DevLoginRequest, db: AsyncSession):
|
||||||
|
db_user = await user_crud.get_by_email(db, payload.email)
|
||||||
|
if not db_user:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||||
|
detail="账号不存在",
|
||||||
|
)
|
||||||
|
if not verify_password(payload.password, db_user.password_hash):
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||||
|
detail="密码错误",
|
||||||
|
)
|
||||||
|
return db_user
|
||||||
|
|
||||||
|
|
||||||
|
def ensure_user_active(db_user) -> None:
|
||||||
|
if db_user.status != UserStatus.ACTIVE:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||||
|
detail="账号未审核或不可用",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@router.post("/register", status_code=status.HTTP_201_CREATED)
|
@router.post("/register", status_code=status.HTTP_201_CREATED)
|
||||||
async def register(
|
async def register(
|
||||||
payload: UserRegisterRequest,
|
payload: UserRegisterRequest,
|
||||||
@@ -112,15 +140,22 @@ async def login_for_access_token(
|
|||||||
payload: LoginRequest, db: AsyncSession = Depends(get_db_session)
|
payload: LoginRequest, db: AsyncSession = Depends(get_db_session)
|
||||||
) -> Token:
|
) -> Token:
|
||||||
db_user = await authenticate_encrypted_password(payload, db)
|
db_user = await authenticate_encrypted_password(payload, db)
|
||||||
if db_user.status != UserStatus.ACTIVE:
|
ensure_user_active(db_user)
|
||||||
raise HTTPException(
|
|
||||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
|
||||||
detail="账号未审核或不可用",
|
|
||||||
)
|
|
||||||
|
|
||||||
return issue_user_token(db_user)
|
return issue_user_token(db_user)
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/dev-login", response_model=Token)
|
||||||
|
async def dev_login_for_access_token(
|
||||||
|
payload: DevLoginRequest, db: AsyncSession = Depends(get_db_session)
|
||||||
|
) -> Token:
|
||||||
|
if settings.ENV != "development":
|
||||||
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Not found")
|
||||||
|
db_user = await authenticate_plain_password(payload, db)
|
||||||
|
ensure_user_active(db_user)
|
||||||
|
return issue_user_token(db_user)
|
||||||
|
|
||||||
|
|
||||||
@router.get("/me", response_model=UserRead)
|
@router.get("/me", response_model=UserRead)
|
||||||
async def read_me(current_user=Depends(get_current_user)) -> UserRead:
|
async def read_me(current_user=Depends(get_current_user)) -> UserRead:
|
||||||
return current_user
|
return current_user
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import json
|
|||||||
import os
|
import os
|
||||||
|
|
||||||
from app.main import create_app
|
from app.main import create_app
|
||||||
|
from app.core.config import settings
|
||||||
from app.core.deps import get_db_session
|
from app.core.deps import get_db_session
|
||||||
from app.core.security import hash_password
|
from app.core.security import hash_password
|
||||||
from app.crud import user as user_crud
|
from app.crud import user as user_crud
|
||||||
@@ -200,6 +201,58 @@ async def test_plaintext_login_is_rejected(client_and_db):
|
|||||||
assert resp.status_code == 422
|
assert resp.status_code == 422
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_dev_login_allows_plaintext_only_in_development(client_and_db):
|
||||||
|
client, _ = client_and_db
|
||||||
|
original_env = settings.ENV
|
||||||
|
settings.ENV = "development"
|
||||||
|
try:
|
||||||
|
resp = await client.post("/api/v1/auth/dev-login", json={"email": "admin@test.com", "password": "admin123"})
|
||||||
|
finally:
|
||||||
|
settings.ENV = original_env
|
||||||
|
|
||||||
|
assert resp.status_code == 200
|
||||||
|
assert resp.json()["token_type"] == "bearer"
|
||||||
|
assert resp.json()["access_token"]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_dev_login_is_disabled_outside_development(client_and_db):
|
||||||
|
client, _ = client_and_db
|
||||||
|
original_env = settings.ENV
|
||||||
|
settings.ENV = "production"
|
||||||
|
try:
|
||||||
|
resp = await client.post("/api/v1/auth/dev-login", json={"email": "admin@test.com", "password": "admin123"})
|
||||||
|
finally:
|
||||||
|
settings.ENV = original_env
|
||||||
|
|
||||||
|
assert resp.status_code == 404
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_dev_login_rejects_inactive_users(client_and_db):
|
||||||
|
client, SessionLocal = client_and_db
|
||||||
|
original_env = settings.ENV
|
||||||
|
settings.ENV = "development"
|
||||||
|
payload = {
|
||||||
|
"email": "pending-dev-login@test.com",
|
||||||
|
"password": "Password123",
|
||||||
|
"full_name": "Pending Dev Login",
|
||||||
|
"clinical_department": "Clinical",
|
||||||
|
}
|
||||||
|
try:
|
||||||
|
await client.post("/api/v1/auth/register", json=payload)
|
||||||
|
resp = await client.post(
|
||||||
|
"/api/v1/auth/dev-login",
|
||||||
|
json={"email": payload["email"], "password": payload["password"]},
|
||||||
|
)
|
||||||
|
finally:
|
||||||
|
settings.ENV = original_env
|
||||||
|
|
||||||
|
assert resp.status_code == 401
|
||||||
|
assert "账号未审核" in resp.json().get("detail", "")
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_login_challenge_cannot_be_reused(client_and_db):
|
async def test_login_challenge_cannot_be_reused(client_and_db):
|
||||||
client, _ = client_and_db
|
client, _ = client_and_db
|
||||||
|
|||||||
@@ -62,6 +62,9 @@ services:
|
|||||||
build:
|
build:
|
||||||
context: .
|
context: .
|
||||||
dockerfile: nginx/Dockerfile
|
dockerfile: nginx/Dockerfile
|
||||||
|
args:
|
||||||
|
VITE_RUNTIME_ENV: ${ENV:-production}
|
||||||
|
VITE_ALLOW_INSECURE_DEV_LOGIN: ${VITE_ALLOW_INSECURE_DEV_LOGIN:-false}
|
||||||
container_name: ctms_nginx
|
container_name: ctms_nginx
|
||||||
restart: always
|
restart: always
|
||||||
ports:
|
ports:
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
# Frontend feature flags and timeline overrides
|
# Frontend feature flags and timeline overrides
|
||||||
|
VITE_RUNTIME_ENV=production
|
||||||
|
VITE_ALLOW_INSECURE_DEV_LOGIN=false
|
||||||
VITE_STARTUP_SUBMIT_ACCEPT_TIMEOUT_MONTHS=3
|
VITE_STARTUP_SUBMIT_ACCEPT_TIMEOUT_MONTHS=3
|
||||||
VITE_STARTUP_ACCEPT_TIMEOUT_MONTHS=3
|
VITE_STARTUP_ACCEPT_TIMEOUT_MONTHS=3
|
||||||
VITE_STARTUP_ACCEPT_APPROVAL_TIMEOUT_MONTHS=6
|
VITE_STARTUP_ACCEPT_APPROVAL_TIMEOUT_MONTHS=6
|
||||||
|
|||||||
@@ -1,10 +1,20 @@
|
|||||||
import type { AxiosResponse } from "axios";
|
import type { AxiosResponse } from "axios";
|
||||||
import api, { apiGet, apiPatch, apiPost } from "./axios";
|
import api, { apiGet, apiPatch, apiPost } from "./axios";
|
||||||
import type { UserMeResponse, LoginRequest, LoginResponse, LoginKeyResponse, RegisterRequest } from "../types/api";
|
import type {
|
||||||
|
UserMeResponse,
|
||||||
|
LoginRequest,
|
||||||
|
DevLoginRequest,
|
||||||
|
LoginResponse,
|
||||||
|
LoginKeyResponse,
|
||||||
|
RegisterRequest,
|
||||||
|
} from "../types/api";
|
||||||
|
|
||||||
export const login = (payload: LoginRequest): Promise<AxiosResponse<LoginResponse>> =>
|
export const login = (payload: LoginRequest): Promise<AxiosResponse<LoginResponse>> =>
|
||||||
apiPost<LoginResponse>("/api/v1/auth/login", payload);
|
apiPost<LoginResponse>("/api/v1/auth/login", payload);
|
||||||
|
|
||||||
|
export const devLogin = (payload: DevLoginRequest): Promise<AxiosResponse<LoginResponse>> =>
|
||||||
|
apiPost<LoginResponse>("/api/v1/auth/dev-login", payload);
|
||||||
|
|
||||||
export const getLoginKey = (): Promise<AxiosResponse<LoginKeyResponse>> =>
|
export const getLoginKey = (): Promise<AxiosResponse<LoginKeyResponse>> =>
|
||||||
apiGet<LoginKeyResponse>("/api/v1/auth/login-key");
|
apiGet<LoginKeyResponse>("/api/v1/auth/login-key");
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,31 @@
|
|||||||
import { beforeEach, describe, expect, it } from "vitest";
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||||
import { createPinia, setActivePinia } from "pinia";
|
import { createPinia, setActivePinia } from "pinia";
|
||||||
|
|
||||||
|
const getLoginKeyMock = vi.fn();
|
||||||
|
const loginMock = vi.fn();
|
||||||
|
const devLoginMock = vi.fn();
|
||||||
|
const fetchMeMock = vi.fn();
|
||||||
|
const encryptLoginPayloadMock = vi.fn();
|
||||||
|
|
||||||
|
vi.mock("../api/auth", () => ({
|
||||||
|
getLoginKey: getLoginKeyMock,
|
||||||
|
login: loginMock,
|
||||||
|
devLogin: devLoginMock,
|
||||||
|
fetchMe: fetchMeMock,
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock("../utils/loginCrypto", () => ({
|
||||||
|
encryptLoginPayload: encryptLoginPayloadMock,
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock("../api/studies", () => ({
|
||||||
|
fetchStudies: vi.fn().mockResolvedValue({ data: { items: [] } }),
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock("../api/projectPermissions", () => ({
|
||||||
|
fetchMyApiEndpointPermissions: vi.fn().mockResolvedValue({ data: {} }),
|
||||||
|
}));
|
||||||
|
|
||||||
type StorageLike = {
|
type StorageLike = {
|
||||||
getItem: (key: string) => string | null;
|
getItem: (key: string) => string | null;
|
||||||
setItem: (key: string, value: string) => void;
|
setItem: (key: string, value: string) => void;
|
||||||
@@ -27,6 +52,30 @@ const createStorage = (): StorageLike => {
|
|||||||
describe("auth store logout", () => {
|
describe("auth store logout", () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
setActivePinia(createPinia());
|
setActivePinia(createPinia());
|
||||||
|
vi.clearAllMocks();
|
||||||
|
vi.unstubAllEnvs();
|
||||||
|
vi.stubEnv("VITE_RUNTIME_ENV", "production");
|
||||||
|
vi.stubGlobal("isSecureContext", true);
|
||||||
|
getLoginKeyMock.mockResolvedValue({
|
||||||
|
data: {
|
||||||
|
key_id: "default",
|
||||||
|
public_key: "public-key",
|
||||||
|
challenge: "challenge-value-with-enough-length",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
loginMock.mockResolvedValue({ data: { access_token: "encrypted-token", token_type: "bearer" } });
|
||||||
|
devLoginMock.mockResolvedValue({ data: { access_token: "dev-token", token_type: "bearer" } });
|
||||||
|
fetchMeMock.mockResolvedValue({
|
||||||
|
data: {
|
||||||
|
id: "user-1",
|
||||||
|
email: "admin@test.com",
|
||||||
|
full_name: "Admin",
|
||||||
|
clinical_department: "Admin",
|
||||||
|
status: "ACTIVE",
|
||||||
|
is_admin: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
encryptLoginPayloadMock.mockResolvedValue("ciphertext");
|
||||||
Object.defineProperty(window, "localStorage", {
|
Object.defineProperty(window, "localStorage", {
|
||||||
value: createStorage(),
|
value: createStorage(),
|
||||||
configurable: true,
|
configurable: true,
|
||||||
@@ -46,4 +95,36 @@ describe("auth store logout", () => {
|
|||||||
expect(session.lockReason).toBeNull();
|
expect(session.lockReason).toBeNull();
|
||||||
expect(session.lockEmail).toBe("");
|
expect(session.lockEmail).toBe("");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("uses encrypted login by default outside a secure browser context", async () => {
|
||||||
|
vi.stubGlobal("isSecureContext", false);
|
||||||
|
const { useAuthStore } = await import("./auth");
|
||||||
|
const auth = useAuthStore();
|
||||||
|
|
||||||
|
await auth.login("admin@test.com", "admin123");
|
||||||
|
|
||||||
|
expect(getLoginKeyMock).toHaveBeenCalledOnce();
|
||||||
|
expect(encryptLoginPayloadMock).toHaveBeenCalledOnce();
|
||||||
|
expect(loginMock).toHaveBeenCalledWith({
|
||||||
|
key_id: "default",
|
||||||
|
challenge: "challenge-value-with-enough-length",
|
||||||
|
ciphertext: "ciphertext",
|
||||||
|
});
|
||||||
|
expect(devLoginMock).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("uses dev login only when enabled in development outside a secure browser context", async () => {
|
||||||
|
vi.stubGlobal("isSecureContext", false);
|
||||||
|
vi.stubEnv("VITE_RUNTIME_ENV", "development");
|
||||||
|
vi.stubEnv("VITE_ALLOW_INSECURE_DEV_LOGIN", "true");
|
||||||
|
const { useAuthStore } = await import("./auth");
|
||||||
|
const auth = useAuthStore();
|
||||||
|
|
||||||
|
await auth.login("admin@test.com", "admin123");
|
||||||
|
|
||||||
|
expect(devLoginMock).toHaveBeenCalledWith({ email: "admin@test.com", password: "admin123" });
|
||||||
|
expect(getLoginKeyMock).not.toHaveBeenCalled();
|
||||||
|
expect(encryptLoginPayloadMock).not.toHaveBeenCalled();
|
||||||
|
expect(loginMock).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
+22
-12
@@ -1,6 +1,6 @@
|
|||||||
import { defineStore } from "pinia";
|
import { defineStore } from "pinia";
|
||||||
import { ref } from "vue";
|
import { ref } from "vue";
|
||||||
import { getLoginKey, login as apiLogin, fetchMe } from "../api/auth";
|
import { getLoginKey, login as apiLogin, devLogin, fetchMe } from "../api/auth";
|
||||||
import { setToken, clearToken, getToken } from "../utils/auth";
|
import { setToken, clearToken, getToken } from "../utils/auth";
|
||||||
import { encryptLoginPayload } from "../utils/loginCrypto";
|
import { encryptLoginPayload } from "../utils/loginCrypto";
|
||||||
import type { UserInfo } from "../types/api";
|
import type { UserInfo } from "../types/api";
|
||||||
@@ -9,6 +9,9 @@ import { useSessionStore } from "./session";
|
|||||||
|
|
||||||
export const useAuthStore = defineStore("auth", () => {
|
export const useAuthStore = defineStore("auth", () => {
|
||||||
const LAST_LOGIN_EMAIL_KEY = "ctms_last_login_email";
|
const LAST_LOGIN_EMAIL_KEY = "ctms_last_login_email";
|
||||||
|
const allowInsecureDevLogin =
|
||||||
|
import.meta.env.VITE_RUNTIME_ENV === "development" &&
|
||||||
|
import.meta.env.VITE_ALLOW_INSECURE_DEV_LOGIN === "true";
|
||||||
const token = ref<string | null>(getToken());
|
const token = ref<string | null>(getToken());
|
||||||
const user = ref<UserInfo | null>(null);
|
const user = ref<UserInfo | null>(null);
|
||||||
const loading = ref(false);
|
const loading = ref(false);
|
||||||
@@ -17,17 +20,10 @@ export const useAuthStore = defineStore("auth", () => {
|
|||||||
const login = async (email: string, password: string) => {
|
const login = async (email: string, password: string) => {
|
||||||
loading.value = true;
|
loading.value = true;
|
||||||
try {
|
try {
|
||||||
const { data: loginKey } = await getLoginKey();
|
const { data } =
|
||||||
const ciphertext = await encryptLoginPayload(loginKey.public_key, {
|
allowInsecureDevLogin && !window.isSecureContext
|
||||||
email,
|
? await devLogin({ email, password })
|
||||||
password,
|
: await encryptedLogin(email, password);
|
||||||
challenge: loginKey.challenge,
|
|
||||||
});
|
|
||||||
const { data } = await apiLogin({
|
|
||||||
key_id: loginKey.key_id,
|
|
||||||
challenge: loginKey.challenge,
|
|
||||||
ciphertext,
|
|
||||||
});
|
|
||||||
token.value = data.access_token;
|
token.value = data.access_token;
|
||||||
setToken(data.access_token);
|
setToken(data.access_token);
|
||||||
// 避免锁屏态下 fetchMe 被请求拦截
|
// 避免锁屏态下 fetchMe 被请求拦截
|
||||||
@@ -44,6 +40,20 @@ export const useAuthStore = defineStore("auth", () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const encryptedLogin = async (email: string, password: string) => {
|
||||||
|
const { data: loginKey } = await getLoginKey();
|
||||||
|
const ciphertext = await encryptLoginPayload(loginKey.public_key, {
|
||||||
|
email,
|
||||||
|
password,
|
||||||
|
challenge: loginKey.challenge,
|
||||||
|
});
|
||||||
|
return apiLogin({
|
||||||
|
key_id: loginKey.key_id,
|
||||||
|
challenge: loginKey.challenge,
|
||||||
|
ciphertext,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
const fetchMeAction = async () => {
|
const fetchMeAction = async () => {
|
||||||
const { data } = await fetchMe();
|
const { data } = await fetchMe();
|
||||||
user.value = data;
|
user.value = data;
|
||||||
|
|||||||
@@ -20,6 +20,11 @@ export interface LoginRequest {
|
|||||||
ciphertext: string;
|
ciphertext: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface DevLoginRequest {
|
||||||
|
email: string;
|
||||||
|
password: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface LoginResponse {
|
export interface LoginResponse {
|
||||||
access_token: string;
|
access_token: string;
|
||||||
token_type: string;
|
token_type: string;
|
||||||
|
|||||||
@@ -2,6 +2,11 @@ FROM node:20-alpine AS frontend-build
|
|||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
|
ARG VITE_RUNTIME_ENV=production
|
||||||
|
ARG VITE_ALLOW_INSECURE_DEV_LOGIN=false
|
||||||
|
ENV VITE_RUNTIME_ENV=$VITE_RUNTIME_ENV
|
||||||
|
ENV VITE_ALLOW_INSECURE_DEV_LOGIN=$VITE_ALLOW_INSECURE_DEV_LOGIN
|
||||||
|
|
||||||
COPY frontend/package*.json ./
|
COPY frontend/package*.json ./
|
||||||
RUN npm ci
|
RUN npm ci
|
||||||
|
|
||||||
|
|||||||
@@ -384,6 +384,8 @@ write_env_file() {
|
|||||||
{
|
{
|
||||||
printf 'COMPOSE_PROJECT_NAME=%s\n' "$project_name"
|
printf 'COMPOSE_PROJECT_NAME=%s\n' "$project_name"
|
||||||
printf 'ENV=%s\n' "$runtime_env"
|
printf 'ENV=%s\n' "$runtime_env"
|
||||||
|
printf 'VITE_RUNTIME_ENV=%s\n' "$runtime_env"
|
||||||
|
printf 'VITE_ALLOW_INSECURE_DEV_LOGIN=%s\n' "$([[ "$runtime_env" == "development" ]] && printf 'true' || printf 'false')"
|
||||||
printf 'JWT_SECRET_KEY=%s\n' "$jwt_secret"
|
printf 'JWT_SECRET_KEY=%s\n' "$jwt_secret"
|
||||||
printf 'LOGIN_RSA_KEY_ID=%s\n' "$login_key_id"
|
printf 'LOGIN_RSA_KEY_ID=%s\n' "$login_key_id"
|
||||||
printf 'LOGIN_RSA_PRIVATE_KEY=%s\n' "$rsa_private_key"
|
printf 'LOGIN_RSA_PRIVATE_KEY=%s\n' "$rsa_private_key"
|
||||||
@@ -393,6 +395,33 @@ write_env_file() {
|
|||||||
ok ".env 写入完成"
|
ok ".env 写入完成"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
upsert_env_value() {
|
||||||
|
local key="$1" value="$2"
|
||||||
|
local temp_file
|
||||||
|
temp_file="$(mktemp "$ROOT_DIR/.env.tmp.XXXXXX")"
|
||||||
|
if [[ -f "$ENV_FILE" && -n "$(read_env_value "$key" || true)" ]]; then
|
||||||
|
awk -v key="$key" -v value="$value" '
|
||||||
|
BEGIN { prefix = key "=" }
|
||||||
|
index($0, prefix) == 1 { print key "=" value; next }
|
||||||
|
{ print }
|
||||||
|
' "$ENV_FILE" > "$temp_file"
|
||||||
|
else
|
||||||
|
[[ -f "$ENV_FILE" ]] && cat "$ENV_FILE" > "$temp_file"
|
||||||
|
printf '%s=%s\n' "$key" "$value" >> "$temp_file"
|
||||||
|
fi
|
||||||
|
mv "$temp_file" "$ENV_FILE"
|
||||||
|
}
|
||||||
|
|
||||||
|
sync_frontend_build_env() {
|
||||||
|
local runtime_env="$1"
|
||||||
|
local allow_insecure_dev_login="false"
|
||||||
|
[[ "$runtime_env" == "development" ]] && allow_insecure_dev_login="true"
|
||||||
|
|
||||||
|
upsert_env_value VITE_RUNTIME_ENV "$runtime_env"
|
||||||
|
upsert_env_value VITE_ALLOW_INSECURE_DEV_LOGIN "$allow_insecure_dev_login"
|
||||||
|
ok "前端构建环境变量已同步"
|
||||||
|
}
|
||||||
|
|
||||||
prepare_env_file() {
|
prepare_env_file() {
|
||||||
step "准备环境配置文件"
|
step "准备环境配置文件"
|
||||||
local project_name="$1" runtime_env="$2" login_key_id="$3"
|
local project_name="$1" runtime_env="$2" login_key_id="$3"
|
||||||
@@ -568,6 +597,7 @@ main() {
|
|||||||
check_dependencies
|
check_dependencies
|
||||||
confirm_install "$EFFECTIVE_PROJECT_NAME" "$EFFECTIVE_RUNTIME_ENV"
|
confirm_install "$EFFECTIVE_PROJECT_NAME" "$EFFECTIVE_RUNTIME_ENV"
|
||||||
prepare_env_file "$project_name" "$runtime_env" "$login_key_id"
|
prepare_env_file "$project_name" "$runtime_env" "$login_key_id"
|
||||||
|
sync_frontend_build_env "$runtime_env"
|
||||||
run_compose_config
|
run_compose_config
|
||||||
run_backend_init
|
run_backend_init
|
||||||
run_build_and_start
|
run_build_and_start
|
||||||
|
|||||||
Reference in New Issue
Block a user