feat: harden auth and study workflows

- replace plaintext login and unlock requests with RSA-OAEP/AES-GCM encrypted payloads

- add login challenge replay protection, production RSA key validation, and auth tests

- wire compose to environment-driven dev/prod settings without committing local secrets

- update setup-config smoke scripts and Postman docs for encrypted login

- add visit schedule migrations/tests and update study/subject setup workflows
This commit is contained in:
Cheng Zhou
2026-05-08 22:13:12 +08:00
parent a7bbcaa5dc
commit 74feca4467
47 changed files with 2423 additions and 534 deletions
+4 -1
View File
@@ -1,10 +1,13 @@
import type { AxiosResponse } from "axios";
import api, { apiGet, apiPatch, apiPost } from "./axios";
import type { UserMeResponse, LoginRequest, LoginResponse, RegisterRequest } from "../types/api";
import type { UserMeResponse, LoginRequest, LoginResponse, LoginKeyResponse, RegisterRequest } from "../types/api";
export const login = (payload: LoginRequest): Promise<AxiosResponse<LoginResponse>> =>
apiPost<LoginResponse>("/api/v1/auth/login", payload);
export const getLoginKey = (): Promise<AxiosResponse<LoginKeyResponse>> =>
apiGet<LoginKeyResponse>("/api/v1/auth/login-key");
export const fetchMe = (): Promise<AxiosResponse<UserMeResponse>> => apiGet<UserMeResponse>("/api/v1/auth/me");
export const register = (payload: RegisterRequest): Promise<AxiosResponse<{ message: string }>> =>
+17 -1
View File
@@ -16,6 +16,19 @@ export type UnlockResponse = {
expiresAt: string;
};
export type LoginKeyResponse = {
key_id: string;
public_key: string;
challenge: string;
expires_at: string;
};
export type EncryptedPasswordRequest = {
key_id: string;
challenge: string;
ciphertext: string;
};
export const extendToken = (token: string): Promise<AxiosResponse<ExtendResponse>> =>
authClient.post<ExtendResponse>(
"/api/v1/auth/extend",
@@ -27,7 +40,10 @@ export const extendToken = (token: string): Promise<AxiosResponse<ExtendResponse
}
);
export const unlockSession = (payload: { email: string; password: string }): Promise<AxiosResponse<UnlockResponse>> =>
export const getLoginKey = (): Promise<AxiosResponse<LoginKeyResponse>> =>
authClient.get<LoginKeyResponse>("/api/v1/auth/login-key");
export const unlockSession = (payload: EncryptedPasswordRequest): Promise<AxiosResponse<UnlockResponse>> =>
authClient.post<UnlockResponse>("/api/v1/auth/unlock", payload);
export default authClient;