支持开发环境局域网登录测试
This commit is contained in:
@@ -1,4 +1,6 @@
|
||||
# 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_ACCEPT_TIMEOUT_MONTHS=3
|
||||
VITE_STARTUP_ACCEPT_APPROVAL_TIMEOUT_MONTHS=6
|
||||
|
||||
@@ -1,10 +1,20 @@
|
||||
import type { AxiosResponse } 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>> =>
|
||||
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>> =>
|
||||
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";
|
||||
|
||||
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 = {
|
||||
getItem: (key: string) => string | null;
|
||||
setItem: (key: string, value: string) => void;
|
||||
@@ -27,6 +52,30 @@ const createStorage = (): StorageLike => {
|
||||
describe("auth store logout", () => {
|
||||
beforeEach(() => {
|
||||
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", {
|
||||
value: createStorage(),
|
||||
configurable: true,
|
||||
@@ -46,4 +95,36 @@ describe("auth store logout", () => {
|
||||
expect(session.lockReason).toBeNull();
|
||||
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 { 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 { encryptLoginPayload } from "../utils/loginCrypto";
|
||||
import type { UserInfo } from "../types/api";
|
||||
@@ -9,6 +9,9 @@ import { useSessionStore } from "./session";
|
||||
|
||||
export const useAuthStore = defineStore("auth", () => {
|
||||
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 user = ref<UserInfo | null>(null);
|
||||
const loading = ref(false);
|
||||
@@ -17,17 +20,10 @@ export const useAuthStore = defineStore("auth", () => {
|
||||
const login = async (email: string, password: string) => {
|
||||
loading.value = true;
|
||||
try {
|
||||
const { data: loginKey } = await getLoginKey();
|
||||
const ciphertext = await encryptLoginPayload(loginKey.public_key, {
|
||||
email,
|
||||
password,
|
||||
challenge: loginKey.challenge,
|
||||
});
|
||||
const { data } = await apiLogin({
|
||||
key_id: loginKey.key_id,
|
||||
challenge: loginKey.challenge,
|
||||
ciphertext,
|
||||
});
|
||||
const { data } =
|
||||
allowInsecureDevLogin && !window.isSecureContext
|
||||
? await devLogin({ email, password })
|
||||
: await encryptedLogin(email, password);
|
||||
token.value = data.access_token;
|
||||
setToken(data.access_token);
|
||||
// 避免锁屏态下 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 { data } = await fetchMe();
|
||||
user.value = data;
|
||||
|
||||
@@ -20,6 +20,11 @@ export interface LoginRequest {
|
||||
ciphertext: string;
|
||||
}
|
||||
|
||||
export interface DevLoginRequest {
|
||||
email: string;
|
||||
password: string;
|
||||
}
|
||||
|
||||
export interface LoginResponse {
|
||||
access_token: string;
|
||||
token_type: string;
|
||||
|
||||
Reference in New Issue
Block a user