fix(front): iframe URL runtime fallback + IdNot CORS via same-origin API + scope encoding\n\nfeat(front): inject build/env metadata comment, favicon links\n\nci: docker_tag=ext
Some checks failed
build-and-push-ext / build_push (push) Failing after 5s

This commit is contained in:
Debian Dev4 2025-09-23 17:27:48 +00:00
parent f659362682
commit c2d64fce15
6 changed files with 191 additions and 35 deletions

View File

@ -33,13 +33,13 @@ export default class Auth extends BaseApiService {
? `${window.location.origin}/authorized-client`
: `${variables.FRONT_APP_HOST}/authorized-client`;
// Resolve backend base for calling the state endpoint (prefer explicit BACK_BASE)
const backBase = variables.BACK_BASE || `${variables.BACK_API_PROTOCOL}://${variables.BACK_API_HOST}${variables.BACK_API_PORT ? `:${variables.BACK_API_PORT}` : ''}`;
const stateEndpoint = new URL(`/api/v1/idnot/state`, backBase);
// Use same-origin API (proxied by dev4) to avoid CORS
const apiOrigin = `${variables.BACK_API_PROTOCOL}://${variables.BACK_API_HOST}${variables.BACK_API_PORT ? `:${variables.BACK_API_PORT}` : ''}`;
const stateEndpoint = new URL(`${variables.BACK_API_ROOT_URL || '/api'}/v1/idnot/state`, apiOrigin);
try {
// 1) Ask backend for a signed state that embeds next_url
const resp = await fetch(stateEndpoint.toString(), {
const resp = await fetch(stateEndpoint.toString(), {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ next_url: nextUrl })
@ -56,7 +56,8 @@ export default class Auth extends BaseApiService {
// 2) Build the IdNot authorization URL with fixed redirect_uri and the signed state
const fixedRedirect = variables.IDNOT_REDIRECT_URI_FIXED || 'http://local.4nkweb.com:3000/authorized-client';
const authorizeBase = `${variables.IDNOT_BASE_URL}${variables.IDNOT_AUTHORIZE_ENDPOINT}`;
const authorizeUrl = `${authorizeBase}?client_id=${encodeURIComponent(variables.IDNOT_CLIENT_ID)}&redirect_uri=${encodeURIComponent(fixedRedirect)}&scope=openid,profile&response_type=code&state=${encodeURIComponent(state)}`;
const scopeParam = encodeURIComponent('openid profile');
const authorizeUrl = `${authorizeBase}?client_id=${encodeURIComponent(variables.IDNOT_CLIENT_ID)}&redirect_uri=${encodeURIComponent(fixedRedirect)}&scope=${scopeParam}&response_type=code&state=${encodeURIComponent(state)}`;
console.log('[IDNOT] authorizeUrl', authorizeUrl);
console.log('[IDNOT] state', state);

View File

@ -53,12 +53,9 @@ export default function StepEmail(props: IProps) {
const variables = FrontendVariables.getInstance();
try {
const nextUrl = typeof window !== 'undefined' ? `${window.location.origin}/authorized-client` : `${variables.FRONT_APP_HOST}/authorized-client`;
let backBase = variables.BACK_BASE || (process.env.NEXT_PUBLIC_BACK_BASE as string) || `${variables.BACK_API_PROTOCOL}://${variables.BACK_API_HOST}${variables.BACK_API_PORT ? `:${variables.BACK_API_PORT}` : ''}`;
if (!backBase || !/^https?:\/\//i.test(backBase)) {
console.warn('[IDNOT] BACK_BASE invalid or missing, falling back to https://dev3.4nkweb.com');
backBase = 'https://dev3.4nkweb.com';
}
const stateEndpoint = `${backBase.replace(/\/$/, '')}/api/v1/idnot/state`;
// Always prefer same-origin API to avoid CORS: https://dev4.4nkweb.com/api
const apiBase = `${variables.BACK_API_PROTOCOL}://${variables.BACK_API_HOST}${variables.BACK_API_PORT ? `:${variables.BACK_API_PORT}` : ''}${variables.BACK_API_ROOT_URL || ''}`;
const stateEndpoint = `${apiBase.replace(/\/$/, '')}/v1/idnot/state`;
const resp = await fetch(stateEndpoint, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
@ -75,7 +72,8 @@ export default function StepEmail(props: IProps) {
}
const fixedRedirect = variables.IDNOT_REDIRECT_URI_FIXED || 'http://local.4nkweb.com:3000/authorized-client';
const authorizeBase = `${variables.IDNOT_BASE_URL}${variables.IDNOT_AUTHORIZE_ENDPOINT}`;
const authorizeUrl = `${authorizeBase}?client_id=${encodeURIComponent(variables.IDNOT_CLIENT_ID)}&redirect_uri=${encodeURIComponent(fixedRedirect)}&scope=openid,profile&response_type=code&state=${encodeURIComponent(state)}`;
const scopeParam = encodeURIComponent('openid profile');
const authorizeUrl = `${authorizeBase}?client_id=${encodeURIComponent(variables.IDNOT_CLIENT_ID)}&redirect_uri=${encodeURIComponent(fixedRedirect)}&scope=${scopeParam}&response_type=code&state=${encodeURIComponent(state)}`;
console.log('[IDNOT] authorizeUrl', authorizeUrl);
console.log('[IDNOT] state', state);
router.push(authorizeUrl);

View File

@ -89,14 +89,18 @@ const MyApp = (({
// Configure iframe target and URL on client-side only
useEffect(() => {
setMounted(true);
try {
if (_4nkUrl && typeof _4nkUrl === 'string' && _4nkUrl.trim().length > 0) {
try {
const runtime4nkUrl = (_4nkUrl && _4nkUrl.trim().length > 0)
? _4nkUrl
: ((process as any)?.env?.NEXT_PUBLIC_4NK_URL || (publicRuntimeConfig as any)?.NEXT_PUBLIC_4NK_URL || '');
if (runtime4nkUrl && typeof runtime4nkUrl === 'string' && runtime4nkUrl.trim().length > 0) {
const origin = (() => {
try { return new URL(_4nkUrl).origin; } catch { return _4nkUrl; }
try { return new URL(runtime4nkUrl).origin; } catch { return runtime4nkUrl; }
})();
IframeReference.setTargetOrigin(origin);
const candidate = (publicRuntimeConfig as any).NEXT_PUBLIC_4NK_IFRAME_URL ?? _4nkUrl;
const candidate = (publicRuntimeConfig as any).NEXT_PUBLIC_4NK_IFRAME_URL ?? runtime4nkUrl;
const iframe = (() => {
try { return new URL(candidate).toString(); } catch { return origin; }
})();
@ -147,25 +151,35 @@ const MyApp = (({
}) as AppType;
MyApp.getInitialProps = async () => {
return {
backApiProtocol: publicRuntimeConfig.NEXT_PUBLIC_BACK_API_PROTOCOL,
backApiHost: publicRuntimeConfig.NEXT_PUBLIC_BACK_API_HOST,
backApiPort: publicRuntimeConfig.NEXT_PUBLIC_BACK_API_PORT,
backApiRootUrl: publicRuntimeConfig.NEXT_PUBLIC_BACK_API_ROOT_URL,
backApiVersion: publicRuntimeConfig.NEXT_PUBLIC_BACK_API_VERSION,
frontAppHost: publicRuntimeConfig.NEXT_PUBLIC_FRONT_APP_HOST,
frontAppPort: publicRuntimeConfig.NEXT_PUBLIC_FRONT_APP_PORT,
idNotBaseUrl: publicRuntimeConfig.NEXT_PUBLIC_IDNOT_BASE_URL,
idNotAuthorizeEndpoint: publicRuntimeConfig.NEXT_PUBLIC_IDNOT_AUTHORIZE_ENDPOINT,
idNotClientId: publicRuntimeConfig.NEXT_PUBLIC_IDNOT_CLIENT_ID,
idNotRedirectUri: publicRuntimeConfig.NEXT_PUBLIC_IDNOT_REDIRECT_URI,
fcAuthorizeEndpoint: publicRuntimeConfig.NEXT_PUBLIC_FC_AUTHORIZE_ENDPOINT,
fcClientId: publicRuntimeConfig.NEXT_PUBLIC_FC_CLIENT_ID,
docaposteApiUrl: publicRuntimeConfig.NEXT_PUBLIC_DOCAPOSTE_API_URL,
_4nkUrl: publicRuntimeConfig.NEXT_PUBLIC_4NK_URL,
_4nkIframeUrl: publicRuntimeConfig.NEXT_PUBLIC_4NK_IFRAME_URL,
apiUrl: publicRuntimeConfig.NEXT_PUBLIC_API_URL,
};
// Fallback runtime: use process.env when runtimeConfig is empty (e.g., image built without build args)
const getEnv = (key: string): string => {
const fromRuntime = (publicRuntimeConfig as any)?.[key];
if (typeof fromRuntime === 'string' && fromRuntime.trim().length > 0) {
return fromRuntime;
}
const fromProcess = (process as any)?.env?.[key];
return typeof fromProcess === 'string' ? fromProcess : '';
};
return {
backApiProtocol: getEnv('NEXT_PUBLIC_BACK_API_PROTOCOL'),
backApiHost: getEnv('NEXT_PUBLIC_BACK_API_HOST'),
backApiPort: getEnv('NEXT_PUBLIC_BACK_API_PORT'),
backApiRootUrl: getEnv('NEXT_PUBLIC_BACK_API_ROOT_URL'),
backApiVersion: getEnv('NEXT_PUBLIC_BACK_API_VERSION'),
frontAppHost: getEnv('NEXT_PUBLIC_FRONT_APP_HOST'),
frontAppPort: getEnv('NEXT_PUBLIC_FRONT_APP_PORT'),
idNotBaseUrl: getEnv('NEXT_PUBLIC_IDNOT_BASE_URL'),
idNotAuthorizeEndpoint: getEnv('NEXT_PUBLIC_IDNOT_AUTHORIZE_ENDPOINT'),
idNotClientId: getEnv('NEXT_PUBLIC_IDNOT_CLIENT_ID'),
idNotRedirectUri: getEnv('NEXT_PUBLIC_IDNOT_REDIRECT_URI'),
fcAuthorizeEndpoint: getEnv('NEXT_PUBLIC_FC_AUTHORIZE_ENDPOINT'),
fcClientId: getEnv('NEXT_PUBLIC_FC_CLIENT_ID'),
docaposteApiUrl: getEnv('NEXT_PUBLIC_DOCAPOSTE_API_URL'),
_4nkUrl: getEnv('NEXT_PUBLIC_4NK_URL'),
_4nkIframeUrl: getEnv('NEXT_PUBLIC_4NK_IFRAME_URL'),
apiUrl: getEnv('NEXT_PUBLIC_API_URL'),
};
};
export default MyApp;

63
src/pages/_document.tsx Normal file
View File

@ -0,0 +1,63 @@
import Document, { Html, Head, Main, NextScript, DocumentContext } from 'next/document';
class MyDocument extends Document {
static async getInitialProps(ctx: DocumentContext) {
const initialProps = await Document.getInitialProps(ctx);
// Collect public env vars
const entries = Object.entries(process.env || {})
.filter(([k]) => k.startsWith('NEXT_PUBLIC_'))
.map(([k, v]) => `${k}=${String(v ?? '')}`)
.sort();
// App version from package.json
let appVersion = '';
try {
// eslint-disable-next-line @typescript-eslint/no-var-requires
const pkg = require('../../package.json');
appVersion = pkg?.version || '';
} catch {
appVersion = '';
}
const buildDate = new Date().toISOString();
const dockerTag = process.env.DOCKER_TAG || process.env.IMAGE_TAG || 'ext';
const metaComment = [
'Build Meta:',
`version=${appVersion}`,
`build_date=${buildDate}`,
`docker_tag=${dockerTag}`,
'env:',
...entries,
].join('\n');
return { ...initialProps, metaComment } as any;
}
render() {
// @ts-expect-error injected by getInitialProps
const metaComment: string = (this.props as any).metaComment || '';
return (
<Html>
<Head>
<link rel="icon" href="/favicon.svg" type="image/svg+xml" />
<link rel="alternate icon" href="/favicon.ico" />
<link rel="apple-touch-icon" href="/apple-touch-icon.png" />
</Head>
<body>
{metaComment && (
<div
aria-hidden
dangerouslySetInnerHTML={{ __html: `<!--\n${metaComment}\n-->` }}
/>
)}
<Main />
<NextScript />
</body>
</Html>
);
}
}
export default MyDocument;

33
src/pages/api/env.ts Normal file
View File

@ -0,0 +1,33 @@
import type { NextApiRequest, NextApiResponse } from 'next';
export default function handler(req: NextApiRequest, res: NextApiResponse) {
res.setHeader('Cache-Control', 'no-store, no-cache, must-revalidate, proxy-revalidate');
const pick = (keys: string[]) => keys.reduce<Record<string, string | undefined>>((acc, k) => {
acc[k] = process.env[k];
return acc;
}, {});
const clientKeys = [
'NEXT_PUBLIC_4NK_URL',
'NEXT_PUBLIC_4NK_IFRAME_URL',
'NEXT_PUBLIC_BACK_BASE',
'NEXT_PUBLIC_IDNOT_BASE_URL',
'NEXT_PUBLIC_IDNOT_AUTHORIZE_ENDPOINT',
'NEXT_PUBLIC_IDNOT_CLIENT_ID',
'NEXT_PUBLIC_IDNOT_REDIRECT_URI',
'NEXT_PUBLIC_IDNOT_REDIRECT_URI_FIXED',
'NEXT_PUBLIC_BACK_API_PROTOCOL',
'NEXT_PUBLIC_BACK_API_HOST',
'NEXT_PUBLIC_BACK_API_PORT',
'NEXT_PUBLIC_BACK_API_ROOT_URL',
'NEXT_PUBLIC_BACK_API_VERSION',
'NEXT_PUBLIC_API_URL',
'NEXT_PUBLIC_DEFAULT_VALIDATOR_ID',
'NEXT_PUBLIC_DEFAULT_STORAGE_URLS',
];
res.status(200).json({
time: new Date().toISOString(),
client: pick(clientKeys),
});
}

47
src/pages/env.tsx Normal file
View File

@ -0,0 +1,47 @@
import { GetServerSideProps } from 'next';
import getConfig from 'next/config';
type Props = {
now: string;
env: Record<string, string | undefined>;
runtime: Record<string, any>;
};
export default function EnvPage({ now, env, runtime }: Props) {
return (
<div style={{ fontFamily: 'monospace', padding: 16 }}>
<h2>Runtime environment</h2>
<div>Time: {now}</div>
<h3>process.env (client-safe)</h3>
<pre>{JSON.stringify(env, null, 2)}</pre>
<h3>next/config publicRuntimeConfig</h3>
<pre>{JSON.stringify(runtime.publicRuntimeConfig ?? {}, null, 2)}</pre>
</div>
);
}
export const getServerSideProps: GetServerSideProps<Props> = async ({ res }) => {
res.setHeader('Cache-Control', 'no-store, no-cache, must-revalidate, proxy-revalidate');
const keys = [
'NEXT_PUBLIC_4NK_URL',
'NEXT_PUBLIC_4NK_IFRAME_URL',
'NEXT_PUBLIC_BACK_BASE',
'NEXT_PUBLIC_IDNOT_BASE_URL',
'NEXT_PUBLIC_IDNOT_AUTHORIZE_ENDPOINT',
'NEXT_PUBLIC_IDNOT_CLIENT_ID',
'NEXT_PUBLIC_IDNOT_REDIRECT_URI',
'NEXT_PUBLIC_IDNOT_REDIRECT_URI_FIXED',
'NEXT_PUBLIC_BACK_API_PROTOCOL',
'NEXT_PUBLIC_BACK_API_HOST',
'NEXT_PUBLIC_BACK_API_PORT',
'NEXT_PUBLIC_BACK_API_ROOT_URL',
'NEXT_PUBLIC_BACK_API_VERSION',
'NEXT_PUBLIC_API_URL',
'NEXT_PUBLIC_DEFAULT_VALIDATOR_ID',
'NEXT_PUBLIC_DEFAULT_STORAGE_URLS',
];
const env: Record<string, string | undefined> = {};
for (const k of keys) env[k] = process.env[k];
const runtime = getConfig();
return { props: { now: new Date().toISOString(), env, runtime } };
};