1
0
forked from baron/baron-sso

dev 브런치 병합 후 code check

This commit is contained in:
2026-04-06 16:03:49 +09:00
parent e3d279cb83
commit 1b8dc2c4ab
8 changed files with 245 additions and 129 deletions

View File

@@ -121,6 +121,18 @@ func (m *e2eMockKratosAdminService) DeleteIdentity(ctx context.Context, identity
return nil
}
func (m *e2eMockKratosAdminService) ListIdentitySessions(ctx context.Context, identityID string) ([]service.KratosSession, error) {
return nil, nil
}
func (m *e2eMockKratosAdminService) GetSession(ctx context.Context, sessionID string) (*service.KratosSession, error) {
return nil, nil
}
func (m *e2eMockKratosAdminService) DeleteSession(ctx context.Context, sessionID string) error {
return nil
}
func newHeadlessLoginE2EApp(h *authhandler.AuthHandler, appEnv string) *fiber.App {
app := fiber.New(fiber.Config{
DisableStartupMessage: true,

View File

@@ -7126,6 +7126,10 @@ func isPrivateIPAddress(raw string) bool {
return utils.IsPrivateOrReservedIP(raw)
}
func parseAuditDetails(details string) (map[string]any, error) {
return utils.ParseAuditDetails(details)
}
func deriveSessionClientInfo(log domain.AuditLog) (string, string) {
details, _ := parseAuditDetails(log.Details)
clientID := ""

View File

@@ -80,6 +80,7 @@ func (m *AsyncMockUserRepo) Create(ctx context.Context, user *domain.User) error
}
return args.Error(0)
}
func (m *AsyncMockUserRepo) Update(ctx context.Context, user *domain.User) error {
args := m.Called(ctx, user)
if m.createCalled != nil {
@@ -87,6 +88,7 @@ func (m *AsyncMockUserRepo) Update(ctx context.Context, user *domain.User) error
}
return args.Error(0)
}
func (m *AsyncMockUserRepo) Delete(ctx context.Context, id string) error { return nil }
func (m *AsyncMockUserRepo) FindByEmail(ctx context.Context, email string) (*domain.User, error) {
return nil, nil

View File

@@ -137,6 +137,7 @@ func (m *MockUserRepoForTenant) CountByTenantIDs(ctx context.Context, tenantIDs
}
return args.Get(0).(map[string]int64), args.Error(1)
}
func (m *MockUserRepoForTenant) CountByCompanyCodes(ctx context.Context, codes []string) (map[string]int64, error) {
args := m.Called(ctx, codes)
if args.Get(0) == nil {

View File

@@ -1,4 +1,4 @@
import { expect, test, type Page, type Route } from '@playwright/test';
import { expect, test, type Locator, type Page, type Route } from '@playwright/test';
type RequestCapture = {
loginBody?: Record<string, unknown>;
@@ -7,15 +7,26 @@ type RequestCapture = {
clientLogs: string[];
};
const resetNewPasswordName = /^(새 비밀번호|ui\.userfront\.reset\.new_password)$/;
const resetConfirmPasswordName =
/^(새 비밀번호 확인|ui\.userfront\.reset\.confirm_password)$/;
const resetSubmitButtonName = /^(비밀번호 변경|ui\.userfront\.reset\.submit)$/;
async function enableFlutterAccessibility(page: Page): Promise<void> {
await page.waitForTimeout(300);
const button = page.getByRole('button', { name: 'Enable accessibility' });
if (await button.count()) {
await button.click({ force: true });
const placeholder = page.locator('flt-semantics-placeholder');
if (await placeholder.count()) {
await placeholder.first().click({ force: true });
}
await button.first().evaluate((node) => {
(node as HTMLElement).click();
});
await page.waitForTimeout(200);
return;
}
await page.waitForTimeout(300);
const placeholder = page.locator('flt-semantics-placeholder').first();
if (await placeholder.count()) {
await placeholder.evaluate((node) => {
(node as HTMLElement).click();
});
await page.waitForTimeout(800);
}
}
@@ -109,6 +120,18 @@ async function fillAt(page: Page, x: number, y: number, value: string): Promise<
await page.keyboard.type(value);
}
async function typeIntoAccessibleField(
page: Page,
field: Locator,
value: string,
): Promise<void> {
await field.click({ force: true });
await page.waitForTimeout(100);
await page.keyboard.press('Control+A');
await page.keyboard.press('Backspace');
await page.keyboard.type(value);
}
async function fillPasswordLoginForm(
page: Page,
loginId: string,
@@ -128,25 +151,29 @@ async function fillPasswordLoginForm(
async function submitPasswordLogin(page: Page): Promise<void> {
if (isMobileProject(page)) {
await enableFlutterAccessibility(page);
await page.getByRole('button', { name: '로그인' }).click({ force: true });
return;
}
const coords = coordsFor(page);
await page.locator('flt-glass-pane').click({
position: { x: coords.signinSubmitX, y: coords.signinSubmitY },
force: true,
});
await page.keyboard.press('Enter');
}
async function fillResetPasswordForm(page: Page, password: string): Promise<void> {
await enableFlutterAccessibility(page);
const newPasswordInput = page.getByRole('textbox', {
name: resetNewPasswordName,
});
const confirmPasswordInput = page.getByRole('textbox', {
name: resetConfirmPasswordName,
});
if ((await newPasswordInput.count()) > 0 && (await confirmPasswordInput.count()) > 0) {
await typeIntoAccessibleField(page, newPasswordInput, password);
await typeIntoAccessibleField(page, confirmPasswordInput, password);
return;
}
if (isMobileProject(page)) {
await enableFlutterAccessibility(page);
await page
.getByRole('textbox', { name: /^새 비밀번호$/ })
.fill(password);
await page
.getByRole('textbox', { name: /^새 비밀번호 확인$/ })
.fill(password);
await page.getByRole('textbox', { name: resetNewPasswordName }).fill(password);
await page.getByRole('textbox', { name: resetConfirmPasswordName }).fill(password);
return;
}
const coords = coordsFor(page);
@@ -160,8 +187,13 @@ async function fillResetPasswordForm(page: Page, password: string): Promise<void
}
async function submitResetPassword(page: Page): Promise<void> {
await enableFlutterAccessibility(page);
const submitButton = page.getByRole('button', { name: resetSubmitButtonName });
if ((await submitButton.count()) > 0) {
await submitButton.click({ force: true });
return;
}
if (isMobileProject(page)) {
await page.getByRole('button', { name: '비밀번호 변경' }).click({ force: true });
return;
}
const coords = coordsFor(page);

View File

@@ -40,9 +40,6 @@ verify_code_failed = "인증 실패: {error}"
[err.userfront.session]
missing = "활성 세션이 없습니다."
[msg.userfront]
greeting = "안녕하세요, {name}님"
[msg.userfront.audit]
date = "접속일자: {value}"
device = "접속환경: {value}"
@@ -53,27 +50,6 @@ result = "인증결과: {value}"
session_id = "Session ID: {value}"
status = "현황: (준비중)"
[msg.userfront.consent]
accept_error = "동의 처리에 실패했습니다: {error}"
client_id = "클라이언트 ID: {id}"
client_unknown = "알 수 없는 앱"
description = "아래 서비스가 회원님의 계정 정보에 접근하려고 합니다.\\\\n계속 진행하려면 동의 여부를 선택해 주세요."
load_error = "동의 정보를 불러오는데 실패했습니다: {error}"
missing_redirect = "동의가 처리되었으나 리다이렉트 URL을 받지 못했습니다."
redirect_notice = "동의 후 자동으로 서비스로 이동합니다."
scope_count = "총 {count}개"
[msg.userfront.consent.cancel]
confirm = "권한 동의를 취소하면 해당 서비스를 이용할 수 없습니다. 취소하시겠습니까?"
error = "취소 처리 중 오류가 발생했습니다: {error}"
[msg.userfront.consent.scope]
email = "이메일 주소 (계정 식별 및 알림 용도)"
offline_access = "오프라인 접근 (로그인 유지)"
openid = "OpenID 인증 정보 (로그인 상태 확인)"
phone = "휴대폰 번호 (본인 인증 및 알림)"
profile = "기본 프로필 정보 (이름, 사용자 식별자)"
[msg.userfront.dashboard]
approved_device = "승인 기기: {device}"
approved_ip = "승인 IP: {ip}"
@@ -89,27 +65,6 @@ link_open_error = "해당 링크를 열 수 없습니다."
render_error = "대시보드 렌더링 오류: {error}"
session_id_copied = "세션 ID가 복사되었습니다."
[msg.userfront.dashboard.activities]
empty = "연동된 앱이 없습니다."
empty_detail = "앱을 연동하면 최근 활동과 상태가 표시됩니다."
error = "연동 정보를 불러오지 못했습니다."
[msg.userfront.dashboard.approved_session]
copy_click = "{label}: {id}\\\\n클릭하면 복사됩니다."
copy_tap = "{label}: {id}\\\\n탭하면 복사됩니다."
none = "{label} 없음"
[msg.userfront.dashboard.revoke]
confirm = "{app} 앱과의 연동을 해지하시겠습니까?\\\\n해지하면 다음 로그인 시 다시 동의가 필요합니다."
error = "해지 실패: {error}"
success = "{app} 연동이 해지되었습니다."
[msg.userfront.dashboard.scopes]
empty = "요청된 권한이 없습니다."
[msg.userfront.dashboard.timeline]
load_error = "접속이력을 불러오지 못했습니다."
[msg.userfront.error]
detail_contact = "관리자에게 문의해 주세요."
detail_generic = "오류가 발생했습니다."
@@ -120,34 +75,6 @@ title_generic = "오류가 발생했습니다"
title_with_code = "오류: {code}"
type = "오류 종류: {type}"
[msg.userfront.error.ory]
"$normalizedCode" = "{error}"
access_denied = "사용자가 동의를 거부했습니다."
consent_required = "앱 접근 동의가 필요합니다."
interaction_required = "추가 상호작용이 필요합니다. 다시 시도해 주세요."
invalid_client = "클라이언트 인증 정보가 유효하지 않습니다."
invalid_grant = "인증 요청이 만료되었거나 유효하지 않습니다."
invalid_request = "잘못된 요청입니다."
invalid_scope = "요청한 권한 범위가 유효하지 않습니다."
login_required = "로그인이 필요합니다."
request_forbidden = "요청이 거부되었습니다."
server_error = "인증 서버 오류가 발생했습니다."
temporarily_unavailable = "인증 서버를 일시적으로 사용할 수 없습니다."
unauthorized_client = "해당 클라이언트는 이 요청을 수행할 수 없습니다."
unsupported_response_type = "지원하지 않는 응답 타입입니다."
[msg.userfront.error.whitelist]
"$normalizedCode" = "{error}"
bad_request = "입력값을 확인해 주세요."
invalid_session = "세션이 만료되었습니다. 다시 로그인해 주세요."
not_found = "요청한 페이지를 찾을 수 없습니다."
password_or_email_mismatch = "이메일 혹은 비밀번호가 일치하지 않습니다."
rate_limited = "요청이 많습니다. 잠시 후 다시 시도해 주세요."
recovery_expired = "재설정 링크가 만료되었습니다. 다시 요청해 주세요."
recovery_invalid = "재설정 링크가 유효하지 않습니다."
settings_disabled = "현재 계정 설정 화면은 준비 중입니다."
verification_required = "추가 인증이 필요합니다. 안내에 따라 진행해 주세요."
[msg.userfront.forgot]
description = "계정과 연결된 이메일 주소 또는 휴대폰 번호를 입력하시면, 비밀번호를 재설정할 수 있는 링크를 보내드립니다."
dry_send = "drySend 모드: 실제 이메일/SMS는 발송되지 않습니다."
@@ -317,6 +244,55 @@ complete = "가입 완료"
next_step = "다음 단계"
title = "회원가입"
[msg.userfront]
greeting = "안녕하세요, {name}님"
[msg.userfront.audit]
date = "접속일자: {value}"
device = "접속환경: {value}"
end = "더 이상 항목이 없습니다."
ip = "접속 IP: {value}"
load_more_error = "더 불러오지 못했습니다."
result = "인증결과: {value}"
session_id = "Session ID: {value}"
status = "현황: (준비중)"
[msg.userfront.consent]
accept_error = "동의 처리에 실패했습니다: {error}"
client_id = "클라이언트 ID: {id}"
client_unknown = "알 수 없는 앱"
description = "아래 서비스가 회원님의 계정 정보에 접근하려고 합니다.\\\\n계속 진행하려면 동의 여부를 선택해 주세요."
load_error = "동의 정보를 불러오는데 실패했습니다: {error}"
missing_redirect = "동의가 처리되었으나 리다이렉트 URL을 받지 못했습니다."
redirect_notice = "동의 후 자동으로 서비스로 이동합니다."
scope_count = "총 {count}개"
[msg.userfront.consent.cancel]
confirm = "권한 동의를 취소하면 해당 서비스를 이용할 수 없습니다. 취소하시겠습니까?"
error = "취소 처리 중 오류가 발생했습니다: {error}"
[msg.userfront.consent.scope]
email = "이메일 주소 (계정 식별 및 알림 용도)"
offline_access = "오프라인 접근 (로그인 유지)"
openid = "OpenID 인증 정보 (로그인 상태 확인)"
phone = "휴대폰 번호 (본인 인증 및 알림)"
profile = "기본 프로필 정보 (이름, 사용자 식별자)"
[msg.userfront.dashboard]
approved_device = "승인 기기: {device}"
approved_ip = "승인 IP: {ip}"
audit_empty = "최근 접속 이력이 없습니다."
audit_load_error = "접속이력을 불러오지 못했습니다."
auth_method = "인증수단: {method}"
client_id = "Client ID: {id}"
client_id_missing = "Client ID 없음"
current_status = "현재 상태: {status}"
last_auth = "최근 인증: {value}"
link_missing = "이동할 페이지 주소(Client URI)가 설정되지 않았습니다."
link_open_error = "해당 링크를 열 수 없습니다."
render_error = "대시보드 렌더링 오류: {error}"
session_id_copied = "세션 ID가 복사되었습니다."
[msg.userfront.dashboard.activities]
empty = "연동된 앱이 없습니다."
empty_detail = "앱을 연동하면 최근 활동과 상태가 표시됩니다."
@@ -337,12 +313,12 @@ error = "세션 종료 실패: {error}"
success = "세션이 종료되었습니다."
[msg.userfront.dashboard.approved_session]
copy_click = "{label}: {id}\n클릭하면 복사됩니다."
copy_tap = "{label}: {id}\n탭하면 복사됩니다."
copy_click = "{label}: {id}\\\\n클릭하면 복사됩니다."
copy_tap = "{label}: {id}\\\\n탭하면 복사됩니다."
none = "{label} 없음"
[msg.userfront.dashboard.revoke]
confirm = "{app} 앱과의 연동을 해지하시겠습니까?\n해지하면 다음 로그인 시 다시 동의가 필요합니다."
confirm = "{app} 앱과의 연동을 해지하시겠습니까?\\\\n해지하면 다음 로그인 시 다시 동의가 필요합니다."
error = "해지 실패: {error}"
success = "{app} 연동이 해지되었습니다."
@@ -352,17 +328,15 @@ empty = "요청된 권한이 없습니다."
[msg.userfront.dashboard.timeline]
load_error = "접속이력을 불러오지 못했습니다."
[msg.userfront.error.whitelist]
"$normalizedCode" = "{error}"
bad_request = "입력값을 확인해 주세요."
invalid_session = "세션이 만료되었습니다. 다시 로그인해 주세요."
not_found = "요청한 페이지를 찾을 수 없습니다."
password_or_email_mismatch = "이메일 혹은 비밀번호가 일치하지 않습니다."
rate_limited = "요청이 많습니다. 잠시 후 다시 시도해 주세요."
recovery_expired = "재설정 링크가 만료되었습니다. 다시 요청해 주세요."
recovery_invalid = "재설정 링크가 유효하지 않습니다."
settings_disabled = "현재 계정 설정 화면은 준비 중입니다."
verification_required = "추가 인증이 필요합니다. 안내에 따라 진행해 주세요."
[msg.userfront.error]
detail_contact = "관리자에게 문의해 주세요."
detail_generic = "오류가 발생했습니다."
detail_request = "요청을 처리하는 중 문제가 발생했습니다."
id = "오류 ID: {id}"
title = "인증 과정에서 오류가 발생했습니다"
title_generic = "오류가 발생했습니다"
title_with_code = "오류: {code}"
type = "오류 종류: {type}"
[msg.userfront.error.ory]
"$normalizedCode" = "{error}"
@@ -380,6 +354,41 @@ temporarily_unavailable = "인증 서버를 일시적으로 사용할 수 없습
unauthorized_client = "해당 클라이언트는 이 요청을 수행할 수 없습니다."
unsupported_response_type = "지원하지 않는 응답 타입입니다."
[msg.userfront.error.whitelist]
"$normalizedCode" = "{error}"
bad_request = "입력값을 확인해 주세요."
invalid_session = "세션이 만료되었습니다. 다시 로그인해 주세요."
not_found = "요청한 페이지를 찾을 수 없습니다."
password_or_email_mismatch = "이메일 혹은 비밀번호가 일치하지 않습니다."
rate_limited = "요청이 많습니다. 잠시 후 다시 시도해 주세요."
recovery_expired = "재설정 링크가 만료되었습니다. 다시 요청해 주세요."
recovery_invalid = "재설정 링크가 유효하지 않습니다."
settings_disabled = "현재 계정 설정 화면은 준비 중입니다."
verification_required = "추가 인증이 필요합니다. 안내에 따라 진행해 주세요."
[msg.userfront.forgot]
description = "계정과 연결된 이메일 주소 또는 휴대폰 번호를 입력하시면, 비밀번호를 재설정할 수 있는 링크를 보내드립니다."
dry_send = "drySend 모드: 실제 이메일/SMS는 발송되지 않습니다."
error = "전송에 실패했습니다: {error}"
input_required = "이메일 또는 휴대폰 번호를 입력해주세요."
sent = "비밀번호 재설정 링크가 전송되었습니다. 이메일 또는 SMS를 확인해주세요."
[msg.userfront.login]
cookie_check_failed = "로그인 확인 실패: {error}"
dry_send = "drySend 모드: 실제 이메일/SMS는 발송되지 않습니다."
link_failed = "오류: {error}"
link_send_failed = "전송 실패: {error}"
link_sent_email = "입력하신 이메일로 로그인 링크를 보냈습니다."
link_sent_phone = "입력하신 번호로 로그인 링크를 보냈습니다."
link_timeout = "시간이 경과되었습니다."
no_account = "계정이 없으신가요?"
oidc_failed = "OIDC 로그인 처리에 실패했습니다. 다시 시도해 주세요."
qr_expired = "시간이 경과되었습니다."
qr_init_failed = "QR 초기화에 실패했습니다: {error}"
qr_login_required = "로그인 한 상태여야 QR 스캔으로 로그인 할 수 있습니다"
token_missing = "로그인 토큰을 확인할 수 없습니다."
verification_failed = "승인 처리에 실패했습니다: {error}"
[msg.userfront.login.link]
approved = "msg.userfront.login.link.approved"
helper = "입력하신 정보로 로그인 링크를 전송합니다."

View File

@@ -40,18 +40,41 @@ verify_code_failed = ""
[err.userfront.session]
missing = ""
[msg.userfront]
greeting = ""
[msg.userfront.error]
detail_contact = ""
detail_generic = ""
detail_request = ""
id = ""
title = ""
title_generic = ""
title_with_code = ""
type = ""
[msg.userfront.audit]
date = ""
device = ""
end = ""
ip = ""
load_more_error = ""
result = ""
session_id = ""
status = ""
[msg.userfront.forgot]
description = ""
dry_send = ""
error = ""
input_required = ""
sent = ""
[msg.userfront.login]
cookie_check_failed = ""
dry_send = ""
link_failed = ""
link_send_failed = ""
link_sent_email = ""
link_sent_phone = ""
link_timeout = ""
no_account = ""
oidc_failed = ""
qr_expired = ""
qr_init_failed = ""
qr_login_required = ""
token_missing = ""
verification_failed = ""
[msg.userfront.login_success]
subtitle = ""
[msg.userfront.consent]
accept_error = ""
@@ -63,16 +86,6 @@ missing_redirect = ""
redirect_notice = ""
scope_count = ""
[msg.userfront.consent.cancel]
confirm = ""
error = ""
[msg.userfront.consent.scope]
email = ""
offline_access = ""
openid = ""
phone = ""
[msg.userfront.profile]
department_missing = ""
department_required = ""
@@ -206,6 +219,40 @@ complete = ""
next_step = ""
title = ""
[msg.userfront]
greeting = ""
[msg.userfront.audit]
date = ""
device = ""
end = ""
ip = ""
load_more_error = ""
result = ""
session_id = ""
status = ""
[msg.userfront.consent]
accept_error = ""
client_id = ""
client_unknown = ""
description = ""
load_error = ""
missing_redirect = ""
redirect_notice = ""
scope_count = ""
[msg.userfront.consent.cancel]
confirm = ""
error = ""
[msg.userfront.consent.scope]
email = ""
offline_access = ""
openid = ""
phone = ""
profile = ""
[msg.userfront.dashboard]
approved_device = ""
approved_ip = ""

View File

@@ -1032,13 +1032,22 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
webWindow.redirectTo(redirectTo);
} else {}
} catch (e) {
final errorMessage = e.toString().replaceFirst('Exception: ', '');
try {
await AuthProxyService.logError(
'[PasswordLogin] $errorMessage',
error: e,
);
} catch (_) {
// Ignore client-log relay failures and continue with user feedback.
}
if (e.toString().contains("User not registered")) {
_showUnregisteredDialog();
} else {
_showError(
tr(
'msg.userfront.login.password.failed',
params: {'error': e.toString().replaceFirst('Exception: ', '')},
params: {'error': errorMessage},
),
);
}