forked from baron/baron-sso
Update dev workflow and org chart settings
This commit is contained in:
@@ -32,6 +32,11 @@ BACKEND_LOG_LEVEL=
|
|||||||
REDIS_ADDR=redis:6389 # compose.infra.yaml의 redis 포트(컨테이너 내부 기준)
|
REDIS_ADDR=redis:6389 # compose.infra.yaml의 redis 포트(컨테이너 내부 기준)
|
||||||
CORS_ALLOWED_ORIGINS=http://localhost:5000 # 쿠키 인증 사용 시 정확한 Origin 지정 필요
|
CORS_ALLOWED_ORIGINS=http://localhost:5000 # 쿠키 인증 사용 시 정확한 Origin 지정 필요
|
||||||
|
|
||||||
|
# --- NAVER WORKS API ---
|
||||||
|
WORKS_ADMIN_API_BASE_URL=https://www.worksapis.com
|
||||||
|
WORKS_ADMIN_OAUTH_TOKEN_URL=https://auth.worksmobile.com/oauth2/v2.0/token
|
||||||
|
|
||||||
|
|
||||||
# Audit System Configuration
|
# Audit System Configuration
|
||||||
AUDIT_WORKER_COUNT=5 # 비동기 감사 로그 처리를 위한 고루틴 워커 수
|
AUDIT_WORKER_COUNT=5 # 비동기 감사 로그 처리를 위한 고루틴 워커 수
|
||||||
AUDIT_QUEUE_SIZE=2000 # 감사 로그 대기열(채널) 버퍼 크기
|
AUDIT_QUEUE_SIZE=2000 # 감사 로그 대기열(채널) 버퍼 크기
|
||||||
|
|||||||
@@ -48,6 +48,8 @@ jobs:
|
|||||||
APP_ENV=stage
|
APP_ENV=stage
|
||||||
BACKEND_LOG_LEVEL=debug
|
BACKEND_LOG_LEVEL=debug
|
||||||
CLIENT_LOG_DEBUG=true
|
CLIENT_LOG_DEBUG=true
|
||||||
|
WORKS_ADMIN_API_BASE_URL=${{ vars.WORKS_ADMIN_API_BASE_URL }}
|
||||||
|
WORKS_ADMIN_OAUTH_TOKEN_URL=${{ vars.WORKS_ADMIN_OAUTH_TOKEN_URL }}
|
||||||
TZ=Asia/Seoul
|
TZ=Asia/Seoul
|
||||||
IDP_PROVIDER=ory
|
IDP_PROVIDER=ory
|
||||||
|
|
||||||
|
|||||||
@@ -58,6 +58,8 @@ jobs:
|
|||||||
APP_ENV=stage
|
APP_ENV=stage
|
||||||
BACKEND_LOG_LEVEL=debug
|
BACKEND_LOG_LEVEL=debug
|
||||||
CLIENT_LOG_DEBUG=true
|
CLIENT_LOG_DEBUG=true
|
||||||
|
WORKS_ADMIN_API_BASE_URL=${{ vars.WORKS_ADMIN_API_BASE_URL }}
|
||||||
|
WORKS_ADMIN_OAUTH_TOKEN_URL=${{ vars.WORKS_ADMIN_OAUTH_TOKEN_URL }}
|
||||||
TZ=Asia/Seoul
|
TZ=Asia/Seoul
|
||||||
IDP_PROVIDER=ory
|
IDP_PROVIDER=ory
|
||||||
|
|
||||||
|
|||||||
@@ -184,6 +184,7 @@ function AppLayout() {
|
|||||||
const manageableCount = profile?.manageableTenants?.length ?? 0;
|
const manageableCount = profile?.manageableTenants?.length ?? 0;
|
||||||
const orgfrontUrl = buildAuthenticatedOrgChartUrl(
|
const orgfrontUrl = buildAuthenticatedOrgChartUrl(
|
||||||
import.meta.env.ORGFRONT_URL || "http://localhost:5175",
|
import.meta.env.ORGFRONT_URL || "http://localhost:5175",
|
||||||
|
{ includeInternal: true },
|
||||||
);
|
);
|
||||||
|
|
||||||
const filteredItems = items.filter((item) => {
|
const filteredItems = items.filter((item) => {
|
||||||
|
|||||||
@@ -15,6 +15,16 @@ describe("orgChartPicker", () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("adds internal visibility to tenant picker URLs only when requested", () => {
|
||||||
|
expect(
|
||||||
|
buildOrgChartTenantPickerUrl("https://orgchart.example.com/", {
|
||||||
|
includeInternal: true,
|
||||||
|
}),
|
||||||
|
).toBe(
|
||||||
|
"https://orgchart.example.com/embed/picker?mode=single&select=tenant&width=400&height=600&includeInternal=true",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
it("adds tenantId to the tenant picker URL when Hanmac family scope is known", () => {
|
it("adds tenantId to the tenant picker URL when Hanmac family scope is known", () => {
|
||||||
expect(
|
expect(
|
||||||
buildOrgChartTenantPickerUrl("https://orgchart.example.com/", {
|
buildOrgChartTenantPickerUrl("https://orgchart.example.com/", {
|
||||||
@@ -34,12 +44,22 @@ describe("orgChartPicker", () => {
|
|||||||
},
|
},
|
||||||
),
|
),
|
||||||
).toBe(
|
).toBe(
|
||||||
"https://orgchart.example.com/login?auto=1&returnTo=%2Fembed%2Fpicker%3Fmode%3Dsingle%26select%3Dtenant%26width%3D400%26height%3D600%26tenantId%3Dhanmac-family-id",
|
"https://orgchart.example.com/login?auto=1&returnTo=%2Fembed%2Fpicker%3Fmode%3Dsingle%26select%3Dtenant%26width%3D400%26height%3D600%26tenantId%3Dhanmac-family-id%26includeInternal%3Dtrue",
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("builds the chart navigation URL through the org-chart auto login entry", () => {
|
it("builds the admin chart navigation URL with internal visibility enabled", () => {
|
||||||
expect(buildAuthenticatedOrgChartUrl("https://orgchart.example.com/")).toBe(
|
expect(buildAuthenticatedOrgChartUrl("https://orgchart.example.com/")).toBe(
|
||||||
|
"https://orgchart.example.com/login?auto=1&returnTo=%2Fchart%3FincludeInternal%3Dtrue",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("can build chart navigation URL without internal visibility", () => {
|
||||||
|
expect(
|
||||||
|
buildAuthenticatedOrgChartUrl("https://orgchart.example.com/", {
|
||||||
|
includeInternal: false,
|
||||||
|
}),
|
||||||
|
).toBe(
|
||||||
"https://orgchart.example.com/login?auto=1&returnTo=%2Fchart",
|
"https://orgchart.example.com/login?auto=1&returnTo=%2Fchart",
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -34,10 +34,12 @@ type OrgChartPickerMessage = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
type OrgChartTenantPickerOptions = {
|
type OrgChartTenantPickerOptions = {
|
||||||
|
includeInternal?: boolean;
|
||||||
tenantId?: string;
|
tenantId?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
type OrgChartLoginOptions = {
|
type OrgChartLoginOptions = {
|
||||||
|
includeInternal?: boolean;
|
||||||
returnTo?: string;
|
returnTo?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -204,6 +206,9 @@ export function buildOrgChartTenantPickerUrl(
|
|||||||
if (tenantId) {
|
if (tenantId) {
|
||||||
params.set("tenantId", tenantId);
|
params.set("tenantId", tenantId);
|
||||||
}
|
}
|
||||||
|
if (options.includeInternal) {
|
||||||
|
params.set("includeInternal", "true");
|
||||||
|
}
|
||||||
|
|
||||||
return `${normalizedBase}/embed/picker?${params.toString()}`;
|
return `${normalizedBase}/embed/picker?${params.toString()}`;
|
||||||
}
|
}
|
||||||
@@ -212,16 +217,25 @@ export function buildAuthenticatedOrgChartTenantPickerUrl(
|
|||||||
baseUrl?: string,
|
baseUrl?: string,
|
||||||
options: OrgChartTenantPickerOptions = {},
|
options: OrgChartTenantPickerOptions = {},
|
||||||
) {
|
) {
|
||||||
const pickerUrl = buildOrgChartTenantPickerUrl("", options);
|
const pickerUrl = buildOrgChartTenantPickerUrl("", {
|
||||||
|
includeInternal: true,
|
||||||
|
...options,
|
||||||
|
});
|
||||||
return buildAuthenticatedOrgChartUrl(baseUrl, { returnTo: pickerUrl });
|
return buildAuthenticatedOrgChartUrl(baseUrl, { returnTo: pickerUrl });
|
||||||
}
|
}
|
||||||
|
|
||||||
export function buildAuthenticatedOrgChartUrl(
|
export function buildAuthenticatedOrgChartUrl(
|
||||||
baseUrl?: string,
|
baseUrl?: string,
|
||||||
options: OrgChartLoginOptions = {},
|
options: OrgChartLoginOptions = { includeInternal: true },
|
||||||
) {
|
) {
|
||||||
const normalizedBase = (baseUrl ?? "").replace(/\/+$/, "");
|
const normalizedBase = (baseUrl ?? "").replace(/\/+$/, "");
|
||||||
const returnTo = options.returnTo?.trim() || "/chart";
|
let returnTo = options.returnTo?.trim() || "/chart";
|
||||||
|
if (options.includeInternal && returnTo.startsWith("/chart")) {
|
||||||
|
const [path, query = ""] = returnTo.split("?", 2);
|
||||||
|
const params = new URLSearchParams(query);
|
||||||
|
params.set("includeInternal", "true");
|
||||||
|
returnTo = `${path}?${params.toString()}`;
|
||||||
|
}
|
||||||
const params = new URLSearchParams({
|
const params = new URLSearchParams({
|
||||||
auto: "1",
|
auto: "1",
|
||||||
returnTo,
|
returnTo,
|
||||||
|
|||||||
@@ -331,8 +331,10 @@ func main() {
|
|||||||
ServiceAccount: getEnv("WORKS_ADMIN_OAUTH_CLIENT_SERVICE_ACCOUNT", ""),
|
ServiceAccount: getEnv("WORKS_ADMIN_OAUTH_CLIENT_SERVICE_ACCOUNT", ""),
|
||||||
PrivateKey: worksmobilePrivateKey,
|
PrivateKey: worksmobilePrivateKey,
|
||||||
Scope: getEnv("WORKS_ADMIN_OAUTH_SCOPE", "directory"),
|
Scope: getEnv("WORKS_ADMIN_OAUTH_SCOPE", "directory"),
|
||||||
|
TokenURL: getEnv("WORKS_ADMIN_OAUTH_TOKEN_URL", ""),
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
configureWorksmobileClientFromEnv(worksmobileClient)
|
||||||
worksmobileService := service.NewWorksmobileSyncService(tenantService, userRepo, worksmobileOutboxRepo, worksmobileClient)
|
worksmobileService := service.NewWorksmobileSyncService(tenantService, userRepo, worksmobileOutboxRepo, worksmobileClient)
|
||||||
worksmobileRelayWorker := service.NewWorksmobileRelayWorker(worksmobileOutboxRepo, worksmobileClient)
|
worksmobileRelayWorker := service.NewWorksmobileRelayWorker(worksmobileOutboxRepo, worksmobileClient)
|
||||||
go worksmobileRelayWorker.Start(context.Background())
|
go worksmobileRelayWorker.Start(context.Background())
|
||||||
@@ -898,3 +900,10 @@ func main() {
|
|||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func configureWorksmobileClientFromEnv(client *service.WorksmobileHTTPClient) {
|
||||||
|
if client == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
client.BaseURL = strings.TrimSpace(getEnv("WORKS_ADMIN_API_BASE_URL", ""))
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"baron-sso-backend/internal/service"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"testing"
|
"testing"
|
||||||
@@ -37,3 +38,14 @@ func TestGetEnvFileOrValueFallsBackToRawEnv(t *testing.T) {
|
|||||||
t.Fatalf("secret value = %q, want raw env value", got)
|
t.Fatalf("secret value = %q, want raw env value", got)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestConfigureWorksmobileClientFromEnvOverridesAPIBaseURL(t *testing.T) {
|
||||||
|
t.Setenv("WORKS_ADMIN_API_BASE_URL", "https://proxy.example.com/works")
|
||||||
|
client := service.NewWorksmobileHTTPClientWithTokens("", "")
|
||||||
|
|
||||||
|
configureWorksmobileClientFromEnv(client)
|
||||||
|
|
||||||
|
if client.BaseURL != "https://proxy.example.com/works" {
|
||||||
|
t.Fatalf("BaseURL = %q, want env override", client.BaseURL)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -21,8 +21,6 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
defaultWorksmobileAPIBaseURL = "https://www.worksapis.com"
|
|
||||||
defaultWorksmobileOAuthTokenURL = "https://auth.worksmobile.com/oauth2/v2.0/token"
|
|
||||||
defaultWorksmobileOAuthScope = "directory"
|
defaultWorksmobileOAuthScope = "directory"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -74,9 +72,6 @@ func (c WorksmobileOAuthConfig) normalized() WorksmobileOAuthConfig {
|
|||||||
c.Scope = defaultWorksmobileOAuthScope
|
c.Scope = defaultWorksmobileOAuthScope
|
||||||
}
|
}
|
||||||
c.TokenURL = strings.TrimSpace(c.TokenURL)
|
c.TokenURL = strings.TrimSpace(c.TokenURL)
|
||||||
if c.TokenURL == "" {
|
|
||||||
c.TokenURL = defaultWorksmobileOAuthTokenURL
|
|
||||||
}
|
|
||||||
return c
|
return c
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -87,6 +82,9 @@ func (c WorksmobileOAuthConfig) validate() error {
|
|||||||
if strings.TrimSpace(c.ServiceAccount) == "" || strings.TrimSpace(c.PrivateKey) == "" {
|
if strings.TrimSpace(c.ServiceAccount) == "" || strings.TrimSpace(c.PrivateKey) == "" {
|
||||||
return fmt.Errorf("worksmobile oauth service account is not configured")
|
return fmt.Errorf("worksmobile oauth service account is not configured")
|
||||||
}
|
}
|
||||||
|
if strings.TrimSpace(c.TokenURL) == "" {
|
||||||
|
return fmt.Errorf("worksmobile oauth token url is not configured")
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -163,14 +161,12 @@ func (e WorksmobileHTTPError) Error() string {
|
|||||||
|
|
||||||
func NewWorksmobileHTTPClient(scimToken string) *WorksmobileHTTPClient {
|
func NewWorksmobileHTTPClient(scimToken string) *WorksmobileHTTPClient {
|
||||||
return &WorksmobileHTTPClient{
|
return &WorksmobileHTTPClient{
|
||||||
BaseURL: defaultWorksmobileAPIBaseURL,
|
|
||||||
SCIMToken: strings.Trim(strings.TrimSpace(scimToken), `"`),
|
SCIMToken: strings.Trim(strings.TrimSpace(scimToken), `"`),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewWorksmobileHTTPClientWithTokens(directoryToken string, scimToken string) *WorksmobileHTTPClient {
|
func NewWorksmobileHTTPClientWithTokens(directoryToken string, scimToken string) *WorksmobileHTTPClient {
|
||||||
return &WorksmobileHTTPClient{
|
return &WorksmobileHTTPClient{
|
||||||
BaseURL: defaultWorksmobileAPIBaseURL,
|
|
||||||
DirectoryToken: strings.Trim(strings.TrimSpace(directoryToken), `"`),
|
DirectoryToken: strings.Trim(strings.TrimSpace(directoryToken), `"`),
|
||||||
SCIMToken: strings.Trim(strings.TrimSpace(scimToken), `"`),
|
SCIMToken: strings.Trim(strings.TrimSpace(scimToken), `"`),
|
||||||
}
|
}
|
||||||
@@ -178,7 +174,6 @@ func NewWorksmobileHTTPClientWithTokens(directoryToken string, scimToken string)
|
|||||||
|
|
||||||
func NewWorksmobileHTTPClientWithAuth(directoryToken string, scimToken string, oauthConfig WorksmobileOAuthConfig) *WorksmobileHTTPClient {
|
func NewWorksmobileHTTPClientWithAuth(directoryToken string, scimToken string, oauthConfig WorksmobileOAuthConfig) *WorksmobileHTTPClient {
|
||||||
return &WorksmobileHTTPClient{
|
return &WorksmobileHTTPClient{
|
||||||
BaseURL: defaultWorksmobileAPIBaseURL,
|
|
||||||
DirectoryToken: strings.Trim(strings.TrimSpace(directoryToken), `"`),
|
DirectoryToken: strings.Trim(strings.TrimSpace(directoryToken), `"`),
|
||||||
SCIMToken: strings.Trim(strings.TrimSpace(scimToken), `"`),
|
SCIMToken: strings.Trim(strings.TrimSpace(scimToken), `"`),
|
||||||
OAuthConfig: oauthConfig.normalized(),
|
OAuthConfig: oauthConfig.normalized(),
|
||||||
@@ -437,7 +432,11 @@ func (c *WorksmobileHTTPClient) getJSON(ctx context.Context, path string, target
|
|||||||
if token == "" {
|
if token == "" {
|
||||||
return fmt.Errorf("worksmobile read token is not configured")
|
return fmt.Errorf("worksmobile read token is not configured")
|
||||||
}
|
}
|
||||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, strings.TrimRight(c.baseURL(), "/")+path, nil)
|
requestURL, err := c.requestURL(path)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
req, err := http.NewRequestWithContext(ctx, http.MethodGet, requestURL, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -461,7 +460,11 @@ func (c *WorksmobileHTTPClient) getDirectoryJSON(ctx context.Context, path strin
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, strings.TrimRight(c.baseURL(), "/")+path, nil)
|
requestURL, err := c.requestURL(path)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
req, err := http.NewRequestWithContext(ctx, http.MethodGet, requestURL, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -672,7 +675,11 @@ func (c *WorksmobileHTTPClient) sendJSONWithToken(ctx context.Context, method st
|
|||||||
body = bytes.NewReader(data)
|
body = bytes.NewReader(data)
|
||||||
}
|
}
|
||||||
|
|
||||||
req, err := http.NewRequestWithContext(ctx, method, strings.TrimRight(c.baseURL(), "/")+path, body)
|
requestURL, err := c.requestURL(path)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
req, err := http.NewRequestWithContext(ctx, method, requestURL, body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -1117,12 +1124,17 @@ func boolPointerFromMap(values map[string]any, keys ...string) *bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (c *WorksmobileHTTPClient) baseURL() string {
|
func (c *WorksmobileHTTPClient) baseURL() string {
|
||||||
if strings.TrimSpace(c.BaseURL) == "" {
|
|
||||||
return defaultWorksmobileAPIBaseURL
|
|
||||||
}
|
|
||||||
return c.BaseURL
|
return c.BaseURL
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *WorksmobileHTTPClient) requestURL(path string) (string, error) {
|
||||||
|
baseURL := strings.TrimSpace(c.baseURL())
|
||||||
|
if baseURL == "" {
|
||||||
|
return "", fmt.Errorf("worksmobile api base url is not configured")
|
||||||
|
}
|
||||||
|
return strings.TrimRight(baseURL, "/") + path, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (c *WorksmobileHTTPClient) httpClient() *http.Client {
|
func (c *WorksmobileHTTPClient) httpClient() *http.Client {
|
||||||
if c.HTTPClient != nil {
|
if c.HTTPClient != nil {
|
||||||
return c.HTTPClient
|
return c.HTTPClient
|
||||||
|
|||||||
@@ -181,6 +181,46 @@ func TestWorksmobileHTTPClientRequestsJWTBearerAccessToken(t *testing.T) {
|
|||||||
require.Equal(t, float64(1710003600), payload["exp"])
|
require.Equal(t, float64(1710003600), payload["exp"])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestWorksmobileHTTPClientRequiresConfiguredAPIBaseURL(t *testing.T) {
|
||||||
|
client := &WorksmobileHTTPClient{
|
||||||
|
DirectoryToken: "directory-token-1",
|
||||||
|
HTTPClient: &http.Client{Transport: &captureRoundTripper{statusCode: http.StatusCreated, body: `{}`}},
|
||||||
|
}
|
||||||
|
|
||||||
|
err := client.CreateUser(context.Background(), WorksmobileUserPayload{
|
||||||
|
DomainID: 300285955,
|
||||||
|
Email: "tester@samaneng.com",
|
||||||
|
UserExternalKey: "user-1",
|
||||||
|
UserName: WorksmobileUserName{LastName: "Tester"},
|
||||||
|
PasswordConfig: WorksmobilePasswordConfig{PasswordCreationType: "ADMIN", Password: "Aa1!Aa1!Aa1!Aa1!"},
|
||||||
|
})
|
||||||
|
|
||||||
|
require.Error(t, err)
|
||||||
|
require.Contains(t, err.Error(), "worksmobile api base url is not configured")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWorksmobileHTTPClientRequiresConfiguredOAuthTokenURL(t *testing.T) {
|
||||||
|
privateKey := testRSAPrivateKeyPEM(t)
|
||||||
|
client := &WorksmobileHTTPClient{
|
||||||
|
BaseURL: "https://works.example.test",
|
||||||
|
OAuthConfig: WorksmobileOAuthConfig{
|
||||||
|
ClientID: "client-id-1",
|
||||||
|
ClientSecret: "client-secret-1",
|
||||||
|
ServiceAccount: "service-account-1",
|
||||||
|
PrivateKey: privateKey,
|
||||||
|
Scope: "directory",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
_, _, err := client.requestDirectoryAccessToken(
|
||||||
|
context.Background(),
|
||||||
|
time.Unix(1710000000, 0),
|
||||||
|
)
|
||||||
|
|
||||||
|
require.Error(t, err)
|
||||||
|
require.Contains(t, err.Error(), "worksmobile oauth token url is not configured")
|
||||||
|
}
|
||||||
|
|
||||||
func TestWorksmobileHTTPClientListUsersUsesDirectoryAPIFirst(t *testing.T) {
|
func TestWorksmobileHTTPClientListUsersUsesDirectoryAPIFirst(t *testing.T) {
|
||||||
t.Setenv("SAMAN_DOMAIN_ID", "300285955")
|
t.Setenv("SAMAN_DOMAIN_ID", "300285955")
|
||||||
transport := &captureRoundTripper{
|
transport := &captureRoundTripper{
|
||||||
@@ -393,13 +433,7 @@ func TestWorksmobileLiveJWTTokenExchange(t *testing.T) {
|
|||||||
if os.Getenv("WORKSMOBILE_LIVE_JWT_TOKEN_EXCHANGE") != "1" {
|
if os.Getenv("WORKSMOBILE_LIVE_JWT_TOKEN_EXCHANGE") != "1" {
|
||||||
t.Skip("live Worksmobile token exchange is disabled")
|
t.Skip("live Worksmobile token exchange is disabled")
|
||||||
}
|
}
|
||||||
client := NewWorksmobileHTTPClientWithAuth("", os.Getenv("SAMAN_SCIM_LONGLIVE_TOKEN"), WorksmobileOAuthConfig{
|
client := newWorksmobileLiveClient()
|
||||||
ClientID: os.Getenv("WORKS_ADMIN_OAUTH_CLIENT_ID"),
|
|
||||||
ClientSecret: os.Getenv("WORKS_ADMIN_OAUTH_CLIENT_SECRET"),
|
|
||||||
ServiceAccount: os.Getenv("WORKS_ADMIN_OAUTH_CLIENT_SERVICE_ACCOUNT"),
|
|
||||||
PrivateKey: os.Getenv("WORKS_ADMIN_OAUTH_CLIENT_PRIVATE_KEY"),
|
|
||||||
Scope: getenvDefault("WORKS_ADMIN_OAUTH_SCOPE", "directory"),
|
|
||||||
})
|
|
||||||
|
|
||||||
token, err := client.directoryAccessToken(context.Background())
|
token, err := client.directoryAccessToken(context.Background())
|
||||||
|
|
||||||
@@ -989,6 +1023,19 @@ func getenvDefault(key string, fallback string) string {
|
|||||||
return fallback
|
return fallback
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func newWorksmobileLiveClient() *WorksmobileHTTPClient {
|
||||||
|
client := NewWorksmobileHTTPClientWithAuth("", os.Getenv("SAMAN_SCIM_LONGLIVE_TOKEN"), WorksmobileOAuthConfig{
|
||||||
|
ClientID: os.Getenv("WORKS_ADMIN_OAUTH_CLIENT_ID"),
|
||||||
|
ClientSecret: os.Getenv("WORKS_ADMIN_OAUTH_CLIENT_SECRET"),
|
||||||
|
ServiceAccount: os.Getenv("WORKS_ADMIN_OAUTH_CLIENT_SERVICE_ACCOUNT"),
|
||||||
|
PrivateKey: os.Getenv("WORKS_ADMIN_OAUTH_CLIENT_PRIVATE_KEY"),
|
||||||
|
Scope: getenvDefault("WORKS_ADMIN_OAUTH_SCOPE", "directory"),
|
||||||
|
TokenURL: os.Getenv("WORKS_ADMIN_OAUTH_TOKEN_URL"),
|
||||||
|
})
|
||||||
|
client.BaseURL = os.Getenv("WORKS_ADMIN_API_BASE_URL")
|
||||||
|
return client
|
||||||
|
}
|
||||||
|
|
||||||
func (f *fakeWorksmobileDirectoryClient) CreateOrgUnit(ctx context.Context, payload WorksmobileOrgUnitPayload) error {
|
func (f *fakeWorksmobileDirectoryClient) CreateOrgUnit(ctx context.Context, payload WorksmobileOrgUnitPayload) error {
|
||||||
f.createdOrgUnits = append(f.createdOrgUnits, payload)
|
f.createdOrgUnits = append(f.createdOrgUnits, payload)
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
@@ -33,13 +33,7 @@ func TestWorksmobileLiveSamanUsersDirectoryProvisioning(t *testing.T) {
|
|||||||
userGroupRepo := repository.NewUserGroupRepository(db)
|
userGroupRepo := repository.NewUserGroupRepository(db)
|
||||||
outboxRepo := repository.NewWorksmobileOutboxRepository(db)
|
outboxRepo := repository.NewWorksmobileOutboxRepository(db)
|
||||||
tenantService := NewTenantService(tenantRepo, userRepo, userGroupRepo, nil)
|
tenantService := NewTenantService(tenantRepo, userRepo, userGroupRepo, nil)
|
||||||
client := NewWorksmobileHTTPClientWithAuth("", os.Getenv("SAMAN_SCIM_LONGLIVE_TOKEN"), WorksmobileOAuthConfig{
|
client := newWorksmobileLiveClient()
|
||||||
ClientID: os.Getenv("WORKS_ADMIN_OAUTH_CLIENT_ID"),
|
|
||||||
ClientSecret: os.Getenv("WORKS_ADMIN_OAUTH_CLIENT_SECRET"),
|
|
||||||
ServiceAccount: os.Getenv("WORKS_ADMIN_OAUTH_CLIENT_SERVICE_ACCOUNT"),
|
|
||||||
PrivateKey: os.Getenv("WORKS_ADMIN_OAUTH_CLIENT_PRIVATE_KEY"),
|
|
||||||
Scope: getenvDefault("WORKS_ADMIN_OAUTH_SCOPE", "directory"),
|
|
||||||
})
|
|
||||||
syncService := NewWorksmobileSyncService(tenantService, userRepo, outboxRepo, client)
|
syncService := NewWorksmobileSyncService(tenantService, userRepo, outboxRepo, client)
|
||||||
worker := NewWorksmobileRelayWorker(outboxRepo, client)
|
worker := NewWorksmobileRelayWorker(outboxRepo, client)
|
||||||
|
|
||||||
@@ -145,13 +139,7 @@ func TestWorksmobileLiveSyncHanmacFamilyOrgUnits(t *testing.T) {
|
|||||||
userRepo := repository.NewUserRepository(db)
|
userRepo := repository.NewUserRepository(db)
|
||||||
userGroupRepo := repository.NewUserGroupRepository(db)
|
userGroupRepo := repository.NewUserGroupRepository(db)
|
||||||
tenantService := NewTenantService(tenantRepo, userRepo, userGroupRepo, nil)
|
tenantService := NewTenantService(tenantRepo, userRepo, userGroupRepo, nil)
|
||||||
client := NewWorksmobileHTTPClientWithAuth("", os.Getenv("SAMAN_SCIM_LONGLIVE_TOKEN"), WorksmobileOAuthConfig{
|
client := newWorksmobileLiveClient()
|
||||||
ClientID: os.Getenv("WORKS_ADMIN_OAUTH_CLIENT_ID"),
|
|
||||||
ClientSecret: os.Getenv("WORKS_ADMIN_OAUTH_CLIENT_SECRET"),
|
|
||||||
ServiceAccount: os.Getenv("WORKS_ADMIN_OAUTH_CLIENT_SERVICE_ACCOUNT"),
|
|
||||||
PrivateKey: os.Getenv("WORKS_ADMIN_OAUTH_CLIENT_PRIVATE_KEY"),
|
|
||||||
Scope: getenvDefault("WORKS_ADMIN_OAUTH_SCOPE", "directory"),
|
|
||||||
})
|
|
||||||
|
|
||||||
root, err := tenantService.GetTenantBySlug(ctx, HanmacFamilyTenantSlug)
|
root, err := tenantService.GetTenantBySlug(ctx, HanmacFamilyTenantSlug)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
@@ -239,13 +227,7 @@ func TestWorksmobileLiveInspectGPDTDCOrgUnits(t *testing.T) {
|
|||||||
userRepo := repository.NewUserRepository(db)
|
userRepo := repository.NewUserRepository(db)
|
||||||
userGroupRepo := repository.NewUserGroupRepository(db)
|
userGroupRepo := repository.NewUserGroupRepository(db)
|
||||||
tenantService := NewTenantService(tenantRepo, userRepo, userGroupRepo, nil)
|
tenantService := NewTenantService(tenantRepo, userRepo, userGroupRepo, nil)
|
||||||
client := NewWorksmobileHTTPClientWithAuth("", os.Getenv("SAMAN_SCIM_LONGLIVE_TOKEN"), WorksmobileOAuthConfig{
|
client := newWorksmobileLiveClient()
|
||||||
ClientID: os.Getenv("WORKS_ADMIN_OAUTH_CLIENT_ID"),
|
|
||||||
ClientSecret: os.Getenv("WORKS_ADMIN_OAUTH_CLIENT_SECRET"),
|
|
||||||
ServiceAccount: os.Getenv("WORKS_ADMIN_OAUTH_CLIENT_SERVICE_ACCOUNT"),
|
|
||||||
PrivateKey: os.Getenv("WORKS_ADMIN_OAUTH_CLIENT_PRIVATE_KEY"),
|
|
||||||
Scope: getenvDefault("WORKS_ADMIN_OAUTH_SCOPE", "directory"),
|
|
||||||
})
|
|
||||||
|
|
||||||
gpdtdcTenant, err := tenantService.GetTenantBySlug(ctx, "gpdtdc")
|
gpdtdcTenant, err := tenantService.GetTenantBySlug(ctx, "gpdtdc")
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
@@ -322,13 +304,7 @@ func TestWorksmobileLiveRecoverGPDTDCOrgUnits(t *testing.T) {
|
|||||||
userRepo := repository.NewUserRepository(db)
|
userRepo := repository.NewUserRepository(db)
|
||||||
userGroupRepo := repository.NewUserGroupRepository(db)
|
userGroupRepo := repository.NewUserGroupRepository(db)
|
||||||
tenantService := NewTenantService(tenantRepo, userRepo, userGroupRepo, nil)
|
tenantService := NewTenantService(tenantRepo, userRepo, userGroupRepo, nil)
|
||||||
client := NewWorksmobileHTTPClientWithAuth("", os.Getenv("SAMAN_SCIM_LONGLIVE_TOKEN"), WorksmobileOAuthConfig{
|
client := newWorksmobileLiveClient()
|
||||||
ClientID: os.Getenv("WORKS_ADMIN_OAUTH_CLIENT_ID"),
|
|
||||||
ClientSecret: os.Getenv("WORKS_ADMIN_OAUTH_CLIENT_SECRET"),
|
|
||||||
ServiceAccount: os.Getenv("WORKS_ADMIN_OAUTH_CLIENT_SERVICE_ACCOUNT"),
|
|
||||||
PrivateKey: os.Getenv("WORKS_ADMIN_OAUTH_CLIENT_PRIVATE_KEY"),
|
|
||||||
Scope: getenvDefault("WORKS_ADMIN_OAUTH_SCOPE", "directory"),
|
|
||||||
})
|
|
||||||
|
|
||||||
gpdtdcTenant, err := tenantService.GetTenantBySlug(ctx, "gpdtdc")
|
gpdtdcTenant, err := tenantService.GetTenantBySlug(ctx, "gpdtdc")
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
@@ -502,13 +478,7 @@ func runWorksmobileLiveCompanyOrgUnitProvisioning(t *testing.T, companySlug stri
|
|||||||
userRepo := repository.NewUserRepository(db)
|
userRepo := repository.NewUserRepository(db)
|
||||||
userGroupRepo := repository.NewUserGroupRepository(db)
|
userGroupRepo := repository.NewUserGroupRepository(db)
|
||||||
tenantService := NewTenantService(tenantRepo, userRepo, userGroupRepo, nil)
|
tenantService := NewTenantService(tenantRepo, userRepo, userGroupRepo, nil)
|
||||||
client := NewWorksmobileHTTPClientWithAuth("", os.Getenv("SAMAN_SCIM_LONGLIVE_TOKEN"), WorksmobileOAuthConfig{
|
client := newWorksmobileLiveClient()
|
||||||
ClientID: os.Getenv("WORKS_ADMIN_OAUTH_CLIENT_ID"),
|
|
||||||
ClientSecret: os.Getenv("WORKS_ADMIN_OAUTH_CLIENT_SECRET"),
|
|
||||||
ServiceAccount: os.Getenv("WORKS_ADMIN_OAUTH_CLIENT_SERVICE_ACCOUNT"),
|
|
||||||
PrivateKey: os.Getenv("WORKS_ADMIN_OAUTH_CLIENT_PRIVATE_KEY"),
|
|
||||||
Scope: getenvDefault("WORKS_ADMIN_OAUTH_SCOPE", "directory"),
|
|
||||||
})
|
|
||||||
|
|
||||||
companyTenant, err := tenantService.GetTenantBySlug(ctx, companySlug)
|
companyTenant, err := tenantService.GetTenantBySlug(ctx, companySlug)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
@@ -565,13 +535,7 @@ func runWorksmobileLiveBaronGroupOrgUnitProvisioning(t *testing.T) {
|
|||||||
userRepo := repository.NewUserRepository(db)
|
userRepo := repository.NewUserRepository(db)
|
||||||
userGroupRepo := repository.NewUserGroupRepository(db)
|
userGroupRepo := repository.NewUserGroupRepository(db)
|
||||||
tenantService := NewTenantService(tenantRepo, userRepo, userGroupRepo, nil)
|
tenantService := NewTenantService(tenantRepo, userRepo, userGroupRepo, nil)
|
||||||
client := NewWorksmobileHTTPClientWithAuth("", os.Getenv("SAMAN_SCIM_LONGLIVE_TOKEN"), WorksmobileOAuthConfig{
|
client := newWorksmobileLiveClient()
|
||||||
ClientID: os.Getenv("WORKS_ADMIN_OAUTH_CLIENT_ID"),
|
|
||||||
ClientSecret: os.Getenv("WORKS_ADMIN_OAUTH_CLIENT_SECRET"),
|
|
||||||
ServiceAccount: os.Getenv("WORKS_ADMIN_OAUTH_CLIENT_SERVICE_ACCOUNT"),
|
|
||||||
PrivateKey: os.Getenv("WORKS_ADMIN_OAUTH_CLIENT_PRIVATE_KEY"),
|
|
||||||
Scope: getenvDefault("WORKS_ADMIN_OAUTH_SCOPE", "directory"),
|
|
||||||
})
|
|
||||||
|
|
||||||
baronGroup, err := tenantService.GetTenantBySlug(ctx, "baron-group")
|
baronGroup, err := tenantService.GetTenantBySlug(ctx, "baron-group")
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|||||||
@@ -11,6 +11,8 @@ services:
|
|||||||
- GO_ENV=${APP_ENV:-development}
|
- GO_ENV=${APP_ENV:-development}
|
||||||
- BACKEND_LOG_LEVEL=${BACKEND_LOG_LEVEL:-info}
|
- BACKEND_LOG_LEVEL=${BACKEND_LOG_LEVEL:-info}
|
||||||
- CLIENT_LOG_DEBUG=${CLIENT_LOG_DEBUG:-false}
|
- CLIENT_LOG_DEBUG=${CLIENT_LOG_DEBUG:-false}
|
||||||
|
- WORKS_ADMIN_API_BASE_URL=${WORKS_ADMIN_API_BASE_URL}
|
||||||
|
- WORKS_ADMIN_OAUTH_TOKEN_URL=${WORKS_ADMIN_OAUTH_TOKEN_URL}
|
||||||
- COOKIE_SECRET=${COOKIE_SECRET}
|
- COOKIE_SECRET=${COOKIE_SECRET}
|
||||||
- JWT_SECRET=${JWT_SECRET}
|
- JWT_SECRET=${JWT_SECRET}
|
||||||
- NAVER_CLOUD_ACCESS_KEY=${NAVER_CLOUD_ACCESS_KEY}
|
- NAVER_CLOUD_ACCESS_KEY=${NAVER_CLOUD_ACCESS_KEY}
|
||||||
|
|||||||
@@ -374,6 +374,8 @@ services:
|
|||||||
environment:
|
environment:
|
||||||
- APP_ENV=${APP_ENV:-development}
|
- APP_ENV=${APP_ENV:-development}
|
||||||
- GO_ENV=${APP_ENV:-development}
|
- GO_ENV=${APP_ENV:-development}
|
||||||
|
- WORKS_ADMIN_API_BASE_URL=${WORKS_ADMIN_API_BASE_URL}
|
||||||
|
- WORKS_ADMIN_OAUTH_TOKEN_URL=${WORKS_ADMIN_OAUTH_TOKEN_URL}
|
||||||
- COOKIE_SECRET=${COOKIE_SECRET}
|
- COOKIE_SECRET=${COOKIE_SECRET}
|
||||||
- JWT_SECRET=${JWT_SECRET}
|
- JWT_SECRET=${JWT_SECRET}
|
||||||
- NAVER_CLOUD_ACCESS_KEY=${NAVER_CLOUD_ACCESS_KEY}
|
- NAVER_CLOUD_ACCESS_KEY=${NAVER_CLOUD_ACCESS_KEY}
|
||||||
|
|||||||
@@ -106,10 +106,20 @@ describe("buildOrgPickerTree", () => {
|
|||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("excludes private tenants and their descendants from picker choices", () => {
|
it("excludes internal and private tenants from picker choices by default", () => {
|
||||||
const tenants = [
|
const tenants = [
|
||||||
tenant("hanmac-family-id", "COMPANY_GROUP", "한맥가족", "hanmac-family"),
|
tenant("hanmac-family-id", "COMPANY_GROUP", "한맥가족", "hanmac-family"),
|
||||||
tenant("saman-id", "COMPANY", "삼안", "saman", "hanmac-family-id"),
|
tenant("saman-id", "COMPANY", "삼안", "saman", "hanmac-family-id"),
|
||||||
|
{
|
||||||
|
...tenant(
|
||||||
|
"internal-id",
|
||||||
|
"ORGANIZATION",
|
||||||
|
"내부 조직",
|
||||||
|
"internal",
|
||||||
|
"saman-id",
|
||||||
|
),
|
||||||
|
config: { visibility: "internal" },
|
||||||
|
},
|
||||||
{
|
{
|
||||||
...tenant(
|
...tenant(
|
||||||
"secret-id",
|
"secret-id",
|
||||||
@@ -138,4 +148,34 @@ describe("buildOrgPickerTree", () => {
|
|||||||
|
|
||||||
expect(tree.roots[0]?.children.map((node) => node.id)).toEqual(["open-id"]);
|
expect(tree.roots[0]?.children.map((node) => node.id)).toEqual(["open-id"]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("includes internal tenants when explicitly requested", () => {
|
||||||
|
const tenants = [
|
||||||
|
tenant("hanmac-family-id", "COMPANY_GROUP", "한맥가족", "hanmac-family"),
|
||||||
|
tenant("saman-id", "COMPANY", "삼안", "saman", "hanmac-family-id"),
|
||||||
|
{
|
||||||
|
...tenant(
|
||||||
|
"internal-id",
|
||||||
|
"ORGANIZATION",
|
||||||
|
"내부 조직",
|
||||||
|
"internal",
|
||||||
|
"saman-id",
|
||||||
|
),
|
||||||
|
config: { visibility: "internal" },
|
||||||
|
},
|
||||||
|
tenant("open-id", "ORGANIZATION", "공개 조직", "open", "saman-id"),
|
||||||
|
];
|
||||||
|
|
||||||
|
const tree = buildOrgPickerTree({
|
||||||
|
includeInternal: true,
|
||||||
|
tenants,
|
||||||
|
users: [] satisfies UserSummary[],
|
||||||
|
tenantId: "saman",
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(tree.roots[0]?.children.map((node) => node.id)).toEqual([
|
||||||
|
"internal-id",
|
||||||
|
"open-id",
|
||||||
|
]);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -108,11 +108,13 @@ function findTenantNode(
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function buildOrgPickerTree({
|
export function buildOrgPickerTree({
|
||||||
|
includeInternal = false,
|
||||||
tenants,
|
tenants,
|
||||||
users,
|
users,
|
||||||
rootTenantId,
|
rootTenantId,
|
||||||
tenantId,
|
tenantId,
|
||||||
}: {
|
}: {
|
||||||
|
includeInternal?: boolean;
|
||||||
tenants: TenantSummary[];
|
tenants: TenantSummary[];
|
||||||
users: UserSummary[];
|
users: UserSummary[];
|
||||||
rootTenantId?: string;
|
rootTenantId?: string;
|
||||||
@@ -120,7 +122,7 @@ export function buildOrgPickerTree({
|
|||||||
}) {
|
}) {
|
||||||
const visibleTenants = filterTenantsByVisibility(
|
const visibleTenants = filterTenantsByVisibility(
|
||||||
tenants.filter(isOrgFrontTenantType),
|
tenants.filter(isOrgFrontTenantType),
|
||||||
"internal",
|
includeInternal ? "internal" : "public",
|
||||||
);
|
);
|
||||||
const usersBySlug = new Map<string, UserSummary[]>();
|
const usersBySlug = new Map<string, UserSummary[]>();
|
||||||
for (const user of users) {
|
for (const user of users) {
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ describe("org picker embed options", () => {
|
|||||||
it("builds slug-based tenant scope urls", () => {
|
it("builds slug-based tenant scope urls", () => {
|
||||||
expect(
|
expect(
|
||||||
buildOrgPickerEmbedSrc({
|
buildOrgPickerEmbedSrc({
|
||||||
|
includeInternal: false,
|
||||||
mode: "single",
|
mode: "single",
|
||||||
select: "tenant",
|
select: "tenant",
|
||||||
includeDescendants: true,
|
includeDescendants: true,
|
||||||
@@ -21,6 +22,27 @@ describe("org picker embed options", () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("builds and parses internal visibility flag only when requested", () => {
|
||||||
|
const src = buildOrgPickerEmbedSrc({
|
||||||
|
includeInternal: true,
|
||||||
|
mode: "single",
|
||||||
|
select: "tenant",
|
||||||
|
includeDescendants: true,
|
||||||
|
showDescendantToggle: true,
|
||||||
|
tenantId: "",
|
||||||
|
width: 400,
|
||||||
|
height: 600,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(src).toBe(
|
||||||
|
"/embed/picker?mode=single&select=tenant&width=400&height=600&includeInternal=true",
|
||||||
|
);
|
||||||
|
expect(parseOrgPickerEmbedOptions(src).includeInternal).toBe(true);
|
||||||
|
expect(parseOrgPickerEmbedOptions("?mode=single").includeInternal).toBe(
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
it("parses tenantSlug first and keeps legacy tenantId compatibility", () => {
|
it("parses tenantSlug first and keeps legacy tenantId compatibility", () => {
|
||||||
expect(
|
expect(
|
||||||
parseOrgPickerEmbedOptions(
|
parseOrgPickerEmbedOptions(
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ export type OrgPickerResult = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export type OrgPickerEmbedOptions = {
|
export type OrgPickerEmbedOptions = {
|
||||||
|
includeInternal: boolean;
|
||||||
mode: OrgPickerMode;
|
mode: OrgPickerMode;
|
||||||
select: OrgPickerSelectableType;
|
select: OrgPickerSelectableType;
|
||||||
includeDescendants: boolean;
|
includeDescendants: boolean;
|
||||||
@@ -68,6 +69,7 @@ export function parseOrgPickerEmbedOptions(search: string) {
|
|||||||
? ("single" as const)
|
? ("single" as const)
|
||||||
: ("multiple" as const),
|
: ("multiple" as const),
|
||||||
select: parseOrgPickerSelectableType(params.get("select")),
|
select: parseOrgPickerSelectableType(params.get("select")),
|
||||||
|
includeInternal: params.get("includeInternal") === "true",
|
||||||
includeDescendants: params.get("includeDescendants") !== "false",
|
includeDescendants: params.get("includeDescendants") !== "false",
|
||||||
showDescendantToggle: params.get("showDescendantToggle") !== "false",
|
showDescendantToggle: params.get("showDescendantToggle") !== "false",
|
||||||
tenantId:
|
tenantId:
|
||||||
@@ -87,6 +89,9 @@ export function buildOrgPickerEmbedSrc(options: OrgPickerEmbedOptions) {
|
|||||||
width: String(options.width),
|
width: String(options.width),
|
||||||
height: String(options.height),
|
height: String(options.height),
|
||||||
});
|
});
|
||||||
|
if (options.includeInternal) {
|
||||||
|
params.set("includeInternal", "true");
|
||||||
|
}
|
||||||
|
|
||||||
const tenantSlug = options.tenantId.trim();
|
const tenantSlug = options.tenantId.trim();
|
||||||
if (tenantSlug) {
|
if (tenantSlug) {
|
||||||
|
|||||||
@@ -1200,6 +1200,8 @@ export function TenantOrgChartPage() {
|
|||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const searchParams = new URLSearchParams(location.search);
|
const searchParams = new URLSearchParams(location.search);
|
||||||
const shareToken = searchParams.get("token");
|
const shareToken = searchParams.get("token");
|
||||||
|
const visibilityMode =
|
||||||
|
searchParams.get("includeInternal") === "true" ? "internal" : "public";
|
||||||
const [selectedTenantFilter, setSelectedTenantFilter] =
|
const [selectedTenantFilter, setSelectedTenantFilter] =
|
||||||
React.useState(FAMILY_FILTER_ID);
|
React.useState(FAMILY_FILTER_ID);
|
||||||
const [collapsedIds, setCollapsedIds] = React.useState<Set<string>>(
|
const [collapsedIds, setCollapsedIds] = React.useState<Set<string>>(
|
||||||
@@ -1270,7 +1272,7 @@ export function TenantOrgChartPage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const rootNodes = buildTenantFullTree(
|
const rootNodes = buildTenantFullTree(
|
||||||
filterSystemGlobalTenants(tenantsQuery.data.items, "internal"),
|
filterSystemGlobalTenants(tenantsQuery.data.items, visibilityMode),
|
||||||
).subTree;
|
).subTree;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -1280,7 +1282,13 @@ export function TenantOrgChartPage() {
|
|||||||
}),
|
}),
|
||||||
sharedWith: "",
|
sharedWith: "",
|
||||||
};
|
};
|
||||||
}, [publicQuery.data, shareToken, tenantsQuery.data, usersQuery.data]);
|
}, [
|
||||||
|
publicQuery.data,
|
||||||
|
shareToken,
|
||||||
|
tenantsQuery.data,
|
||||||
|
usersQuery.data,
|
||||||
|
visibilityMode,
|
||||||
|
]);
|
||||||
|
|
||||||
const familyRoot = React.useMemo(() => {
|
const familyRoot = React.useMemo(() => {
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ function PickerScenarioControls({
|
|||||||
onChange: (options: OrgPickerEmbedOptions) => void;
|
onChange: (options: OrgPickerEmbedOptions) => void;
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<div className="grid gap-3 rounded-md border border-border bg-card p-4 md:grid-cols-2 lg:grid-cols-[1fr,1fr,1fr,auto,auto,auto,auto] lg:items-end">
|
<div className="grid gap-3 rounded-md border border-border bg-card p-4 md:grid-cols-2 lg:grid-cols-[1fr,1fr,1fr,auto,auto,auto,auto,auto] lg:items-end">
|
||||||
<label className="space-y-1 text-sm font-medium">
|
<label className="space-y-1 text-sm font-medium">
|
||||||
<span className="block text-muted-foreground">선택 모드</span>
|
<span className="block text-muted-foreground">선택 모드</span>
|
||||||
<select
|
<select
|
||||||
@@ -104,6 +104,20 @@ function PickerScenarioControls({
|
|||||||
<span>하위 선택 스위치 표시</span>
|
<span>하위 선택 스위치 표시</span>
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
|
<label className="flex h-10 items-center gap-2 rounded-md border border-border bg-background px-3 text-sm">
|
||||||
|
<input
|
||||||
|
checked={options.includeInternal}
|
||||||
|
onChange={(event) =>
|
||||||
|
onChange({
|
||||||
|
...options,
|
||||||
|
includeInternal: event.target.checked,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
type="checkbox"
|
||||||
|
/>
|
||||||
|
<span>internal 표시</span>
|
||||||
|
</label>
|
||||||
|
|
||||||
<label className="space-y-1 text-sm font-medium">
|
<label className="space-y-1 text-sm font-medium">
|
||||||
<span className="block text-muted-foreground">임베딩 너비</span>
|
<span className="block text-muted-foreground">임베딩 너비</span>
|
||||||
<input
|
<input
|
||||||
|
|||||||
@@ -333,6 +333,7 @@ export function OrgPickerEmbedPage() {
|
|||||||
const mode = parseOrgPickerMode(searchParams.get("mode"));
|
const mode = parseOrgPickerMode(searchParams.get("mode"));
|
||||||
const select = parseOrgPickerSelectableType(searchParams.get("select"));
|
const select = parseOrgPickerSelectableType(searchParams.get("select"));
|
||||||
const rootTenantId = searchParams.get("rootTenantId") || undefined;
|
const rootTenantId = searchParams.get("rootTenantId") || undefined;
|
||||||
|
const includeInternal = searchParams.get("includeInternal") === "true";
|
||||||
const tenantId =
|
const tenantId =
|
||||||
searchParams.get("tenantSlug") ||
|
searchParams.get("tenantSlug") ||
|
||||||
searchParams.get("tenantId") ||
|
searchParams.get("tenantId") ||
|
||||||
@@ -363,12 +364,20 @@ export function OrgPickerEmbedPage() {
|
|||||||
|
|
||||||
const tree = React.useMemo(() => {
|
const tree = React.useMemo(() => {
|
||||||
return buildOrgPickerTree({
|
return buildOrgPickerTree({
|
||||||
|
includeInternal,
|
||||||
tenants: tenantsQuery.data?.items ?? [],
|
tenants: tenantsQuery.data?.items ?? [],
|
||||||
users: select === "tenant" ? [] : (usersQuery.data?.items ?? []),
|
users: select === "tenant" ? [] : (usersQuery.data?.items ?? []),
|
||||||
rootTenantId,
|
rootTenantId,
|
||||||
tenantId,
|
tenantId,
|
||||||
});
|
});
|
||||||
}, [rootTenantId, select, tenantId, tenantsQuery.data, usersQuery.data]);
|
}, [
|
||||||
|
includeInternal,
|
||||||
|
rootTenantId,
|
||||||
|
select,
|
||||||
|
tenantId,
|
||||||
|
tenantsQuery.data,
|
||||||
|
usersQuery.data,
|
||||||
|
]);
|
||||||
|
|
||||||
const selectedItems = React.useMemo(
|
const selectedItems = React.useMemo(
|
||||||
() =>
|
() =>
|
||||||
@@ -579,7 +588,7 @@ export function OrgPickerPage() {
|
|||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<section className="grid gap-3 rounded-md border border-border bg-card p-4 md:grid-cols-2 lg:grid-cols-[1fr,1fr,1fr,auto,auto,auto,auto] lg:items-end">
|
<section className="grid gap-3 rounded-md border border-border bg-card p-4 md:grid-cols-2 lg:grid-cols-[1fr,1fr,1fr,auto,auto,auto,auto,auto] lg:items-end">
|
||||||
<label className="space-y-1 text-sm font-medium">
|
<label className="space-y-1 text-sm font-medium">
|
||||||
<span className="block text-muted-foreground">선택 모드</span>
|
<span className="block text-muted-foreground">선택 모드</span>
|
||||||
<select
|
<select
|
||||||
@@ -661,6 +670,20 @@ export function OrgPickerPage() {
|
|||||||
<span>하위 선택 스위치 표시</span>
|
<span>하위 선택 스위치 표시</span>
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
|
<label className="flex h-10 items-center gap-2 rounded-md border border-border bg-background px-3 text-sm">
|
||||||
|
<input
|
||||||
|
checked={options.includeInternal}
|
||||||
|
onChange={(event) =>
|
||||||
|
setOptions((current) => ({
|
||||||
|
...current,
|
||||||
|
includeInternal: event.target.checked,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
type="checkbox"
|
||||||
|
/>
|
||||||
|
<span>internal 표시</span>
|
||||||
|
</label>
|
||||||
|
|
||||||
<label className="space-y-1 text-sm font-medium">
|
<label className="space-y-1 text-sm font-medium">
|
||||||
<span className="block text-muted-foreground">임베딩 너비</span>
|
<span className="block text-muted-foreground">임베딩 너비</span>
|
||||||
<input
|
<input
|
||||||
|
|||||||
@@ -17,13 +17,19 @@ do
|
|||||||
assert_contains "$workflow" "APP_ENV=stage"
|
assert_contains "$workflow" "APP_ENV=stage"
|
||||||
assert_contains "$workflow" "BACKEND_LOG_LEVEL=debug"
|
assert_contains "$workflow" "BACKEND_LOG_LEVEL=debug"
|
||||||
assert_contains "$workflow" "CLIENT_LOG_DEBUG=true"
|
assert_contains "$workflow" "CLIENT_LOG_DEBUG=true"
|
||||||
|
assert_contains "$workflow" 'WORKS_ADMIN_API_BASE_URL=${{ vars.WORKS_ADMIN_API_BASE_URL }}'
|
||||||
|
assert_contains "$workflow" 'WORKS_ADMIN_OAUTH_TOKEN_URL=${{ vars.WORKS_ADMIN_OAUTH_TOKEN_URL }}'
|
||||||
assert_contains "$workflow" 'BACKEND_PUBLIC_URL=${{ vars.BACKEND_URL }}'
|
assert_contains "$workflow" 'BACKEND_PUBLIC_URL=${{ vars.BACKEND_URL }}'
|
||||||
done
|
done
|
||||||
|
|
||||||
assert_contains ".gitea/workflows/staging_release.yml" "scp adminfront/seed-tenant.csv"
|
assert_contains ".gitea/workflows/staging_release.yml" "scp adminfront/seed-tenant.csv"
|
||||||
|
assert_contains "docker-compose.yaml" 'WORKS_ADMIN_API_BASE_URL=${WORKS_ADMIN_API_BASE_URL}'
|
||||||
|
assert_contains "docker-compose.yaml" 'WORKS_ADMIN_OAUTH_TOKEN_URL=${WORKS_ADMIN_OAUTH_TOKEN_URL}'
|
||||||
assert_contains "docker/docker-compose.staging.template.yaml" "SEED_TENANT_CSV_PATH=/app/seed-tenant.csv"
|
assert_contains "docker/docker-compose.staging.template.yaml" "SEED_TENANT_CSV_PATH=/app/seed-tenant.csv"
|
||||||
assert_contains "docker/docker-compose.staging.template.yaml" "./adminfront/seed-tenant.csv:/app/seed-tenant.csv:ro"
|
assert_contains "docker/docker-compose.staging.template.yaml" "./adminfront/seed-tenant.csv:/app/seed-tenant.csv:ro"
|
||||||
assert_contains "docker/staging_pull_compose.template.yaml" "SEED_TENANT_CSV_PATH=/app/seed-tenant.csv"
|
assert_contains "docker/staging_pull_compose.template.yaml" "SEED_TENANT_CSV_PATH=/app/seed-tenant.csv"
|
||||||
assert_contains "docker/staging_pull_compose.template.yaml" "./adminfront/seed-tenant.csv:/app/seed-tenant.csv:ro"
|
assert_contains "docker/staging_pull_compose.template.yaml" "./adminfront/seed-tenant.csv:/app/seed-tenant.csv:ro"
|
||||||
|
assert_contains "docker/staging_pull_compose.template.yaml" 'WORKS_ADMIN_API_BASE_URL=${WORKS_ADMIN_API_BASE_URL}'
|
||||||
|
assert_contains "docker/staging_pull_compose.template.yaml" 'WORKS_ADMIN_OAUTH_TOKEN_URL=${WORKS_ADMIN_OAUTH_TOKEN_URL}'
|
||||||
|
|
||||||
echo "staging workflow env checks passed"
|
echo "staging workflow env checks passed"
|
||||||
|
|||||||
Reference in New Issue
Block a user