Feat build new
This commit is contained in:
+461
-9
@@ -83398,6 +83398,8 @@ var require_env = __commonJS({
|
||||
Object.defineProperty(exports2, "__esModule", { value: true });
|
||||
exports2.envOr = envOr;
|
||||
exports2.envOrNull = envOrNull;
|
||||
exports2.envList = envList;
|
||||
exports2.pickExactUri = pickExactUri;
|
||||
function envOr(name, fallback) {
|
||||
const raw = process.env[name];
|
||||
const trimmed = typeof raw === "string" ? raw.trim() : "";
|
||||
@@ -83408,6 +83410,18 @@ var require_env = __commonJS({
|
||||
const trimmed = typeof raw === "string" ? raw.trim() : "";
|
||||
return trimmed === "" ? null : trimmed;
|
||||
}
|
||||
function envList(name) {
|
||||
const raw = envOrNull(name);
|
||||
if (!raw)
|
||||
return [];
|
||||
return raw.split(",").map((item) => item.trim()).filter(Boolean);
|
||||
}
|
||||
function pickExactUri(requested, allowed) {
|
||||
const unique = [...new Set(allowed.filter((item) => Boolean(item)))];
|
||||
if (requested)
|
||||
return unique.includes(requested) ? requested : null;
|
||||
return unique[0] ?? null;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -83492,6 +83506,9 @@ var require_iam_service = __commonJS({
|
||||
body.tenant_slug = tenantSlug;
|
||||
if (clientIp)
|
||||
body.client_ip = clientIp;
|
||||
const returnTo = (0, env_12.envOrNull)("IAM_ADMIN_BROWSER_RETURN_URL") || (0, env_12.envOrNull)("IAM_BROWSER_RETURN_URL");
|
||||
if (returnTo)
|
||||
body.return_to = returnTo;
|
||||
const timeout = Number((0, env_12.envOr)("IAM_HTTP_TIMEOUT_MS", "8000"));
|
||||
let response;
|
||||
try {
|
||||
@@ -83519,7 +83536,8 @@ var require_iam_service = __commonJS({
|
||||
phoneNumber: user.phone_number ?? null,
|
||||
firstName: user.first_name ?? null,
|
||||
lastName: user.last_name ?? null,
|
||||
sessionId: payload.session_id ?? null
|
||||
sessionId: payload.session_id ?? null,
|
||||
browserTicket: typeof payload.browser_ticket === "string" ? payload.browser_ticket : null
|
||||
};
|
||||
}
|
||||
if ([401, 403, 422].includes(response.status)) {
|
||||
@@ -83541,9 +83559,8 @@ var require_iam_service = __commonJS({
|
||||
* and no way for a central sign-out to end it.
|
||||
*
|
||||
* The IAM only accepts this from a trusted confidential client carrying the
|
||||
* explicit `passwordless_delegation_allowed` flag, and it never creates an
|
||||
* account: an unknown user is reported as such so this app can carry on with
|
||||
* its own local login.
|
||||
* explicit `passwordless_delegation_allowed` flag. An unknown mobile is
|
||||
* created there. A locked or inactive account is still refused.
|
||||
*/
|
||||
async attestOtpLogin(identifier, clientIp, factorReference) {
|
||||
const body = {
|
||||
@@ -83562,6 +83579,9 @@ var require_iam_service = __commonJS({
|
||||
body.client_ip = clientIp;
|
||||
if (factorReference)
|
||||
body.factor_reference = factorReference;
|
||||
const returnTo = (0, env_12.envOrNull)("IAM_BROWSER_RETURN_URL");
|
||||
if (returnTo)
|
||||
body.return_to = returnTo;
|
||||
const timeout = Number((0, env_12.envOr)("IAM_HTTP_TIMEOUT_MS", "8000"));
|
||||
let response;
|
||||
try {
|
||||
@@ -83586,7 +83606,8 @@ var require_iam_service = __commonJS({
|
||||
phoneNumber: user.phone_number ?? null,
|
||||
firstName: user.first_name ?? null,
|
||||
lastName: user.last_name ?? null,
|
||||
sessionId: payload.session_id ?? null
|
||||
sessionId: payload.session_id ?? null,
|
||||
browserTicket: typeof payload.browser_ticket === "string" ? payload.browser_ticket : null
|
||||
};
|
||||
}
|
||||
const code = String(payload.error ?? "");
|
||||
@@ -83599,11 +83620,63 @@ var require_iam_service = __commonJS({
|
||||
this.logger.warn(`identity service returned ${response.status} for an OTP attestation`);
|
||||
throw new IamUnavailableError(`identity service returned ${response.status}`);
|
||||
}
|
||||
browserBootstrapUrl(ticket) {
|
||||
if (!ticket || !this.issuer)
|
||||
return null;
|
||||
return `${this.issuer}/auth/bootstrap?ticket=${encodeURIComponent(ticket)}`;
|
||||
}
|
||||
/** تبادل کد مجوز مرورگر. پاسخ از خود issuer و با راز کلاینت آمده است. */
|
||||
async exchangeAuthorizationCode(code, codeVerifier, redirectUri) {
|
||||
const timeout = Number((0, env_12.envOr)("IAM_HTTP_TIMEOUT_MS", "8000"));
|
||||
const body = new URLSearchParams({
|
||||
grant_type: "authorization_code",
|
||||
code,
|
||||
redirect_uri: redirectUri,
|
||||
code_verifier: codeVerifier,
|
||||
client_id: this.clientId,
|
||||
client_secret: (0, env_12.envOrNull)("IAM_CLIENT_SECRET")
|
||||
});
|
||||
let response;
|
||||
try {
|
||||
response = await fetch(`${this.issuer}/oauth/token`, {
|
||||
method: "POST",
|
||||
headers: { "content-type": "application/x-www-form-urlencoded", accept: "application/json" },
|
||||
body,
|
||||
signal: AbortSignal.timeout(timeout)
|
||||
});
|
||||
} catch (error) {
|
||||
throw new IamUnavailableError(`identity service unreachable: ${error.message}`);
|
||||
}
|
||||
const payload = await response.json().catch(() => ({}));
|
||||
if (!response.ok || typeof payload.id_token !== "string") {
|
||||
throw new IamUnavailableError(`identity service refused the authorization code (${response.status})`);
|
||||
}
|
||||
const claims = decodeIdToken(payload.id_token);
|
||||
if (claims.iss !== this.issuer) {
|
||||
throw new IamUnavailableError("identity token issuer does not match");
|
||||
}
|
||||
const aud = claims.aud;
|
||||
const audiences = Array.isArray(aud) ? aud : [aud];
|
||||
if (!audiences.includes(this.clientId)) {
|
||||
throw new IamUnavailableError("identity token audience does not match");
|
||||
}
|
||||
return {
|
||||
sub: String(claims.sub ?? ""),
|
||||
phoneNumber: typeof claims.phone_number === "string" ? claims.phone_number : null,
|
||||
sessionId: typeof claims.sid === "string" ? claims.sid : null
|
||||
};
|
||||
}
|
||||
};
|
||||
exports2.IamService = IamService;
|
||||
exports2.IamService = IamService = IamService_1 = __decorate4([
|
||||
(0, common_12.Injectable)()
|
||||
], IamService);
|
||||
function decodeIdToken(token) {
|
||||
const part = token.split(".")[1];
|
||||
if (!part)
|
||||
throw new IamUnavailableError("identity token is malformed");
|
||||
return JSON.parse(Buffer.from(part, "base64url").toString("utf8"));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -84291,6 +84364,7 @@ var require_customer_auth_service = __commonJS({
|
||||
exports2.masterOtpCode = masterOtpCode;
|
||||
exports2.masterOtpEnabled = masterOtpEnabled;
|
||||
exports2.customerJwtSecret = customerJwtSecret;
|
||||
var node_crypto_1 = require("node:crypto");
|
||||
var common_12 = require_common();
|
||||
var jwt_1 = (init_dist(), __toCommonJS(dist_exports));
|
||||
var client_1 = require("@prisma/client");
|
||||
@@ -84320,6 +84394,15 @@ var require_customer_auth_service = __commonJS({
|
||||
function masterOtpEnabled() {
|
||||
return masterOtpCode() !== null;
|
||||
}
|
||||
function mobileVariants(raw) {
|
||||
const digits = raw.replace(/\D/g, "");
|
||||
const variants = /* @__PURE__ */ new Set([raw.trim(), digits]);
|
||||
if (digits.startsWith("98") && digits.length >= 12)
|
||||
variants.add(`0${digits.slice(2)}`);
|
||||
if (digits.startsWith("0") && digits.length === 11)
|
||||
variants.add(`98${digits.slice(1)}`);
|
||||
return [...variants].filter((value) => value !== "");
|
||||
}
|
||||
function maskMobile(mobile) {
|
||||
return mobile.length < 7 ? null : `${mobile.slice(0, 4)}***${mobile.slice(-4)}`;
|
||||
}
|
||||
@@ -84561,15 +84644,15 @@ var require_customer_auth_service = __commonJS({
|
||||
const identity = await this.attestToIam(mobile, ip);
|
||||
const expert = await this.resolveExpertPrincipal(mobile);
|
||||
if (expert) {
|
||||
return {
|
||||
return this.withBootstrap({
|
||||
token: await this.issueExpertToken(expert.adminUserId, mobile, { identity, ip }),
|
||||
customer: { id: expert.tradingCustomerId, mobile }
|
||||
};
|
||||
}, identity);
|
||||
}
|
||||
return {
|
||||
return this.withBootstrap({
|
||||
token: await this.issueToken(challenge.customer.id, challenge.customer.mobile, { identity, ip }),
|
||||
customer: { id: challenge.customer.id, mobile: challenge.customer.mobile }
|
||||
};
|
||||
}, identity);
|
||||
}
|
||||
/**
|
||||
* D21 — is this mobile an active sales expert with a usable trading account?
|
||||
@@ -84639,6 +84722,129 @@ var require_customer_auth_service = __commonJS({
|
||||
* Reference reused api_token forever once set (:60-62); a fresh token is
|
||||
* issued on every verify here (T1).
|
||||
*/
|
||||
/**
|
||||
* شروع بررسی بیصدای نشست IAM. کوکی امضاشده code_verifier را نگه میدارد
|
||||
* تا در آدرس بازگشت دیده نشود.
|
||||
*/
|
||||
beginBrowserSso(requestedRedirect) {
|
||||
if (!this.iam.enabled)
|
||||
return null;
|
||||
const redirectUri = (0, env_12.pickExactUri)(requestedRedirect, [
|
||||
(0, env_12.envOrNull)("IAM_REDIRECT_URI"),
|
||||
...(0, env_12.envList)("IAM_REDIRECT_URIS")
|
||||
]);
|
||||
const clientId = (0, env_12.envOrNull)("IAM_CLIENT_ID");
|
||||
if (!redirectUri || !clientId)
|
||||
return null;
|
||||
const state = (0, node_crypto_1.randomBytes)(24).toString("base64url");
|
||||
const nonce = (0, node_crypto_1.randomBytes)(24).toString("base64url");
|
||||
const verifier = (0, node_crypto_1.randomBytes)(48).toString("base64url");
|
||||
const challenge = (0, node_crypto_1.createHash)("sha256").update(verifier).digest("base64url");
|
||||
const payload = Buffer.from(JSON.stringify({ state, nonce, verifier, redirectUri })).toString("base64url");
|
||||
const sig = (0, node_crypto_1.createHmac)("sha256", this.ssoSecret()).update(payload).digest("base64url");
|
||||
const secure = redirectUri.startsWith("https:");
|
||||
const cookie = `gb_iam_sso=${payload}.${sig}; HttpOnly; Path=/; Max-Age=180; SameSite=Lax` + (secure ? "; Secure" : "");
|
||||
const url = new URL(`${this.iam.issuer}/oauth/authorize`);
|
||||
url.searchParams.set("response_type", "code");
|
||||
url.searchParams.set("client_id", clientId);
|
||||
url.searchParams.set("redirect_uri", redirectUri);
|
||||
url.searchParams.set("scope", "openid profile email");
|
||||
url.searchParams.set("state", state);
|
||||
url.searchParams.set("nonce", nonce);
|
||||
url.searchParams.set("code_challenge", challenge);
|
||||
url.searchParams.set("code_challenge_method", "S256");
|
||||
url.searchParams.set("prompt", "none");
|
||||
return { authorizeUrl: url.toString(), cookie };
|
||||
}
|
||||
async completeBrowserSso(cookieValue, code, state, ip) {
|
||||
const pending = this.readSsoCookie(cookieValue);
|
||||
if (!pending || pending.state !== state) {
|
||||
throw new common_12.UnauthorizedException("\u062F\u0631\u062E\u0648\u0627\u0633\u062A \u0648\u0631\u0648\u062F \u0645\u0634\u062A\u0631\u06A9 \u0645\u0639\u062A\u0628\u0631 \u0646\u06CC\u0633\u062A");
|
||||
}
|
||||
const claims = await this.iam.exchangeAuthorizationCode(code, pending.verifier, pending.redirectUri);
|
||||
if (!claims.phoneNumber || !claims.sub) {
|
||||
throw new common_12.UnauthorizedException("\u0647\u0648\u06CC\u062A \u0645\u0634\u062A\u0631\u06A9 \u0634\u0645\u0627\u0631\u0647 \u0645\u0648\u0628\u0627\u06CC\u0644 \u0646\u062F\u0627\u0631\u062F");
|
||||
}
|
||||
const mobiles = mobileVariants(claims.phoneNumber);
|
||||
let expert = null;
|
||||
let expertMobile = claims.phoneNumber;
|
||||
for (const candidate of mobiles) {
|
||||
expert = await this.resolveExpertPrincipal(candidate);
|
||||
if (expert) {
|
||||
expertMobile = candidate;
|
||||
break;
|
||||
}
|
||||
}
|
||||
const identity = {
|
||||
sub: claims.sub,
|
||||
email: null,
|
||||
phoneNumber: claims.phoneNumber,
|
||||
firstName: null,
|
||||
lastName: null,
|
||||
sessionId: claims.sessionId,
|
||||
browserTicket: null
|
||||
};
|
||||
if (expert) {
|
||||
return {
|
||||
token: await this.issueExpertToken(expert.adminUserId, expertMobile, { identity, ip }),
|
||||
customer: { id: expert.tradingCustomerId, mobile: expertMobile }
|
||||
};
|
||||
}
|
||||
const customer = await this.prisma.customer.findFirst({
|
||||
where: { mobile: { in: mobiles }, isActive: true, isArchived: false },
|
||||
select: { id: true, mobile: true }
|
||||
});
|
||||
if (!customer) {
|
||||
throw new common_12.UnauthorizedException("\u062D\u0633\u0627\u0628\u06CC \u0628\u0627 \u0627\u06CC\u0646 \u0634\u0645\u0627\u0631\u0647 \u062F\u0631 \u0627\u06CC\u0646 \u0633\u0627\u0645\u0627\u0646\u0647 \u0646\u06CC\u0633\u062A");
|
||||
}
|
||||
return {
|
||||
token: await this.issueToken(customer.id, customer.mobile, { identity, ip }),
|
||||
customer: { id: customer.id, mobile: customer.mobile }
|
||||
};
|
||||
}
|
||||
centralLogoutUrl(requestedReturn) {
|
||||
if (!this.iam.enabled)
|
||||
return null;
|
||||
const redirect = (0, env_12.pickExactUri)(requestedReturn, [
|
||||
(0, env_12.envOrNull)("IAM_POST_LOGOUT_REDIRECT_URI"),
|
||||
(0, env_12.envOrNull)("IAM_BROWSER_RETURN_URL"),
|
||||
...(0, env_12.envList)("IAM_ALLOWED_RETURN_URLS")
|
||||
]);
|
||||
const clientId = (0, env_12.envOrNull)("IAM_CLIENT_ID");
|
||||
if (!redirect || !clientId)
|
||||
return null;
|
||||
const url = new URL(`${this.iam.issuer}/oauth/logout`);
|
||||
url.searchParams.set("client_id", clientId);
|
||||
url.searchParams.set("post_logout_redirect_uri", redirect);
|
||||
return url.toString();
|
||||
}
|
||||
withBootstrap(body, identity) {
|
||||
const url = this.iam.browserBootstrapUrl(identity?.browserTicket);
|
||||
return url ? { ...body, iamBootstrapUrl: url } : body;
|
||||
}
|
||||
ssoSecret() {
|
||||
return (0, env_12.envOrNull)("JWT_CUSTOMER_SECRET") || (0, env_12.envOrNull)("JWT_SECRET") || (0, env_12.envOr)("IAM_CLIENT_SECRET", "iam-sso");
|
||||
}
|
||||
readSsoCookie(value) {
|
||||
if (!value || !value.includes("."))
|
||||
return null;
|
||||
const split = value.lastIndexOf(".");
|
||||
const payload = value.slice(0, split);
|
||||
const sig = value.slice(split + 1);
|
||||
const expected = (0, node_crypto_1.createHmac)("sha256", this.ssoSecret()).update(payload).digest("base64url");
|
||||
if (expected.length !== sig.length)
|
||||
return null;
|
||||
let same = 0;
|
||||
for (let i = 0; i < expected.length; i++)
|
||||
same |= expected.charCodeAt(i) ^ sig.charCodeAt(i);
|
||||
if (same !== 0)
|
||||
return null;
|
||||
try {
|
||||
return JSON.parse(Buffer.from(payload, "base64url").toString("utf8"));
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
async issueToken(customerId, mobile, context = {}) {
|
||||
const jti = this.sessions.newTokenId();
|
||||
const payload = {
|
||||
@@ -84880,6 +85086,21 @@ var require_customer_auth_controller = __commonJS({
|
||||
verifyOtp(dto, ip) {
|
||||
return this.auth.verifyOtp(dto.username, dto.password, ip);
|
||||
}
|
||||
/** اگر نشست IAM در مرورگر باشد، آدرس authorize با prompt=none برمیگردد. */
|
||||
startSso(redirectUri, res) {
|
||||
const started = this.auth.beginBrowserSso(redirectUri);
|
||||
if (!started)
|
||||
return { enabled: false, authorize_url: null };
|
||||
res.setHeader("Set-Cookie", started.cookie);
|
||||
return { enabled: true, authorize_url: started.authorizeUrl };
|
||||
}
|
||||
ssoCallback(body, cookieHeader, ip, res) {
|
||||
res.setHeader("Set-Cookie", "gb_iam_sso=; HttpOnly; Path=/; Max-Age=0; SameSite=Lax");
|
||||
return this.auth.completeBrowserSso(readCookie(cookieHeader, "gb_iam_sso"), body.code ?? "", body.state ?? "", ip);
|
||||
}
|
||||
ssoLogoutUrl(returnUrl) {
|
||||
return { logout_url: this.auth.centralLogoutUrl(returnUrl) };
|
||||
}
|
||||
me(me) {
|
||||
return this.customers.getOrThrow(me.id);
|
||||
}
|
||||
@@ -84911,6 +85132,35 @@ var require_customer_auth_controller = __commonJS({
|
||||
__metadata3("design:paramtypes", [verify_otp_dto_1.VerifyOtpDto, String]),
|
||||
__metadata3("design:returntype", void 0)
|
||||
], CustomerAuthController.prototype, "verifyOtp", null);
|
||||
__decorate4([
|
||||
(0, guards_1.Public)(),
|
||||
(0, common_12.Get)("auth/sso/start"),
|
||||
__param3(0, (0, common_12.Query)("redirect_uri")),
|
||||
__param3(1, (0, common_12.Res)({ passthrough: true })),
|
||||
__metadata3("design:type", Function),
|
||||
__metadata3("design:paramtypes", [Object, Object]),
|
||||
__metadata3("design:returntype", void 0)
|
||||
], CustomerAuthController.prototype, "startSso", null);
|
||||
__decorate4([
|
||||
(0, guards_1.Public)(),
|
||||
(0, common_12.Post)("auth/sso/callback"),
|
||||
(0, common_12.HttpCode)(200),
|
||||
__param3(0, (0, common_12.Body)()),
|
||||
__param3(1, (0, common_12.Headers)("cookie")),
|
||||
__param3(2, (0, common_12.Ip)()),
|
||||
__param3(3, (0, common_12.Res)({ passthrough: true })),
|
||||
__metadata3("design:type", Function),
|
||||
__metadata3("design:paramtypes", [Object, Object, String, Object]),
|
||||
__metadata3("design:returntype", void 0)
|
||||
], CustomerAuthController.prototype, "ssoCallback", null);
|
||||
__decorate4([
|
||||
(0, guards_1.Public)(),
|
||||
(0, common_12.Get)("auth/sso/logout-url"),
|
||||
__param3(0, (0, common_12.Query)("return_url")),
|
||||
__metadata3("design:type", Function),
|
||||
__metadata3("design:paramtypes", [String]),
|
||||
__metadata3("design:returntype", void 0)
|
||||
], CustomerAuthController.prototype, "ssoLogoutUrl", null);
|
||||
__decorate4([
|
||||
(0, customer_guards_1.CustomerAuth)(),
|
||||
(0, common_12.Get)("me"),
|
||||
@@ -84942,6 +85192,16 @@ var require_customer_auth_controller = __commonJS({
|
||||
customers_service_1.CustomersService
|
||||
])
|
||||
], CustomerAuthController);
|
||||
function readCookie(header, name) {
|
||||
if (!header)
|
||||
return null;
|
||||
for (const part of header.split(";")) {
|
||||
const [key, ...rest] = part.trim().split("=");
|
||||
if (key === name)
|
||||
return decodeURIComponent(rest.join("="));
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -87469,12 +87729,14 @@ var require_auth_service = __commonJS({
|
||||
var AuthService_1;
|
||||
Object.defineProperty(exports2, "__esModule", { value: true });
|
||||
exports2.AuthService = void 0;
|
||||
var node_crypto_1 = require("node:crypto");
|
||||
var common_12 = require_common();
|
||||
var jwt_1 = (init_dist(), __toCommonJS(dist_exports));
|
||||
var bcrypt = __importStar2(require_umd());
|
||||
var prisma_service_1 = require_prisma_service();
|
||||
var iam_service_1 = require_iam_service();
|
||||
var admin_session_service_1 = require_admin_session_service();
|
||||
var env_12 = require_env();
|
||||
var AuthService = AuthService_1 = class AuthService {
|
||||
prisma;
|
||||
jwt;
|
||||
@@ -87529,6 +87791,110 @@ var require_auth_service = __commonJS({
|
||||
userAgent: userAgent ?? null
|
||||
});
|
||||
}
|
||||
const bootstrap2 = this.iam.browserBootstrapUrl(verified.identity?.browserTicket);
|
||||
return {
|
||||
id: user.id,
|
||||
name: user.fullName,
|
||||
fullName: user.fullName,
|
||||
mobile: user.mobile,
|
||||
role: user.role,
|
||||
ut: user.userTypeId,
|
||||
branch_id: user.branchId,
|
||||
api_token: apiToken,
|
||||
roles: (user.userType?.permissions ?? []).map((p) => ({ rid: p.rid })),
|
||||
...bootstrap2 ? { iamBootstrapUrl: bootstrap2 } : {}
|
||||
};
|
||||
}
|
||||
beginBrowserSso(requestedRedirect) {
|
||||
if (!this.iam.enabled)
|
||||
return null;
|
||||
const redirectUri = (0, env_12.pickExactUri)(requestedRedirect, [
|
||||
(0, env_12.envOrNull)("IAM_ADMIN_REDIRECT_URI"),
|
||||
...(0, env_12.envList)("IAM_ADMIN_REDIRECT_URIS")
|
||||
]);
|
||||
const clientId = (0, env_12.envOrNull)("IAM_CLIENT_ID");
|
||||
if (!redirectUri || !clientId)
|
||||
return null;
|
||||
const state = (0, node_crypto_1.randomBytes)(24).toString("base64url");
|
||||
const nonce = (0, node_crypto_1.randomBytes)(24).toString("base64url");
|
||||
const verifier = (0, node_crypto_1.randomBytes)(48).toString("base64url");
|
||||
const challenge = (0, node_crypto_1.createHash)("sha256").update(verifier).digest("base64url");
|
||||
const payload = Buffer.from(JSON.stringify({ state, nonce, verifier, redirectUri })).toString("base64url");
|
||||
const sig = (0, node_crypto_1.createHmac)("sha256", this.ssoSecret()).update(payload).digest("base64url");
|
||||
const secure = redirectUri.startsWith("https:");
|
||||
const cookie = `gb_admin_iam_sso=${payload}.${sig}; HttpOnly; Path=/; Max-Age=180; SameSite=Lax` + (secure ? "; Secure" : "");
|
||||
const url = new URL(`${this.iam.issuer}/oauth/authorize`);
|
||||
url.searchParams.set("response_type", "code");
|
||||
url.searchParams.set("client_id", clientId);
|
||||
url.searchParams.set("redirect_uri", redirectUri);
|
||||
url.searchParams.set("scope", "openid profile email");
|
||||
url.searchParams.set("state", state);
|
||||
url.searchParams.set("nonce", nonce);
|
||||
url.searchParams.set("code_challenge", challenge);
|
||||
url.searchParams.set("code_challenge_method", "S256");
|
||||
url.searchParams.set("prompt", "none");
|
||||
return { authorizeUrl: url.toString(), cookie };
|
||||
}
|
||||
async completeBrowserSso(cookieValue, code, state, ip, userAgent) {
|
||||
const pending = this.readSsoCookie(cookieValue);
|
||||
if (!pending || pending.state !== state) {
|
||||
throw new common_12.UnauthorizedException("\u062F\u0631\u062E\u0648\u0627\u0633\u062A \u0648\u0631\u0648\u062F \u0645\u0634\u062A\u0631\u06A9 \u0645\u0639\u062A\u0628\u0631 \u0646\u06CC\u0633\u062A");
|
||||
}
|
||||
const claims = await this.iam.exchangeAuthorizationCode(code, pending.verifier, pending.redirectUri);
|
||||
if (!claims.phoneNumber) {
|
||||
throw new common_12.UnauthorizedException("\u0647\u0648\u06CC\u062A \u0645\u0634\u062A\u0631\u06A9 \u0634\u0645\u0627\u0631\u0647 \u0645\u0648\u0628\u0627\u06CC\u0644 \u0646\u062F\u0627\u0631\u062F");
|
||||
}
|
||||
const user = await this.prisma.adminUser.findFirst({
|
||||
where: { mobile: { in: mobileVariants(claims.phoneNumber) }, isActive: true },
|
||||
include: { userType: { include: { permissions: true } } }
|
||||
});
|
||||
if (!user) {
|
||||
throw new common_12.UnauthorizedException("\u062D\u0633\u0627\u0628 \u0645\u062F\u06CC\u0631\u06CC \u0628\u0627 \u0627\u06CC\u0646 \u0634\u0645\u0627\u0631\u0647 \u062F\u0631 \u0627\u06CC\u0646 \u0633\u0627\u0645\u0627\u0646\u0647 \u0646\u06CC\u0633\u062A");
|
||||
}
|
||||
const identity = {
|
||||
sub: claims.sub,
|
||||
email: null,
|
||||
phoneNumber: claims.phoneNumber,
|
||||
firstName: null,
|
||||
lastName: null,
|
||||
sessionId: claims.sessionId,
|
||||
browserTicket: null
|
||||
};
|
||||
return this.issueAdminToken(user, identity, ip, userAgent);
|
||||
}
|
||||
centralLogoutUrl(requestedReturn) {
|
||||
if (!this.iam.enabled)
|
||||
return null;
|
||||
const redirect = (0, env_12.pickExactUri)(requestedReturn, [
|
||||
(0, env_12.envOrNull)("IAM_ADMIN_POST_LOGOUT_REDIRECT_URI"),
|
||||
(0, env_12.envOrNull)("IAM_ADMIN_BROWSER_RETURN_URL"),
|
||||
...(0, env_12.envList)("IAM_ADMIN_ALLOWED_RETURN_URLS")
|
||||
]);
|
||||
const clientId = (0, env_12.envOrNull)("IAM_CLIENT_ID");
|
||||
if (!redirect || !clientId)
|
||||
return null;
|
||||
const url = new URL(`${this.iam.issuer}/oauth/logout`);
|
||||
url.searchParams.set("client_id", clientId);
|
||||
url.searchParams.set("post_logout_redirect_uri", redirect);
|
||||
return url.toString();
|
||||
}
|
||||
async issueAdminToken(user, identity, ip, userAgent) {
|
||||
const jti = this.sessions.newTokenId();
|
||||
const apiToken = await this.jwt.signAsync({
|
||||
sub: user.id,
|
||||
mobile: user.mobile,
|
||||
...jti ? { jti } : {}
|
||||
});
|
||||
if (jti) {
|
||||
await this.sessions.record({
|
||||
jti,
|
||||
adminUserId: user.id,
|
||||
iamSessionId: identity?.sessionId ?? null,
|
||||
iamSub: identity?.sub ?? null,
|
||||
ipAddress: ip ?? null,
|
||||
userAgent: userAgent ?? null
|
||||
});
|
||||
}
|
||||
return {
|
||||
id: user.id,
|
||||
name: user.fullName,
|
||||
@@ -87541,6 +87907,29 @@ var require_auth_service = __commonJS({
|
||||
roles: (user.userType?.permissions ?? []).map((p) => ({ rid: p.rid }))
|
||||
};
|
||||
}
|
||||
ssoSecret() {
|
||||
return (0, env_12.envOrNull)("JWT_SECRET") || (0, env_12.envOr)("IAM_CLIENT_SECRET", "iam-sso");
|
||||
}
|
||||
readSsoCookie(value) {
|
||||
if (!value || !value.includes("."))
|
||||
return null;
|
||||
const split = value.lastIndexOf(".");
|
||||
const payload = value.slice(0, split);
|
||||
const sig = value.slice(split + 1);
|
||||
const expected = (0, node_crypto_1.createHmac)("sha256", this.ssoSecret()).update(payload).digest("base64url");
|
||||
if (expected.length !== sig.length)
|
||||
return null;
|
||||
let same = 0;
|
||||
for (let i = 0; i < expected.length; i++)
|
||||
same |= expected.charCodeAt(i) ^ sig.charCodeAt(i);
|
||||
if (same !== 0)
|
||||
return null;
|
||||
try {
|
||||
return JSON.parse(Buffer.from(payload, "base64url").toString("utf8"));
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Password check, optionally delegated to the central identity service.
|
||||
*
|
||||
@@ -87612,6 +88001,15 @@ var require_auth_service = __commonJS({
|
||||
admin_session_service_1.AdminSessionService
|
||||
])
|
||||
], AuthService);
|
||||
function mobileVariants(raw) {
|
||||
const digits = raw.replace(/\D/g, "");
|
||||
const variants = /* @__PURE__ */ new Set([raw.trim(), digits]);
|
||||
if (digits.startsWith("98") && digits.length >= 12)
|
||||
variants.add(`0${digits.slice(2)}`);
|
||||
if (digits.startsWith("0") && digits.length === 11)
|
||||
variants.add(`98${digits.slice(1)}`);
|
||||
return [...variants].filter((value) => value !== "");
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -87681,6 +88079,20 @@ var require_auth_controller = __commonJS({
|
||||
login(dto, ip, userAgent) {
|
||||
return this.authService.login(dto, ip, userAgent);
|
||||
}
|
||||
startSso(redirectUri, res) {
|
||||
const started = this.authService.beginBrowserSso(redirectUri);
|
||||
if (!started)
|
||||
return { enabled: false, authorize_url: null };
|
||||
res.setHeader("Set-Cookie", started.cookie);
|
||||
return { enabled: true, authorize_url: started.authorizeUrl };
|
||||
}
|
||||
ssoCallback(body, cookieHeader, ip, userAgent, res) {
|
||||
res.setHeader("Set-Cookie", "gb_admin_iam_sso=; HttpOnly; Path=/; Max-Age=0; SameSite=Lax");
|
||||
return this.authService.completeBrowserSso(readCookie(cookieHeader, "gb_admin_iam_sso"), body.code ?? "", body.state ?? "", ip, userAgent);
|
||||
}
|
||||
ssoLogoutUrl(returnUrl) {
|
||||
return { logout_url: this.authService.centralLogoutUrl(returnUrl) };
|
||||
}
|
||||
/** Legacy `users/show` — the router guard's permission refresh. */
|
||||
me(admin) {
|
||||
return this.authService.showData(admin.id);
|
||||
@@ -87698,6 +88110,36 @@ var require_auth_controller = __commonJS({
|
||||
__metadata3("design:paramtypes", [login_dto_1.LoginDto, String, String]),
|
||||
__metadata3("design:returntype", void 0)
|
||||
], AuthController.prototype, "login", null);
|
||||
__decorate4([
|
||||
(0, guards_1.Public)(),
|
||||
(0, common_12.Get)("sso/start"),
|
||||
__param3(0, (0, common_12.Query)("redirect_uri")),
|
||||
__param3(1, (0, common_12.Res)({ passthrough: true })),
|
||||
__metadata3("design:type", Function),
|
||||
__metadata3("design:paramtypes", [Object, Object]),
|
||||
__metadata3("design:returntype", void 0)
|
||||
], AuthController.prototype, "startSso", null);
|
||||
__decorate4([
|
||||
(0, guards_1.Public)(),
|
||||
(0, common_12.Post)("sso/callback"),
|
||||
(0, common_12.HttpCode)(200),
|
||||
__param3(0, (0, common_12.Body)()),
|
||||
__param3(1, (0, common_12.Headers)("cookie")),
|
||||
__param3(2, (0, common_12.Ip)()),
|
||||
__param3(3, (0, common_12.Headers)("user-agent")),
|
||||
__param3(4, (0, common_12.Res)({ passthrough: true })),
|
||||
__metadata3("design:type", Function),
|
||||
__metadata3("design:paramtypes", [Object, Object, String, Object, Object]),
|
||||
__metadata3("design:returntype", void 0)
|
||||
], AuthController.prototype, "ssoCallback", null);
|
||||
__decorate4([
|
||||
(0, guards_1.Public)(),
|
||||
(0, common_12.Get)("sso/logout-url"),
|
||||
__param3(0, (0, common_12.Query)("return_url")),
|
||||
__metadata3("design:type", Function),
|
||||
__metadata3("design:paramtypes", [String]),
|
||||
__metadata3("design:returntype", void 0)
|
||||
], AuthController.prototype, "ssoLogoutUrl", null);
|
||||
__decorate4([
|
||||
(0, common_12.Get)("me"),
|
||||
__param3(0, (0, guards_1.CurrentAdmin)()),
|
||||
@@ -87709,6 +88151,16 @@ var require_auth_controller = __commonJS({
|
||||
(0, common_12.Controller)("admin/auth"),
|
||||
__metadata3("design:paramtypes", [auth_service_1.AuthService])
|
||||
], AuthController);
|
||||
function readCookie(header, name) {
|
||||
if (!header)
|
||||
return null;
|
||||
for (const part of header.split(";")) {
|
||||
const [key, ...rest] = part.trim().split("=");
|
||||
if (key === name)
|
||||
return decodeURIComponent(rest.join("="));
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user