fix and update
This commit is contained in:
@@ -76,6 +76,15 @@ export class FedcmController {
|
|||||||
applyFedcmPreflightHeaders(res, origin);
|
applyFedcmPreflightHeaders(res, origin);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Options('discover.json')
|
||||||
|
@HttpCode(204)
|
||||||
|
discoverPreflight(@Res({ passthrough: true }) res: Response) {
|
||||||
|
res.setHeader('Access-Control-Allow-Origin', '*');
|
||||||
|
res.setHeader('Access-Control-Allow-Methods', 'GET, OPTIONS');
|
||||||
|
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Accept');
|
||||||
|
res.setHeader('Access-Control-Max-Age', '86400');
|
||||||
|
}
|
||||||
|
|
||||||
@Get('config.json')
|
@Get('config.json')
|
||||||
@ApiOperation({ summary: 'FedCM provider config', description: 'Конфигурация Identity Provider для Federated Credential Management API.' })
|
@ApiOperation({ summary: 'FedCM provider config', description: 'Конфигурация Identity Provider для Federated Credential Management API.' })
|
||||||
async config(@Req() req: Request, @Res({ passthrough: true }) res: Response) {
|
async config(@Req() req: Request, @Res({ passthrough: true }) res: Response) {
|
||||||
|
|||||||
@@ -143,7 +143,7 @@ export default function DataPage() {
|
|||||||
const status = await fetchAccountDeletionStatus(userId, token);
|
const status = await fetchAccountDeletionStatus(userId, token);
|
||||||
setDeletionStatus(status);
|
setDeletionStatus(status);
|
||||||
} catch {
|
} catch {
|
||||||
setDeletionStatus(null);
|
// Фоновый сбой gateway не должен сбрасывать уже показанный статус удаления.
|
||||||
}
|
}
|
||||||
}, [isPinLocked, token, userId]);
|
}, [isPinLocked, token, userId]);
|
||||||
|
|
||||||
|
|||||||
@@ -19,23 +19,24 @@ export default function HomePage() {
|
|||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { user, token, isPinLocked } = useAuth();
|
const { user, token, isPinLocked } = useAuth();
|
||||||
const { isReady } = useRequireAuth();
|
const { isReady } = useRequireAuth();
|
||||||
|
const userId = user?.id;
|
||||||
const contactLine = [user?.phone, user?.username ?? user?.email].filter(Boolean).join(' · ');
|
const contactLine = [user?.phone, user?.username ?? user?.email].filter(Boolean).join(' · ');
|
||||||
const [documents, setDocuments] = useState<UserDocument[]>([]);
|
const [documents, setDocuments] = useState<UserDocument[]>([]);
|
||||||
const [activeDocumentType, setActiveDocumentType] = useState<DocumentTypeCode | null>(null);
|
const [activeDocumentType, setActiveDocumentType] = useState<DocumentTypeCode | null>(null);
|
||||||
|
|
||||||
const loadDocuments = useCallback(async () => {
|
const loadDocuments = useCallback(async () => {
|
||||||
if (!user || !token || isPinLocked) return;
|
if (!userId || !token || isPinLocked) return;
|
||||||
try {
|
try {
|
||||||
const response = await apiFetch<{ documents?: UserDocument[] }>(`/documents/users/${user.id}`, {}, token);
|
const response = await apiFetch<{ documents?: UserDocument[] }>(`/documents/users/${userId}`, {}, token);
|
||||||
setDocuments(response.documents ?? []);
|
setDocuments(response.documents ?? []);
|
||||||
} catch {
|
} catch {
|
||||||
setDocuments([]);
|
// Фоновая ошибка не должна очищать уже показанные документы.
|
||||||
}
|
}
|
||||||
}, [isPinLocked, token, user]);
|
}, [isPinLocked, token, userId]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isReady && user && !isPinLocked) void loadDocuments();
|
if (isReady && userId && !isPinLocked) void loadDocuments();
|
||||||
}, [isPinLocked, isReady, loadDocuments, user]);
|
}, [isPinLocked, isReady, loadDocuments, userId]);
|
||||||
|
|
||||||
const documentsByType = useMemo(() => indexDocumentsByType(documents), [documents]);
|
const documentsByType = useMemo(() => indexDocumentsByType(documents), [documents]);
|
||||||
|
|
||||||
|
|||||||
@@ -67,6 +67,7 @@ export default function SecurityPage() {
|
|||||||
const { user, token, refreshProfile } = useAuth();
|
const { user, token, refreshProfile } = useAuth();
|
||||||
const { isReady } = useRequireAuth();
|
const { isReady } = useRequireAuth();
|
||||||
const { showToast } = useToast();
|
const { showToast } = useToast();
|
||||||
|
const userId = user?.id;
|
||||||
const [devices, setDevices] = useState<ActiveDevice[]>([]);
|
const [devices, setDevices] = useState<ActiveDevice[]>([]);
|
||||||
const [sessions, setSessions] = useState<ActiveSession[]>([]);
|
const [sessions, setSessions] = useState<ActiveSession[]>([]);
|
||||||
const [history, setHistory] = useState<SignInEvent[]>([]);
|
const [history, setHistory] = useState<SignInEvent[]>([]);
|
||||||
@@ -92,14 +93,14 @@ export default function SecurityPage() {
|
|||||||
const hasEmailContact = Boolean(user?.email || user?.backupEmail);
|
const hasEmailContact = Boolean(user?.email || user?.backupEmail);
|
||||||
|
|
||||||
const loadSecurity = useCallback(async () => {
|
const loadSecurity = useCallback(async () => {
|
||||||
if (!user || !token) return;
|
if (!userId || !token) return;
|
||||||
setIsSecurityLoading(true);
|
setIsSecurityLoading(true);
|
||||||
try {
|
try {
|
||||||
const [devicesResponse, sessionsResponse, historyResponse, totpResponse] = await Promise.all([
|
const [devicesResponse, sessionsResponse, historyResponse, totpResponse] = await Promise.all([
|
||||||
apiFetch<{ devices: ActiveDevice[] }>(`/security/users/${user.id}/devices`, {}, token),
|
apiFetch<{ devices: ActiveDevice[] }>(`/security/users/${userId}/devices`, {}, token),
|
||||||
apiFetch<{ sessions: ActiveSession[] }>(`/security/users/${user.id}/sessions`, {}, token),
|
apiFetch<{ sessions: ActiveSession[] }>(`/security/users/${userId}/sessions`, {}, token),
|
||||||
apiFetch<{ events: SignInEvent[] }>(`/security/users/${user.id}/sign-in-history`, {}, token),
|
apiFetch<{ events: SignInEvent[] }>(`/security/users/${userId}/sign-in-history`, {}, token),
|
||||||
apiFetch<TotpStatusResponse>(`/security/users/${user.id}/totp/status`, {}, token)
|
apiFetch<TotpStatusResponse>(`/security/users/${userId}/totp/status`, {}, token)
|
||||||
]);
|
]);
|
||||||
setDevices(devicesResponse.devices ?? []);
|
setDevices(devicesResponse.devices ?? []);
|
||||||
setSessions(sessionsResponse.sessions ?? []);
|
setSessions(sessionsResponse.sessions ?? []);
|
||||||
@@ -107,16 +108,17 @@ export default function SecurityPage() {
|
|||||||
setCurrentSessionId(window.localStorage.getItem(AUTH_SESSION_KEY));
|
setCurrentSessionId(window.localStorage.getItem(AUTH_SESSION_KEY));
|
||||||
setTotpEnabled(Boolean(totpResponse.isEnabled));
|
setTotpEnabled(Boolean(totpResponse.isEnabled));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
showToast(error instanceof Error ? error.message : 'Не удалось загрузить безопасность');
|
const message = getApiErrorMessage(error, 'Не удалось загрузить безопасность');
|
||||||
|
if (message) showToast(message);
|
||||||
} finally {
|
} finally {
|
||||||
setIsSecurityLoading(false);
|
setIsSecurityLoading(false);
|
||||||
}
|
}
|
||||||
}, [showToast, token, user]);
|
}, [showToast, token, userId]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isReady || !user) return;
|
if (!isReady || !userId) return;
|
||||||
void loadSecurity();
|
void loadSecurity();
|
||||||
}, [isReady, loadSecurity, user]);
|
}, [isReady, loadSecurity, userId]);
|
||||||
|
|
||||||
async function revokeSession(sessionId: string) {
|
async function revokeSession(sessionId: string) {
|
||||||
if (!user || !token) return;
|
if (!user || !token) return;
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { usePathname, useRouter } from 'next/navigation';
|
|||||||
import {
|
import {
|
||||||
apiFetch,
|
apiFetch,
|
||||||
ApiError,
|
ApiError,
|
||||||
|
AUTH_SESSION_INVALID_EVENT,
|
||||||
AUTH_REFRESH_KEY,
|
AUTH_REFRESH_KEY,
|
||||||
AUTH_SESSION_KEY,
|
AUTH_SESSION_KEY,
|
||||||
AUTH_TOKEN_KEY,
|
AUTH_TOKEN_KEY,
|
||||||
@@ -106,14 +107,12 @@ function applySessionState(
|
|||||||
}
|
}
|
||||||
|
|
||||||
function readInitialAuthState() {
|
function readInitialAuthState() {
|
||||||
if (typeof window === 'undefined') {
|
// Первичный render должен совпадать на сервере и в браузере. Токены из
|
||||||
return { token: null as string | null, hasStoredSession: false };
|
// localStorage читаются уже в refreshProfile(), иначе авторизованная вкладка
|
||||||
}
|
// получает hydration mismatch (React #418) ещё до запуска эффектов.
|
||||||
const token = window.localStorage.getItem(AUTH_TOKEN_KEY);
|
|
||||||
const refresh = window.localStorage.getItem(AUTH_REFRESH_KEY);
|
|
||||||
return {
|
return {
|
||||||
token,
|
token: null as string | null,
|
||||||
hasStoredSession: Boolean(refresh || token)
|
hasStoredSession: false
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -274,6 +273,16 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
|
|||||||
router.push('/auth/login');
|
router.push('/auth/login');
|
||||||
}, [clearPinLock, router]);
|
}, [clearPinLock, router]);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
function handleInvalidSession() {
|
||||||
|
if (!window.localStorage.getItem(AUTH_TOKEN_KEY) && !window.localStorage.getItem(AUTH_REFRESH_KEY)) return;
|
||||||
|
logout();
|
||||||
|
}
|
||||||
|
|
||||||
|
window.addEventListener(AUTH_SESSION_INVALID_EVENT, handleInvalidSession);
|
||||||
|
return () => window.removeEventListener(AUTH_SESSION_INVALID_EVENT, handleInvalidSession);
|
||||||
|
}, [logout]);
|
||||||
|
|
||||||
const applyUserPatch = React.useCallback((patch: Partial<PublicUser>) => {
|
const applyUserPatch = React.useCallback((patch: Partial<PublicUser>) => {
|
||||||
setUser((current) => {
|
setUser((current) => {
|
||||||
if (!current) return current;
|
if (!current) return current;
|
||||||
@@ -291,7 +300,7 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
|
|||||||
const task: Promise<void> = (async () => {
|
const task: Promise<void> = (async () => {
|
||||||
const currentToken = window.localStorage.getItem(AUTH_TOKEN_KEY);
|
const currentToken = window.localStorage.getItem(AUTH_TOKEN_KEY);
|
||||||
const refreshToken = window.localStorage.getItem(AUTH_REFRESH_KEY);
|
const refreshToken = window.localStorage.getItem(AUTH_REFRESH_KEY);
|
||||||
setHasStoredSession(Boolean(refreshToken));
|
setHasStoredSession(Boolean(currentToken || refreshToken));
|
||||||
|
|
||||||
if (!currentToken && !refreshToken) {
|
if (!currentToken && !refreshToken) {
|
||||||
window.localStorage.removeItem(AUTH_USER_CACHE_KEY);
|
window.localStorage.removeItem(AUTH_USER_CACHE_KEY);
|
||||||
|
|||||||
@@ -598,7 +598,10 @@ async function trySilentTokenRefresh(): Promise<RefreshSessionResponse | null> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
return refreshed;
|
return refreshed;
|
||||||
} catch {
|
} catch (error) {
|
||||||
|
if (error instanceof ApiError && error.status === 401) {
|
||||||
|
notifyAuthSessionInvalid();
|
||||||
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -685,6 +688,8 @@ const API_BURST_MAX_CONCURRENT = 2;
|
|||||||
const API_BURST_WINDOW_MS = 5000;
|
const API_BURST_WINDOW_MS = 5000;
|
||||||
const API_READY_EVENT = 'idp-api-ready';
|
const API_READY_EVENT = 'idp-api-ready';
|
||||||
const API_NOT_READY_EVENT = 'idp-api-not-ready';
|
const API_NOT_READY_EVENT = 'idp-api-not-ready';
|
||||||
|
export const AUTH_SESSION_INVALID_EVENT = 'idp-auth-session-invalid';
|
||||||
|
const GATEWAY_CIRCUIT_COOLDOWN_MS = 8_000;
|
||||||
|
|
||||||
function sleep(ms: number): Promise<void> {
|
function sleep(ms: number): Promise<void> {
|
||||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||||
@@ -694,6 +699,7 @@ let gatewayReadyResolved = false;
|
|||||||
let gatewayReadyPromise: Promise<void> | null = null;
|
let gatewayReadyPromise: Promise<void> | null = null;
|
||||||
let stableGatewayPromise: Promise<void> | null = null;
|
let stableGatewayPromise: Promise<void> | null = null;
|
||||||
let gatewayReadyAt = 0;
|
let gatewayReadyAt = 0;
|
||||||
|
let gatewayCircuitOpenUntil = 0;
|
||||||
let activeApiRequests = 0;
|
let activeApiRequests = 0;
|
||||||
const apiRequestWaiters: Array<() => void> = [];
|
const apiRequestWaiters: Array<() => void> = [];
|
||||||
|
|
||||||
@@ -701,6 +707,7 @@ export function resetApiGatewayWarmup() {
|
|||||||
gatewayReadyResolved = false;
|
gatewayReadyResolved = false;
|
||||||
gatewayReadyPromise = null;
|
gatewayReadyPromise = null;
|
||||||
stableGatewayPromise = null;
|
stableGatewayPromise = null;
|
||||||
|
gatewayCircuitOpenUntil = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function invalidateApiGatewayReady() {
|
export function invalidateApiGatewayReady() {
|
||||||
@@ -712,6 +719,29 @@ export function invalidateApiGatewayReady() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function openGatewayCircuit(): void {
|
||||||
|
gatewayReadyResolved = false;
|
||||||
|
gatewayReadyAt = 0;
|
||||||
|
stableGatewayPromise = null;
|
||||||
|
gatewayCircuitOpenUntil = Date.now() + GATEWAY_CIRCUIT_COOLDOWN_MS;
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
window.dispatchEvent(new CustomEvent(API_NOT_READY_EVENT));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function assertGatewayCircuitClosed(): void {
|
||||||
|
if (typeof window === 'undefined') return;
|
||||||
|
if (Date.now() < gatewayCircuitOpenUntil) {
|
||||||
|
throw new ApiError('Сервер API временно недоступен. Запрос отложен, чтобы не перегружать консоль ошибками.', 503, 'GATEWAY_UNAVAILABLE');
|
||||||
|
}
|
||||||
|
gatewayCircuitOpenUntil = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
function notifyAuthSessionInvalid(): void {
|
||||||
|
if (typeof window === 'undefined') return;
|
||||||
|
window.dispatchEvent(new CustomEvent(AUTH_SESSION_INVALID_EVENT));
|
||||||
|
}
|
||||||
|
|
||||||
export function isApiGatewayReady() {
|
export function isApiGatewayReady() {
|
||||||
return gatewayReadyResolved;
|
return gatewayReadyResolved;
|
||||||
}
|
}
|
||||||
@@ -720,6 +750,7 @@ export function markApiGatewayReady() {
|
|||||||
if (gatewayReadyResolved) return;
|
if (gatewayReadyResolved) return;
|
||||||
gatewayReadyResolved = true;
|
gatewayReadyResolved = true;
|
||||||
gatewayReadyAt = Date.now();
|
gatewayReadyAt = Date.now();
|
||||||
|
gatewayCircuitOpenUntil = 0;
|
||||||
if (typeof window !== 'undefined') {
|
if (typeof window !== 'undefined') {
|
||||||
window.dispatchEvent(new CustomEvent(API_READY_EVENT));
|
window.dispatchEvent(new CustomEvent(API_READY_EVENT));
|
||||||
}
|
}
|
||||||
@@ -816,6 +847,7 @@ export async function ensureApiGatewayReady(force = false): Promise<void> {
|
|||||||
gatewayReadyResolved = false;
|
gatewayReadyResolved = false;
|
||||||
gatewayReadyAt = 0;
|
gatewayReadyAt = 0;
|
||||||
stableGatewayPromise = null;
|
stableGatewayPromise = null;
|
||||||
|
gatewayCircuitOpenUntil = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
const probes = force ? 2 : 1;
|
const probes = force ? 2 : 1;
|
||||||
@@ -835,6 +867,7 @@ async function fetchWithGatewayRetry(url: string, init: RequestInit): Promise<Re
|
|||||||
let lastNetworkError: unknown;
|
let lastNetworkError: unknown;
|
||||||
for (let attempt = 0; attempt < GATEWAY_RETRY_ATTEMPTS; attempt++) {
|
for (let attempt = 0; attempt < GATEWAY_RETRY_ATTEMPTS; attempt++) {
|
||||||
try {
|
try {
|
||||||
|
assertGatewayCircuitClosed();
|
||||||
const response = await fetch(url, init);
|
const response = await fetch(url, init);
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
markApiGatewayReady();
|
markApiGatewayReady();
|
||||||
@@ -843,16 +876,24 @@ async function fetchWithGatewayRetry(url: string, init: RequestInit): Promise<Re
|
|||||||
await sleep(gatewayRetryDelay(attempt));
|
await sleep(gatewayRetryDelay(attempt));
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
if (GATEWAY_RETRY_STATUS.has(response.status)) {
|
||||||
|
openGatewayCircuit();
|
||||||
|
}
|
||||||
return response;
|
return response;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
lastNetworkError = error;
|
lastNetworkError = error;
|
||||||
|
if (isGatewayUnavailableError(error)) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
if (attempt + 1 < GATEWAY_RETRY_ATTEMPTS) {
|
if (attempt + 1 < GATEWAY_RETRY_ATTEMPTS) {
|
||||||
await sleep(gatewayRetryDelay(attempt));
|
await sleep(gatewayRetryDelay(attempt));
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
openGatewayCircuit();
|
||||||
throw mapFetchError(error);
|
throw mapFetchError(error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
openGatewayCircuit();
|
||||||
throw mapFetchError(lastNetworkError);
|
throw mapFetchError(lastNetworkError);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -871,6 +912,13 @@ export async function apiFetch<T>(path: string, options: RequestInit = {}, token
|
|||||||
};
|
};
|
||||||
|
|
||||||
let response: Response;
|
let response: Response;
|
||||||
|
let apiSlotReleased = false;
|
||||||
|
const releaseCurrentApiSlot = () => {
|
||||||
|
if (apiSlotReleased) return;
|
||||||
|
apiSlotReleased = true;
|
||||||
|
releaseApiSlot();
|
||||||
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
try {
|
try {
|
||||||
response = await fetchWithGatewayRetry(`${getApiUrl()}${path}`, {
|
response = await fetchWithGatewayRetry(`${getApiUrl()}${path}`, {
|
||||||
@@ -892,10 +940,15 @@ export async function apiFetch<T>(path: string, options: RequestInit = {}, token
|
|||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (resolvedToken && error.status === 401 && error.code !== 'TOKEN_EXPIRED') {
|
||||||
|
notifyAuthSessionInvalid();
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
if (allowRetry && resolvedToken && error.code === 'TOKEN_EXPIRED') {
|
if (allowRetry && resolvedToken && error.code === 'TOKEN_EXPIRED') {
|
||||||
const refreshed = await trySilentTokenRefresh();
|
const refreshed = await trySilentTokenRefresh();
|
||||||
if (refreshed?.accessToken) {
|
if (refreshed?.accessToken) {
|
||||||
releaseApiSlot();
|
releaseCurrentApiSlot();
|
||||||
return apiFetch<T>(path, options, refreshed.accessToken, false);
|
return apiFetch<T>(path, options, refreshed.accessToken, false);
|
||||||
}
|
}
|
||||||
if (refreshed?.requiresPin) {
|
if (refreshed?.requiresPin) {
|
||||||
@@ -903,13 +956,17 @@ export async function apiFetch<T>(path: string, options: RequestInit = {}, token
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (resolvedToken && error.status === 401) {
|
||||||
|
notifyAuthSessionInvalid();
|
||||||
|
}
|
||||||
|
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
|
|
||||||
markApiGatewayReady();
|
markApiGatewayReady();
|
||||||
return response.json() as Promise<T>;
|
return response.json() as Promise<T>;
|
||||||
} finally {
|
} finally {
|
||||||
releaseApiSlot();
|
releaseCurrentApiSlot();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
11
install.sh
11
install.sh
@@ -2336,12 +2336,13 @@ write_nginx_site_api() {
|
|||||||
local domain="$1"
|
local domain="$1"
|
||||||
local ssl_type="${2:-none}"
|
local ssl_type="${2:-none}"
|
||||||
local ws_on_api="${3:-true}"
|
local ws_on_api="${3:-true}"
|
||||||
local conf ws_block http_preamble ssl_block api_upstream api_location
|
local conf ws_block http_preamble ssl_block api_upstream api_location server_dns
|
||||||
conf="$(nginx_conf_target api)"
|
conf="$(nginx_conf_target api)"
|
||||||
ws_block="$(build_ws_block "$ws_on_api")"
|
ws_block="$(build_ws_block "$ws_on_api")"
|
||||||
http_preamble="$(nginx_http_server_preamble "$domain" "$ssl_type")"
|
http_preamble="$(nginx_http_server_preamble "$domain" "$ssl_type")"
|
||||||
api_upstream="$(build_nginx_api_upstream_block)"
|
api_upstream="$(build_nginx_api_upstream_block)"
|
||||||
api_location="$(build_nginx_api_location_block)"
|
api_location="$(build_nginx_api_location_block)"
|
||||||
|
server_dns="$(nginx_docker_server_dns_block)"
|
||||||
|
|
||||||
if [[ "$ssl_type" == "none" ]]; then
|
if [[ "$ssl_type" == "none" ]]; then
|
||||||
nginx_write_conf "$conf" <<EOF
|
nginx_write_conf "$conf" <<EOF
|
||||||
@@ -2351,6 +2352,7 @@ server {
|
|||||||
listen [::]:80;
|
listen [::]:80;
|
||||||
server_name ${domain};
|
server_name ${domain};
|
||||||
client_max_body_size 100m;
|
client_max_body_size 100m;
|
||||||
|
${server_dns}
|
||||||
${ws_block}
|
${ws_block}
|
||||||
${api_location}
|
${api_location}
|
||||||
}
|
}
|
||||||
@@ -2365,6 +2367,7 @@ ${http_preamble}server {
|
|||||||
server_name ${domain};
|
server_name ${domain};
|
||||||
${ssl_block}
|
${ssl_block}
|
||||||
client_max_body_size 100m;
|
client_max_body_size 100m;
|
||||||
|
${server_dns}
|
||||||
${ws_block}
|
${ws_block}
|
||||||
${api_location}
|
${api_location}
|
||||||
}
|
}
|
||||||
@@ -2713,6 +2716,9 @@ validate_docker_api_nginx_config() {
|
|||||||
if grep -q 'proxy_pass.*\$request_uri' "$conf" 2>/dev/null; then
|
if grep -q 'proxy_pass.*\$request_uri' "$conf" 2>/dev/null; then
|
||||||
fail "Конфиг ${conf} содержит proxy_pass \$request_uri — нужен ./install.sh --fix-all (502 на / без resolver)"
|
fail "Конфиг ${conf} содержит proxy_pass \$request_uri — нужен ./install.sh --fix-all (502 на / без resolver)"
|
||||||
fi
|
fi
|
||||||
|
if grep -q 'proxy_pass \$' "$conf" 2>/dev/null && ! grep -q 'resolver 127\.0\.0\.11' "$conf" 2>/dev/null; then
|
||||||
|
fail "Конфиг ${conf} использует variable proxy_pass без Docker resolver — будет 502 на api.idpmvk.lpr. Выполните ./install.sh --fix-all"
|
||||||
|
fi
|
||||||
ok "Конфиг API-домена: proxy_pass → api-gateway:3000"
|
ok "Конфиг API-домена: proxy_pass → api-gateway:3000"
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2743,6 +2749,9 @@ validate_all_docker_nginx_proxy_pass() {
|
|||||||
if grep -q 'proxy_pass.*\$request_uri' "$conf" 2>/dev/null; then
|
if grep -q 'proxy_pass.*\$request_uri' "$conf" 2>/dev/null; then
|
||||||
fail "Конфиг $(basename "$conf") содержит proxy_pass \$request_uri — 502 «no resolver defined» на /. Выполните ./install.sh --fix-all"
|
fail "Конфиг $(basename "$conf") содержит proxy_pass \$request_uri — 502 «no resolver defined» на /. Выполните ./install.sh --fix-all"
|
||||||
fi
|
fi
|
||||||
|
if grep -q 'proxy_pass \$' "$conf" 2>/dev/null && ! grep -q 'resolver 127\.0\.0\.11' "$conf" 2>/dev/null; then
|
||||||
|
fail "Конфиг $(basename "$conf") использует variable proxy_pass без resolver 127.0.0.11 — после recreate контейнеров будут 502"
|
||||||
|
fi
|
||||||
done
|
done
|
||||||
shopt -u nullglob
|
shopt -u nullglob
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user