Compare commits
3
Commits
be241d4eea
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a07698b17f | ||
|
|
407511ea13 | ||
|
|
41cb084479 |
No files matched your search
@@ -65,6 +65,7 @@ jobs:
|
|||||||
SSO_TOKEN_ENDPOINT: ${{ vars.SSO_TOKEN_ENDPOINT }}
|
SSO_TOKEN_ENDPOINT: ${{ vars.SSO_TOKEN_ENDPOINT }}
|
||||||
SSO_USERINFO_ENDPOINT: ${{ vars.SSO_USERINFO_ENDPOINT }}
|
SSO_USERINFO_ENDPOINT: ${{ vars.SSO_USERINFO_ENDPOINT }}
|
||||||
SSO_SCOPE: ${{ vars.SSO_SCOPE }}
|
SSO_SCOPE: ${{ vars.SSO_SCOPE }}
|
||||||
|
SSO_DEBUG_USERINFO: ${{ vars.SSO_DEBUG_USERINFO }}
|
||||||
SSO_CLIENT_ID_VAR: ${{ vars.SSO_CLIENT_ID }}
|
SSO_CLIENT_ID_VAR: ${{ vars.SSO_CLIENT_ID }}
|
||||||
SSO_CLIENT_ID_SECRET: ${{ secrets.SSO_CLIENT_ID }}
|
SSO_CLIENT_ID_SECRET: ${{ secrets.SSO_CLIENT_ID }}
|
||||||
SUPPORT_TENANT_ID: ${{ vars.SUPPORT_TENANT_ID }}
|
SUPPORT_TENANT_ID: ${{ vars.SUPPORT_TENANT_ID }}
|
||||||
@@ -87,7 +88,8 @@ jobs:
|
|||||||
sso_authorization_endpoint="${SSO_AUTHORIZATION_ENDPOINT:-https://sso.hmac.kr/oidc/oauth2/auth}"
|
sso_authorization_endpoint="${SSO_AUTHORIZATION_ENDPOINT:-https://sso.hmac.kr/oidc/oauth2/auth}"
|
||||||
sso_token_endpoint="${SSO_TOKEN_ENDPOINT:-https://sso.hmac.kr/oidc/oauth2/token}"
|
sso_token_endpoint="${SSO_TOKEN_ENDPOINT:-https://sso.hmac.kr/oidc/oauth2/token}"
|
||||||
sso_userinfo_endpoint="${SSO_USERINFO_ENDPOINT:-https://sso.hmac.kr/oidc/userinfo}"
|
sso_userinfo_endpoint="${SSO_USERINFO_ENDPOINT:-https://sso.hmac.kr/oidc/userinfo}"
|
||||||
sso_scope="${SSO_SCOPE:-openid profile email}"
|
sso_scope="${SSO_SCOPE:-openid profile email tenants}"
|
||||||
|
sso_debug_userinfo="${SSO_DEBUG_USERINFO:-false}"
|
||||||
sso_client_id="${SSO_CLIENT_ID_SECRET:-${SSO_CLIENT_ID_VAR:-}}"
|
sso_client_id="${SSO_CLIENT_ID_SECRET:-${SSO_CLIENT_ID_VAR:-}}"
|
||||||
sso_client_secret="$SSO_CLIENT_SECRET"
|
sso_client_secret="$SSO_CLIENT_SECRET"
|
||||||
jwt_secret="$JWT_SECRET"
|
jwt_secret="$JWT_SECRET"
|
||||||
@@ -148,6 +150,7 @@ jobs:
|
|||||||
sso_token_endpoint_b64="$(base64_value "$sso_token_endpoint")"
|
sso_token_endpoint_b64="$(base64_value "$sso_token_endpoint")"
|
||||||
sso_userinfo_endpoint_b64="$(base64_value "$sso_userinfo_endpoint")"
|
sso_userinfo_endpoint_b64="$(base64_value "$sso_userinfo_endpoint")"
|
||||||
sso_scope_b64="$(base64_value "$sso_scope")"
|
sso_scope_b64="$(base64_value "$sso_scope")"
|
||||||
|
sso_debug_userinfo_b64="$(base64_value "$sso_debug_userinfo")"
|
||||||
sso_client_id_b64="$(base64_value "$sso_client_id")"
|
sso_client_id_b64="$(base64_value "$sso_client_id")"
|
||||||
sso_client_secret_b64="$(base64_value "$sso_client_secret")"
|
sso_client_secret_b64="$(base64_value "$sso_client_secret")"
|
||||||
jwt_secret_b64="$(base64_value "$jwt_secret")"
|
jwt_secret_b64="$(base64_value "$jwt_secret")"
|
||||||
@@ -172,6 +175,7 @@ jobs:
|
|||||||
export SSO_TOKEN_ENDPOINT="\$(printf '%s' '$sso_token_endpoint_b64' | base64 -d)"
|
export SSO_TOKEN_ENDPOINT="\$(printf '%s' '$sso_token_endpoint_b64' | base64 -d)"
|
||||||
export SSO_USERINFO_ENDPOINT="\$(printf '%s' '$sso_userinfo_endpoint_b64' | base64 -d)"
|
export SSO_USERINFO_ENDPOINT="\$(printf '%s' '$sso_userinfo_endpoint_b64' | base64 -d)"
|
||||||
export SSO_SCOPE="\$(printf '%s' '$sso_scope_b64' | base64 -d)"
|
export SSO_SCOPE="\$(printf '%s' '$sso_scope_b64' | base64 -d)"
|
||||||
|
export SSO_DEBUG_USERINFO="\$(printf '%s' '$sso_debug_userinfo_b64' | base64 -d)"
|
||||||
export SSO_CLIENT_ID="\$(printf '%s' '$sso_client_id_b64' | base64 -d)"
|
export SSO_CLIENT_ID="\$(printf '%s' '$sso_client_id_b64' | base64 -d)"
|
||||||
export SSO_CLIENT_SECRET="\$(printf '%s' '$sso_client_secret_b64' | base64 -d)"
|
export SSO_CLIENT_SECRET="\$(printf '%s' '$sso_client_secret_b64' | base64 -d)"
|
||||||
export JWT_SECRET="\$(printf '%s' '$jwt_secret_b64' | base64 -d)"
|
export JWT_SECRET="\$(printf '%s' '$jwt_secret_b64' | base64 -d)"
|
||||||
|
|||||||
@@ -0,0 +1,43 @@
|
|||||||
|
"""store all SSO tenant memberships on support tickets"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from alembic import op
|
||||||
|
from sqlalchemy import inspect
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
revision = "0017_ticket_requester_tenant_ids"
|
||||||
|
down_revision = "0016_feedback_status_stages"
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
bind = op.get_bind()
|
||||||
|
columns = {
|
||||||
|
column["name"]
|
||||||
|
for column in inspect(bind).get_columns("support_tickets")
|
||||||
|
}
|
||||||
|
if "requester_tenant_ids" not in columns:
|
||||||
|
op.add_column(
|
||||||
|
"support_tickets",
|
||||||
|
sa.Column("requester_tenant_ids", sa.JSON(), nullable=True),
|
||||||
|
)
|
||||||
|
op.execute(
|
||||||
|
"""
|
||||||
|
UPDATE support_tickets
|
||||||
|
SET requester_tenant_ids = JSON_ARRAY(requester_tenant_id)
|
||||||
|
WHERE requester_tenant_ids IS NULL
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
bind = op.get_bind()
|
||||||
|
columns = {
|
||||||
|
column["name"]
|
||||||
|
for column in inspect(bind).get_columns("support_tickets")
|
||||||
|
}
|
||||||
|
if "requester_tenant_ids" in columns:
|
||||||
|
op.drop_column("support_tickets", "requester_tenant_ids")
|
||||||
@@ -209,6 +209,7 @@ def get_my_access(
|
|||||||
return {
|
return {
|
||||||
"user_id": principal.user_id,
|
"user_id": principal.user_id,
|
||||||
"tenant_id": principal.tenant_id,
|
"tenant_id": principal.tenant_id,
|
||||||
|
"tenant_ids": principal.tenant_ids,
|
||||||
"email": principal.email,
|
"email": principal.email,
|
||||||
"name": principal.name,
|
"name": principal.name,
|
||||||
"department": principal.department,
|
"department": principal.department,
|
||||||
|
|||||||
@@ -276,6 +276,7 @@ async def create_ticket(
|
|||||||
# because empty values fail min_length validation first.
|
# because empty values fail min_length validation first.
|
||||||
requester_id=principal.user_id,
|
requester_id=principal.user_id,
|
||||||
requester_tenant_id=principal.tenant_id,
|
requester_tenant_id=principal.tenant_id,
|
||||||
|
requester_tenant_ids=principal.tenant_ids,
|
||||||
requester_contact=str(form.get("requester_contact") or ""),
|
requester_contact=str(form.get("requester_contact") or ""),
|
||||||
title=str(form.get("title") or ""),
|
title=str(form.get("title") or ""),
|
||||||
description=str(form.get("description") or ""),
|
description=str(form.get("description") or ""),
|
||||||
@@ -288,6 +289,7 @@ async def create_ticket(
|
|||||||
request_payload = request_payload.model_copy(update={
|
request_payload = request_payload.model_copy(update={
|
||||||
"requester_id": principal.user_id,
|
"requester_id": principal.user_id,
|
||||||
"requester_tenant_id": principal.tenant_id,
|
"requester_tenant_id": principal.tenant_id,
|
||||||
|
"requester_tenant_ids": principal.tenant_ids,
|
||||||
"requester_email": principal.email,
|
"requester_email": principal.email,
|
||||||
"requester_name": principal.name,
|
"requester_name": principal.name,
|
||||||
"requester_department": principal.department,
|
"requester_department": principal.department,
|
||||||
@@ -300,6 +302,7 @@ async def create_ticket(
|
|||||||
"workspace_code": workspace_code,
|
"workspace_code": workspace_code,
|
||||||
"requester_id": principal.user_id,
|
"requester_id": principal.user_id,
|
||||||
"requester_tenant_id": principal.tenant_id,
|
"requester_tenant_id": principal.tenant_id,
|
||||||
|
"requester_tenant_ids": principal.tenant_ids,
|
||||||
"requester_email": principal.email,
|
"requester_email": principal.email,
|
||||||
"requester_name": principal.name,
|
"requester_name": principal.name,
|
||||||
"requester_department": principal.department,
|
"requester_department": principal.department,
|
||||||
|
|||||||
@@ -144,6 +144,7 @@ class SupportTicket(Base):
|
|||||||
workspace_id: Mapped[int] = mapped_column(ForeignKey("workspaces.id"))
|
workspace_id: Mapped[int] = mapped_column(ForeignKey("workspaces.id"))
|
||||||
requester_id: Mapped[str] = mapped_column(String(100))
|
requester_id: Mapped[str] = mapped_column(String(100))
|
||||||
requester_tenant_id: Mapped[str] = mapped_column(String(100))
|
requester_tenant_id: Mapped[str] = mapped_column(String(100))
|
||||||
|
requester_tenant_ids: Mapped[list[str] | None] = mapped_column(JSON, nullable=True)
|
||||||
requester_contact: Mapped[str | None] = mapped_column(String(100), nullable=True)
|
requester_contact: Mapped[str | None] = mapped_column(String(100), nullable=True)
|
||||||
requester_email: Mapped[str | None] = mapped_column(String(320), nullable=True)
|
requester_email: Mapped[str | None] = mapped_column(String(320), nullable=True)
|
||||||
requester_name: Mapped[str | None] = mapped_column(String(100), nullable=True)
|
requester_name: Mapped[str | None] = mapped_column(String(100), nullable=True)
|
||||||
@@ -323,4 +324,4 @@ class Attachment(Base):
|
|||||||
|
|
||||||
ticket: Mapped[SupportTicket] = relationship(back_populates="attachments")
|
ticket: Mapped[SupportTicket] = relationship(back_populates="attachments")
|
||||||
comment: Mapped[TicketComment | None] = relationship(back_populates="attachments")
|
comment: Mapped[TicketComment | None] = relationship(back_populates="attachments")
|
||||||
workspace: Mapped[Workspace] = relationship(back_populates="attachments")
|
workspace: Mapped[Workspace] = relationship(back_populates="attachments")
|
||||||
@@ -7,6 +7,7 @@ class TicketCreateRequest(BaseModel):
|
|||||||
workspace_code: str = Field(default="", max_length=50)
|
workspace_code: str = Field(default="", max_length=50)
|
||||||
requester_id: str = Field(..., min_length=1, max_length=100)
|
requester_id: str = Field(..., min_length=1, max_length=100)
|
||||||
requester_tenant_id: str = Field(..., min_length=1, max_length=100)
|
requester_tenant_id: str = Field(..., min_length=1, max_length=100)
|
||||||
|
requester_tenant_ids: list[str] = Field(default_factory=list)
|
||||||
requester_contact: str | None = Field(default=None, max_length=100)
|
requester_contact: str | None = Field(default=None, max_length=100)
|
||||||
requester_email: str | None = Field(default=None, max_length=320)
|
requester_email: str | None = Field(default=None, max_length=320)
|
||||||
requester_name: str | None = Field(default=None, max_length=100)
|
requester_name: str | None = Field(default=None, max_length=100)
|
||||||
@@ -187,6 +188,7 @@ class SupportTicketRecord(BaseModel):
|
|||||||
description: str
|
description: str
|
||||||
requester_id: str
|
requester_id: str
|
||||||
requester_tenant_id: str
|
requester_tenant_id: str
|
||||||
|
requester_tenant_ids: list[str] = Field(default_factory=list)
|
||||||
requester_contact: str
|
requester_contact: str
|
||||||
requester_email: str | None
|
requester_email: str | None
|
||||||
requester_name: str | None
|
requester_name: str | None
|
||||||
|
|||||||
@@ -169,6 +169,7 @@ class TicketService:
|
|||||||
workspace_id=workspace.id,
|
workspace_id=workspace.id,
|
||||||
requester_id=payload.requester_id,
|
requester_id=payload.requester_id,
|
||||||
requester_tenant_id=payload.requester_tenant_id,
|
requester_tenant_id=payload.requester_tenant_id,
|
||||||
|
requester_tenant_ids=payload.requester_tenant_ids or [payload.requester_tenant_id],
|
||||||
requester_contact=payload.requester_contact or "",
|
requester_contact=payload.requester_contact or "",
|
||||||
requester_email=payload.requester_email,
|
requester_email=payload.requester_email,
|
||||||
requester_name=payload.requester_name,
|
requester_name=payload.requester_name,
|
||||||
@@ -1301,6 +1302,7 @@ class TicketService:
|
|||||||
description=ticket.description,
|
description=ticket.description,
|
||||||
requester_id=ticket.requester_id,
|
requester_id=ticket.requester_id,
|
||||||
requester_tenant_id=ticket.requester_tenant_id,
|
requester_tenant_id=ticket.requester_tenant_id,
|
||||||
|
requester_tenant_ids=ticket.requester_tenant_ids or [ticket.requester_tenant_id],
|
||||||
requester_contact=ticket.requester_contact or "",
|
requester_contact=ticket.requester_contact or "",
|
||||||
requester_email=requester_email,
|
requester_email=requester_email,
|
||||||
requester_name=requester_name,
|
requester_name=requester_name,
|
||||||
|
|||||||
@@ -76,12 +76,7 @@ function App({ Component, pageProps }: AppPropsWithLayout) {
|
|||||||
).join('')
|
).join('')
|
||||||
: readCookie('baron_sso_debug_tenants');
|
: readCookie('baron_sso_debug_tenants');
|
||||||
|
|
||||||
if (!cookie) {
|
if (!cookie) return;
|
||||||
window.console.warn(
|
|
||||||
'[SSO OAuth debug] callback 디버그 쿠키가 없습니다. 로그아웃 후 다시 로그인하세요.',
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const data = JSON.parse(
|
const data = JSON.parse(
|
||||||
|
|||||||
@@ -17,22 +17,34 @@ import { createNextApiHandler } from '@/server/api-handler';
|
|||||||
import { getSupportAuthHeaders } from '@/server/support-auth';
|
import { getSupportAuthHeaders } from '@/server/support-auth';
|
||||||
import { supportExternalUrl } from '@/server/support-external';
|
import { supportExternalUrl } from '@/server/support-external';
|
||||||
|
|
||||||
|
const getErrorMessage = (error: unknown) =>
|
||||||
|
error instanceof Error ? error.message : String(error);
|
||||||
|
|
||||||
const handler = createNextApiHandler({
|
const handler = createNextApiHandler({
|
||||||
GET: async (req, res) => {
|
GET: async (req, res) => {
|
||||||
|
const query =
|
||||||
|
req.query.candidates ?
|
||||||
|
'?candidates=1'
|
||||||
|
: req.query.management ?
|
||||||
|
'?management=1'
|
||||||
|
: '';
|
||||||
|
const target = supportExternalUrl('/access') + query;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const query =
|
const response = await fetch(target, { headers: getSupportAuthHeaders(req) });
|
||||||
req.query.candidates ?
|
const raw = await response.text();
|
||||||
'?candidates=1'
|
let data: unknown = {};
|
||||||
: req.query.management ?
|
try {
|
||||||
'?management=1'
|
data = raw.trim() ? JSON.parse(raw) : {};
|
||||||
: '';
|
} catch {
|
||||||
const response = await fetch(
|
data = { message: raw.trim().slice(0, 1000) };
|
||||||
supportExternalUrl('/access') + query,
|
}
|
||||||
{ headers: getSupportAuthHeaders(req) },
|
|
||||||
);
|
|
||||||
const data = (await response.json()) as unknown;
|
|
||||||
return res.status(response.status).json(data);
|
return res.status(response.status).json(data);
|
||||||
} catch {
|
} catch (error) {
|
||||||
|
console.error('[support access] upstream request failed', {
|
||||||
|
target,
|
||||||
|
error: getErrorMessage(error),
|
||||||
|
});
|
||||||
return res.status(503).json({
|
return res.status(503).json({
|
||||||
message: '권한 정보를 확인할 수 없습니다.',
|
message: '권한 정보를 확인할 수 없습니다.',
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -67,6 +67,30 @@ const getSsoRequesterFields = (claims: Record<string, unknown>) => {
|
|||||||
(ancestor) => getStringValue(ancestor.type) === 'COMPANY',
|
(ancestor) => getStringValue(ancestor.type) === 'COMPANY',
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const tenantIds = new Set<string>();
|
||||||
|
const addTenantId = (value: unknown) => {
|
||||||
|
if (typeof value === 'string' && value.trim()) tenantIds.add(value.trim());
|
||||||
|
};
|
||||||
|
for (const source of tokenSources) {
|
||||||
|
const directTenantIds = source.tenant_ids ?? source.tenantIds;
|
||||||
|
if (Array.isArray(directTenantIds)) {
|
||||||
|
directTenantIds.forEach(addTenantId);
|
||||||
|
}
|
||||||
|
addTenantId(source.tenant_id);
|
||||||
|
addTenantId(source.tenantId);
|
||||||
|
addTenantId(asRecord(source.tenant)?.id);
|
||||||
|
if (Array.isArray(source.joinedTenants)) {
|
||||||
|
source.joinedTenants.forEach((tenant) => addTenantId(asRecord(tenant)?.id));
|
||||||
|
}
|
||||||
|
if (Array.isArray(source.tenants)) {
|
||||||
|
source.tenants.forEach((tenant) => addTenantId(asRecord(tenant)?.id));
|
||||||
|
} else if (source.tenants && typeof source.tenants === 'object') {
|
||||||
|
Object.values(source.tenants as Record<string, unknown>).forEach((tenant) =>
|
||||||
|
addTenantId(asRecord(tenant)?.id),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
requester_id: readClaim('sub', 'user_id', 'id'),
|
requester_id: readClaim('sub', 'user_id', 'id'),
|
||||||
requester_uuid: readClaim('sub', 'user_id', 'id'),
|
requester_uuid: readClaim('sub', 'user_id', 'id'),
|
||||||
@@ -89,6 +113,7 @@ const getSsoRequesterFields = (claims: Record<string, unknown>) => {
|
|||||||
'employee_number',
|
'employee_number',
|
||||||
'employeeNumber',
|
'employeeNumber',
|
||||||
),
|
),
|
||||||
|
requester_tenant_ids: JSON.stringify([...tenantIds]),
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
const MAX_ATTACHMENT_COUNT = 10;
|
const MAX_ATTACHMENT_COUNT = 10;
|
||||||
@@ -263,6 +288,10 @@ const handler = createNextApiHandler({
|
|||||||
|
|
||||||
return res.status(response.status).json(await readJsonOrMessage(response));
|
return res.status(response.status).json(await readJsonOrMessage(response));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
console.error('[support submit] upstream request failed', {
|
||||||
|
workspaceCode,
|
||||||
|
error: error instanceof Error ? error.message : String(error),
|
||||||
|
});
|
||||||
return res.status(isUploadSizeError(error) ? 413 : 502).json({
|
return res.status(isUploadSizeError(error) ? 413 : 502).json({
|
||||||
message:
|
message:
|
||||||
isUploadSizeError(error) ?
|
isUploadSizeError(error) ?
|
||||||
|
|||||||
@@ -46,6 +46,7 @@ interface SsoOrgMember {
|
|||||||
grade?: unknown;
|
grade?: unknown;
|
||||||
position?: unknown;
|
position?: unknown;
|
||||||
jobTitle?: unknown;
|
jobTitle?: unknown;
|
||||||
|
tenant_ids?: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
type SsoValueShape =
|
type SsoValueShape =
|
||||||
@@ -220,14 +221,25 @@ const getSsoOrgMember = async (
|
|||||||
tree?: { members?: unknown };
|
tree?: { members?: unknown };
|
||||||
tenants?: unknown;
|
tenants?: unknown;
|
||||||
};
|
};
|
||||||
|
const tenantIds = new Set<string>();
|
||||||
|
const rawTenants = Array.isArray(data.tenants) ?
|
||||||
|
data.tenants
|
||||||
|
: data.tenants && typeof data.tenants === 'object' ?
|
||||||
|
Object.values(data.tenants as Record<string, unknown>)
|
||||||
|
: [];
|
||||||
|
const tenantRecords = rawTenants
|
||||||
|
.map((tenant) => asRecord(tenant))
|
||||||
|
.filter((tenant): tenant is Record<string, unknown> => tenant !== null);
|
||||||
|
for (const tenant of tenantRecords) {
|
||||||
|
const id = asString(tenant.id) ?? asString(tenant.tenant_id);
|
||||||
|
if (id) tenantIds.add(id);
|
||||||
|
}
|
||||||
|
if (tenantId) tenantIds.add(tenantId);
|
||||||
const members: Record<string, unknown>[] = [];
|
const members: Record<string, unknown>[] = [];
|
||||||
const treeMembers = Array.isArray(data.tree?.members) ? data.tree.members : [];
|
const treeMembers = Array.isArray(data.tree?.members) ? data.tree.members : [];
|
||||||
const tenantMembers = Array.isArray(data.tenants) ?
|
const tenantMembers = tenantRecords.flatMap((tenant) =>
|
||||||
data.tenants.flatMap((tenant) => {
|
Array.isArray(tenant.members) ? tenant.members : [],
|
||||||
const record = asRecord(tenant);
|
);
|
||||||
return Array.isArray(record?.members) ? record.members : [];
|
|
||||||
})
|
|
||||||
: [];
|
|
||||||
|
|
||||||
for (const candidate of [...treeMembers, ...tenantMembers]) {
|
for (const candidate of [...treeMembers, ...tenantMembers]) {
|
||||||
const member = asRecord(candidate);
|
const member = asRecord(candidate);
|
||||||
@@ -236,13 +248,14 @@ const getSsoOrgMember = async (
|
|||||||
|
|
||||||
const subject = asString(info.id) ?? asString(info.sub) ?? asString(info.user_id);
|
const subject = asString(info.id) ?? asString(info.sub) ?? asString(info.user_id);
|
||||||
const email = asString(info.email);
|
const email = asString(info.email);
|
||||||
return (
|
const matchedMember = members.find((member) => {
|
||||||
members.find((member) => {
|
|
||||||
const memberId = asString(member.id);
|
const memberId = asString(member.id);
|
||||||
const memberEmail = asString(member.email);
|
const memberEmail = asString(member.email);
|
||||||
return (subject && memberId === subject) || (email && memberEmail === email);
|
return (subject && memberId === subject) || (email && memberEmail === email);
|
||||||
}) ?? null
|
});
|
||||||
);
|
return matchedMember || tenantIds.size > 0 ?
|
||||||
|
{ ...(matchedMember ?? {}), tenant_ids: [...tenantIds] }
|
||||||
|
: null;
|
||||||
};
|
};
|
||||||
|
|
||||||
const enrichSsoUserInfo = async (accessToken: string, info: OidcUserInfo) => {
|
const enrichSsoUserInfo = async (accessToken: string, info: OidcUserInfo) => {
|
||||||
@@ -258,6 +271,12 @@ const enrichSsoUserInfo = async (accessToken: string, info: OidcUserInfo) => {
|
|||||||
grade: asString(info.grade) ?? asString(member.grade),
|
grade: asString(info.grade) ?? asString(member.grade),
|
||||||
position: asString(info.position) ?? asString(member.position),
|
position: asString(info.position) ?? asString(member.position),
|
||||||
jobTitle: asString(info.jobTitle) ?? asString(member.jobTitle),
|
jobTitle: asString(info.jobTitle) ?? asString(member.jobTitle),
|
||||||
|
tenant_ids: [
|
||||||
|
...new Set([
|
||||||
|
...asStringArray(info.tenant_ids ?? info.tenantIds),
|
||||||
|
...(member.tenant_ids ?? []),
|
||||||
|
]),
|
||||||
|
],
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -390,6 +409,7 @@ export const getSsoDebugInfo = async (req: NextApiRequest) => {
|
|||||||
tenant: userInfo.tenant ?? null,
|
tenant: userInfo.tenant ?? null,
|
||||||
joinedTenants: Array.isArray(userInfo.joinedTenants) ? userInfo.joinedTenants : [],
|
joinedTenants: Array.isArray(userInfo.joinedTenants) ? userInfo.joinedTenants : [],
|
||||||
},
|
},
|
||||||
|
tenantIds: asStringArray(userInfo.tenant_ids ?? userInfo.tenantIds),
|
||||||
user: {
|
user: {
|
||||||
id: asString(userInfo.id) ?? asString(userInfo.sub) ?? asString(userInfo.user_id),
|
id: asString(userInfo.id) ?? asString(userInfo.sub) ?? asString(userInfo.user_id),
|
||||||
name: asString(userInfo.name),
|
name: asString(userInfo.name),
|
||||||
@@ -539,15 +559,33 @@ const createSupportSession = (info: OidcUserInfo) => {
|
|||||||
'SSO 사용자 정보에 sub 또는 tenant_id가 없습니다. SUPPORT_TENANT_ID를 확인하세요.',
|
'SSO 사용자 정보에 sub 또는 tenant_id가 없습니다. SUPPORT_TENANT_ID를 확인하세요.',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
const tenantIds = asStringArray(info.tenant_ids ?? info.tenantIds);
|
const tenantIds = new Set(asStringArray(info.tenant_ids ?? info.tenantIds));
|
||||||
if (!tenantIds.includes(tenantId)) tenantIds.unshift(tenantId);
|
const addTenantId = (value: unknown) => {
|
||||||
|
const id = asString(value);
|
||||||
|
if (id) tenantIds.add(id);
|
||||||
|
};
|
||||||
|
|
||||||
|
addTenantId(info.tenant_id);
|
||||||
|
addTenantId(info.tenantId);
|
||||||
|
addTenantId(asRecord(info.tenant)?.id);
|
||||||
|
for (const tenant of Array.isArray(info.joinedTenants) ? info.joinedTenants : []) {
|
||||||
|
addTenantId(asRecord(tenant)?.id);
|
||||||
|
}
|
||||||
|
if (Array.isArray(info.tenants)) {
|
||||||
|
for (const tenant of info.tenants) addTenantId(asRecord(tenant)?.id);
|
||||||
|
} else if (info.tenants && typeof info.tenants === 'object') {
|
||||||
|
for (const tenant of Object.values(info.tenants as Record<string, unknown>)) {
|
||||||
|
addTenantId(asRecord(tenant)?.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
tenantIds.add(tenantId);
|
||||||
const now = Math.floor(Date.now() / 1000);
|
const now = Math.floor(Date.now() / 1000);
|
||||||
return signHs256(
|
return signHs256(
|
||||||
{
|
{
|
||||||
sub: subject,
|
sub: subject,
|
||||||
sso_sub: subject,
|
sso_sub: subject,
|
||||||
tenant_id: tenantId,
|
tenant_id: tenantId,
|
||||||
tenant_ids: tenantIds,
|
tenant_ids: [...tenantIds],
|
||||||
email: asString(info.email),
|
email: asString(info.email),
|
||||||
name: asString(info.name),
|
name: asString(info.name),
|
||||||
department: asString(info.department),
|
department: asString(info.department),
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ const SSO_TOKEN_CLAIMS_COOKIE_COUNT = `${SSO_TOKEN_CLAIMS_COOKIE}_count`;
|
|||||||
export interface SupportPrincipal {
|
export interface SupportPrincipal {
|
||||||
user_id: string;
|
user_id: string;
|
||||||
tenant_id: string;
|
tenant_id: string;
|
||||||
|
tenant_ids?: string[];
|
||||||
email?: string | null;
|
email?: string | null;
|
||||||
name?: string | null;
|
name?: string | null;
|
||||||
department?: string | null;
|
department?: string | null;
|
||||||
|
|||||||
@@ -6,9 +6,11 @@
|
|||||||
* URL such as `http://127.0.0.1:8010`; both forms are supported here.
|
* URL such as `http://127.0.0.1:8010`; both forms are supported here.
|
||||||
*/
|
*/
|
||||||
const configuredBaseUrl =
|
const configuredBaseUrl =
|
||||||
process.env.SUPPORT_CONSOLE_API_BASE_URL ??
|
(
|
||||||
process.env.SUPPORT_API_BASE_URL ??
|
process.env.SUPPORT_CONSOLE_API_BASE_URL ??
|
||||||
'https://feedback.hmac.kr/api/support';
|
process.env.SUPPORT_API_BASE_URL ??
|
||||||
|
'https://feedback.hmac.kr/api/support'
|
||||||
|
).trim() || 'https://feedback.hmac.kr/api/support';
|
||||||
|
|
||||||
export const supportExternalUrl = (path: string) => {
|
export const supportExternalUrl = (path: string) => {
|
||||||
const baseUrl = configuredBaseUrl.replace(/\/$/, '');
|
const baseUrl = configuredBaseUrl.replace(/\/$/, '');
|
||||||
|
|||||||
@@ -87,6 +87,7 @@ export interface SupportTicketRecord extends SupportFeedbackAutomationMetadata {
|
|||||||
description: string;
|
description: string;
|
||||||
requester_id: string;
|
requester_id: string;
|
||||||
requester_tenant_id: string;
|
requester_tenant_id: string;
|
||||||
|
requester_tenant_ids?: string[];
|
||||||
requester_contact: string;
|
requester_contact: string;
|
||||||
requester_email?: string | null;
|
requester_email?: string | null;
|
||||||
requester_name?: string | null;
|
requester_name?: string | null;
|
||||||
|
|||||||
Reference in new issue
Block a user