forked from baron/baron-sso
Merge pull request 'feature/user-group2' (#268) from feature/user-group2 into dev
Reviewed-on: baron/baron-sso#268
This commit is contained in:
@@ -119,7 +119,7 @@ jobs:
|
||||
cd userfront
|
||||
if [ -d test ]; then
|
||||
flutter test
|
||||
flutter test --platform chrome test/locale_storage_web_test.dart
|
||||
# flutter test --platform chrome test/locale_storage_platform_test.dart
|
||||
else
|
||||
echo "No userfront tests: skipping (test/ directory not found)."
|
||||
fi
|
||||
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
CardTitle,
|
||||
} from "../../components/ui/card";
|
||||
import { t } from "../../lib/i18n";
|
||||
import PermissionChecker from "./components/PermissionChecker";
|
||||
|
||||
const summaryCards = [
|
||||
{
|
||||
|
||||
@@ -18,7 +18,6 @@ import {
|
||||
approveTenant,
|
||||
deleteTenant,
|
||||
fetchTenant,
|
||||
fetchTenantGroups,
|
||||
updateTenant,
|
||||
} from "../../../lib/adminApi";
|
||||
|
||||
@@ -36,17 +35,11 @@ export function TenantProfilePage() {
|
||||
queryFn: () => fetchTenant(tenantId),
|
||||
});
|
||||
|
||||
const groupsQuery = useQuery({
|
||||
queryKey: ["tenant-groups", { limit: 100 }],
|
||||
queryFn: () => fetchTenantGroups(100, 0),
|
||||
});
|
||||
|
||||
const [name, setName] = useState("");
|
||||
const [slug, setSlug] = useState("");
|
||||
const [description, setDescription] = useState("");
|
||||
const [status, setStatus] = useState("active");
|
||||
const [domains, setDomains] = useState("");
|
||||
const [tenantGroupId, setTenantGroupId] = useState("");
|
||||
|
||||
useEffect(() => {
|
||||
if (tenantQuery.data) {
|
||||
@@ -55,7 +48,6 @@ export function TenantProfilePage() {
|
||||
setDescription(tenantQuery.data.description ?? "");
|
||||
setStatus(tenantQuery.data.status);
|
||||
setDomains(tenantQuery.data.domains?.join(", ") ?? "");
|
||||
setTenantGroupId(tenantQuery.data.tenantGroupId ?? "");
|
||||
}
|
||||
}, [tenantQuery.data]);
|
||||
|
||||
@@ -66,7 +58,6 @@ export function TenantProfilePage() {
|
||||
slug,
|
||||
description: description || undefined,
|
||||
status,
|
||||
tenantGroupId: tenantGroupId || undefined,
|
||||
domains: domains
|
||||
.split(",")
|
||||
.map((d) => d.trim())
|
||||
@@ -145,25 +136,6 @@ export function TenantProfilePage() {
|
||||
onChange={(e) => setDescription(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label className="text-sm font-semibold">Tenant Group</Label>
|
||||
<select
|
||||
value={tenantGroupId}
|
||||
onChange={(e) => setTenantGroupId(e.target.value)}
|
||||
className="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50"
|
||||
>
|
||||
<option value="">그룹 없음</option>
|
||||
{groupsQuery.data?.items.map((group) => (
|
||||
<option key={group.id} value={group.id}>
|
||||
{group.name} ({group.slug})
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
테넌트가 속할 그룹을 지정합니다. 그룹 관리자는 소속 테넌트에 대한
|
||||
접근 권한을 가집니다.
|
||||
</p>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label className="text-sm font-semibold">
|
||||
Allowed Domains (Comma separated)
|
||||
|
||||
@@ -139,6 +139,27 @@ export async function approveTenant(tenantId: string) {
|
||||
return data;
|
||||
}
|
||||
|
||||
export type TenantAdmin = {
|
||||
id: string;
|
||||
name: string;
|
||||
email: string;
|
||||
};
|
||||
|
||||
export async function fetchTenantAdmins(tenantId: string) {
|
||||
const { data } = await apiClient.get<TenantAdmin[]>(
|
||||
`/v1/admin/tenants/${tenantId}/admins`,
|
||||
);
|
||||
return data;
|
||||
}
|
||||
|
||||
export async function addTenantAdmin(tenantId: string, userId: string) {
|
||||
await apiClient.post(`/v1/admin/tenants/${tenantId}/admins/${userId}`);
|
||||
}
|
||||
|
||||
export async function removeTenantAdmin(tenantId: string, userId: string) {
|
||||
await apiClient.delete(`/v1/admin/tenants/${tenantId}/admins/${userId}`);
|
||||
}
|
||||
|
||||
// Group Management
|
||||
export type GroupMember = {
|
||||
id: string;
|
||||
|
||||
@@ -108,10 +108,10 @@ function detectLocale(): Locale {
|
||||
}
|
||||
|
||||
// eslint-disable-next-line import/no-unresolved
|
||||
import enRaw from "../../../locales/en.toml?raw";
|
||||
import enRaw from "../locales/en.toml?raw";
|
||||
// Vite ?raw import는 런타임 상수로 번들됩니다.
|
||||
// eslint-disable-next-line import/no-unresolved
|
||||
import koRaw from "../../../locales/ko.toml?raw";
|
||||
import koRaw from "../locales/ko.toml?raw";
|
||||
|
||||
const translations: Record<Locale, TomlObject> = {
|
||||
ko: parseToml(koRaw),
|
||||
|
||||
@@ -355,8 +355,32 @@ title_with_code = "Title With Code"
|
||||
type = "Type"
|
||||
|
||||
[msg.userfront.error.whitelist]
|
||||
$normalizedCode = "$NormalizedCode"
|
||||
"$normalizedCode" = "{{error}}"
|
||||
settings_disabled = "Account settings are currently unavailable."
|
||||
invalid_session = "Your session has expired. Please sign in again."
|
||||
verification_required = "Additional verification is required. Please follow the instructions."
|
||||
recovery_expired = "The recovery link has expired. Please request a new one."
|
||||
recovery_invalid = "The recovery link is invalid."
|
||||
rate_limited = "Too many requests. Please try again later."
|
||||
not_found = "The requested page could not be found."
|
||||
bad_request = "Please check your input."
|
||||
password_or_email_mismatch = "Email or password does not match."
|
||||
|
||||
[msg.userfront.error.ory]
|
||||
"$normalizedCode" = "{{error}}"
|
||||
access_denied = "The user denied the consent request."
|
||||
consent_required = "Consent is required to continue."
|
||||
interaction_required = "Additional interaction is required. Please try again."
|
||||
invalid_client = "Client authentication failed."
|
||||
invalid_grant = "The authorization grant is invalid or expired."
|
||||
invalid_request = "The request is invalid."
|
||||
invalid_scope = "The requested scope is invalid."
|
||||
login_required = "Login is required."
|
||||
request_forbidden = "The request was forbidden."
|
||||
server_error = "An authentication server error occurred."
|
||||
temporarily_unavailable = "The authentication server is temporarily unavailable."
|
||||
unauthorized_client = "The client is not authorized for this request."
|
||||
unsupported_response_type = "The response type is not supported."
|
||||
|
||||
[msg.userfront.forgot]
|
||||
description = "Description"
|
||||
@@ -372,10 +396,10 @@ link_failed = "Link Failed"
|
||||
link_send_failed = "Link Send Failed"
|
||||
link_sent_email = "Link Sent Email"
|
||||
link_sent_phone = "Link Sent Phone"
|
||||
link_timeout = "Link Timeout"
|
||||
no_account = "No Account"
|
||||
link_timeout = "Time expired."
|
||||
no_account = "New to Baron?"
|
||||
oidc_failed = "OIDC Failed"
|
||||
qr_expired = "QR Expired"
|
||||
qr_expired = "Time expired."
|
||||
qr_init_failed = "QR Init Failed"
|
||||
qr_login_required = "QR Login Required"
|
||||
token_missing = "Token Missing"
|
||||
@@ -383,7 +407,7 @@ verification_failed = "Verification Failed"
|
||||
|
||||
[msg.userfront.login.link]
|
||||
approved = "Approved"
|
||||
helper = "Helper"
|
||||
helper = "Sending you a login link"
|
||||
missing_login_id = "Missing Login Id"
|
||||
missing_phone = "Missing Phone"
|
||||
resend_wait = "Resend Wait"
|
||||
@@ -871,6 +895,9 @@ retry = "Retry"
|
||||
save = "Save"
|
||||
search = "Search"
|
||||
show_more = "Show More"
|
||||
language = "Language"
|
||||
language_ko = "한국어"
|
||||
language_en = "English"
|
||||
theme_dark = "Dark"
|
||||
theme_light = "Light"
|
||||
theme_toggle = "Theme Toggle"
|
||||
@@ -1091,7 +1118,7 @@ subtitle = "Manage your applications"
|
||||
|
||||
|
||||
[ui.userfront]
|
||||
app_title = "App Title"
|
||||
app_title = "Baron SW Portal"
|
||||
|
||||
[ui.userfront.app_label]
|
||||
admin_console = "Admin Console"
|
||||
@@ -1161,7 +1188,7 @@ signup = "Signup"
|
||||
submit = "Submit"
|
||||
|
||||
[ui.userfront.login.field]
|
||||
login_id = "Login Id"
|
||||
login_id = "Emain or Phone Number"
|
||||
password = "Password"
|
||||
|
||||
[ui.userfront.login.link]
|
||||
@@ -1175,7 +1202,7 @@ title = "Title"
|
||||
[ui.userfront.login.qr]
|
||||
expired = "Expired"
|
||||
refresh = "Refresh"
|
||||
remaining = "Remaining"
|
||||
remaining = "Remaining: {{time}}"
|
||||
|
||||
[ui.userfront.login.short_code]
|
||||
digits = "Digits"
|
||||
@@ -1184,9 +1211,9 @@ prefix = "Prefix"
|
||||
submit = "Submit"
|
||||
|
||||
[ui.userfront.login.tabs]
|
||||
link = "Link"
|
||||
link = "Link/Code"
|
||||
password = "Password"
|
||||
qr = "QR"
|
||||
qr = "QR Code"
|
||||
|
||||
[ui.userfront.login.unregistered]
|
||||
action = "Action"
|
||||
@@ -1311,6 +1338,6 @@ logout = "Logout"
|
||||
overview = "Overview"
|
||||
relying_parties = "Apps (RP)"
|
||||
tenant_dashboard = "Tenant Dashboard"
|
||||
tenant_groups = "Tenant Groups"
|
||||
user_groups = "User Groups"
|
||||
tenants = "Tenants"
|
||||
users = "Users"
|
||||
|
||||
@@ -355,8 +355,32 @@ title_with_code = "오류: {{code}}"
|
||||
type = "오류 종류: {{type}}"
|
||||
|
||||
[msg.userfront.error.whitelist]
|
||||
$normalizedCode = "에러가 계속되면 관리자에게 문의해주세요"
|
||||
"$normalizedCode" = "{{error}}"
|
||||
settings_disabled = "현재 계정 설정 화면은 준비 중입니다."
|
||||
invalid_session = "세션이 만료되었습니다. 다시 로그인해 주세요."
|
||||
verification_required = "추가 인증이 필요합니다. 안내에 따라 진행해 주세요."
|
||||
recovery_expired = "재설정 링크가 만료되었습니다. 다시 요청해 주세요."
|
||||
recovery_invalid = "재설정 링크가 유효하지 않습니다."
|
||||
rate_limited = "요청이 많습니다. 잠시 후 다시 시도해 주세요."
|
||||
not_found = "요청한 페이지를 찾을 수 없습니다."
|
||||
bad_request = "입력값을 확인해 주세요."
|
||||
password_or_email_mismatch = "이메일 혹은 비밀번호가 일치하지 않습니다."
|
||||
|
||||
[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.forgot]
|
||||
description = "계정과 연결된 이메일 주소 또는 휴대폰 번호를 입력하시면, 비밀번호를 재설정할 수 있는 링크를 보내드립니다."
|
||||
@@ -372,10 +396,10 @@ link_failed = "오류: {{error}}"
|
||||
link_send_failed = "전송 실패: {{error}}"
|
||||
link_sent_email = "입력하신 이메일로 로그인 링크를 보냈습니다."
|
||||
link_sent_phone = "입력하신 번호로 로그인 링크를 보냈습니다."
|
||||
link_timeout = "로그인 요청 시간이 초과되었습니다."
|
||||
link_timeout = "시간이 경과되었습니다."
|
||||
no_account = "계정이 없으신가요?"
|
||||
oidc_failed = "OIDC 로그인 처리에 실패했습니다. 다시 시도해 주세요."
|
||||
qr_expired = "QR 세션이 만료되었습니다."
|
||||
qr_expired = "시간이 경과되었습니다."
|
||||
qr_init_failed = "QR 초기화에 실패했습니다: {{error}}"
|
||||
qr_login_required = "로그인 한 상태여야 QR 스캔으로 로그인 할 수 있습니다"
|
||||
token_missing = "로그인 토큰을 확인할 수 없습니다."
|
||||
@@ -871,6 +895,9 @@ retry = "다시 시도"
|
||||
save = "저장"
|
||||
search = "검색"
|
||||
show_more = "+ 더보기"
|
||||
language = "언어"
|
||||
language_ko = "한국어"
|
||||
language_en = "English"
|
||||
theme_dark = "Dark"
|
||||
theme_light = "Light"
|
||||
theme_toggle = "테마 전환"
|
||||
@@ -1091,7 +1118,7 @@ subtitle = "Manage your applications"
|
||||
|
||||
|
||||
[ui.userfront]
|
||||
app_title = "Baron 로그인"
|
||||
app_title = "Baron SW 포탈"
|
||||
|
||||
[ui.userfront.app_label]
|
||||
admin_console = "Admin Console"
|
||||
@@ -1311,6 +1338,6 @@ logout = "로그아웃"
|
||||
overview = "개요"
|
||||
relying_parties = "애플리케이션(RP)"
|
||||
tenant_dashboard = "테넌트 대시보드"
|
||||
tenant_groups = "테넌트 그룹"
|
||||
user_groups = "유저 그룹"
|
||||
tenants = "테넌트"
|
||||
users = "사용자"
|
||||
|
||||
@@ -355,8 +355,32 @@ title_with_code = ""
|
||||
type = ""
|
||||
|
||||
[msg.userfront.error.whitelist]
|
||||
$normalizedCode = ""
|
||||
"$normalizedCode" = ""
|
||||
settings_disabled = ""
|
||||
invalid_session = ""
|
||||
verification_required = ""
|
||||
recovery_expired = ""
|
||||
recovery_invalid = ""
|
||||
rate_limited = ""
|
||||
not_found = ""
|
||||
bad_request = ""
|
||||
password_or_email_mismatch = ""
|
||||
|
||||
[msg.userfront.error.ory]
|
||||
"$normalizedCode" = ""
|
||||
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.forgot]
|
||||
description = ""
|
||||
@@ -668,7 +692,7 @@ logout = ""
|
||||
overview = ""
|
||||
relying_parties = ""
|
||||
tenant_dashboard = ""
|
||||
tenant_groups = ""
|
||||
user_groups = ""
|
||||
tenants = ""
|
||||
users = ""
|
||||
|
||||
@@ -883,6 +907,9 @@ retry = ""
|
||||
save = ""
|
||||
search = ""
|
||||
show_more = ""
|
||||
language = ""
|
||||
language_ko = ""
|
||||
language_en = ""
|
||||
theme_dark = ""
|
||||
theme_light = ""
|
||||
theme_toggle = ""
|
||||
@@ -1187,7 +1214,7 @@ title = ""
|
||||
[ui.userfront.login.qr]
|
||||
expired = ""
|
||||
refresh = ""
|
||||
remaining = ""
|
||||
remaining = "Remaining: {{time}}"
|
||||
|
||||
[ui.userfront.login.short_code]
|
||||
digits = ""
|
||||
|
||||
@@ -68,18 +68,18 @@ type SignupRequest struct {
|
||||
// User Profile Models
|
||||
|
||||
type UserProfileResponse struct {
|
||||
ID string `json:"id"`
|
||||
Email string `json:"email"`
|
||||
Name string `json:"name"`
|
||||
Phone string `json:"phone"`
|
||||
Role string `json:"role"` // 추가
|
||||
Department string `json:"department"`
|
||||
AffiliationType string `json:"affiliationType"`
|
||||
CompanyCode string `json:"companyCode,omitempty"`
|
||||
TenantID *string `json:"tenantId,omitempty"` // 추가
|
||||
RelyingPartyID *string `json:"relyingPartyId,omitempty"` // 추가
|
||||
Metadata map[string]any `json:"metadata,omitempty"`
|
||||
Tenant *Tenant `json:"tenant,omitempty"`
|
||||
ID string `json:"id"`
|
||||
Email string `json:"email"`
|
||||
Name string `json:"name"`
|
||||
Phone string `json:"phone"`
|
||||
Role string `json:"role"` // 추가
|
||||
Department string `json:"department"`
|
||||
AffiliationType string `json:"affiliationType"`
|
||||
CompanyCode string `json:"companyCode,omitempty"`
|
||||
TenantID *string `json:"tenantId,omitempty"` // 추가
|
||||
RelyingPartyID *string `json:"relyingPartyId,omitempty"` // 추가
|
||||
Metadata map[string]any `json:"metadata,omitempty"`
|
||||
Tenant *Tenant `json:"tenant,omitempty"`
|
||||
ManageableTenants []Tenant `json:"manageableTenants,omitempty"` // 추가: 관리 가능한 테넌트 목록
|
||||
}
|
||||
|
||||
|
||||
@@ -1,16 +1,19 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"baron-sso-backend/internal/service"
|
||||
"runtime"
|
||||
"time"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
)
|
||||
|
||||
type AdminHandler struct{}
|
||||
type AdminHandler struct {
|
||||
Keto service.KetoService
|
||||
}
|
||||
|
||||
func NewAdminHandler() *AdminHandler {
|
||||
return &AdminHandler{}
|
||||
func NewAdminHandler(keto service.KetoService) *AdminHandler {
|
||||
return &AdminHandler{Keto: keto}
|
||||
}
|
||||
|
||||
func (h *AdminHandler) CheckAuth(c *fiber.Ctx) error {
|
||||
|
||||
@@ -27,24 +27,33 @@ func (m *AsyncMockIdpProvider) Name() string { return "mock-idp" }
|
||||
func (m *AsyncMockIdpProvider) GetMetadata() (*domain.IDPMetadata, error) {
|
||||
return &domain.IDPMetadata{}, nil
|
||||
}
|
||||
|
||||
func (m *AsyncMockIdpProvider) UserExists(loginID string) (bool, error) {
|
||||
args := m.Called(loginID)
|
||||
return args.Bool(0), args.Error(1)
|
||||
}
|
||||
|
||||
func (m *AsyncMockIdpProvider) CreateUser(user *domain.BrokerUser, password string) (string, error) {
|
||||
args := m.Called(user, password)
|
||||
return args.String(0), args.Error(1)
|
||||
}
|
||||
|
||||
func (m *AsyncMockIdpProvider) SignIn(loginID, password string) (*domain.AuthInfo, error) {
|
||||
return nil, nil
|
||||
}
|
||||
func (m *AsyncMockIdpProvider) IssueSession(loginID string) (*domain.AuthInfo, error) { return nil, nil }
|
||||
|
||||
func (m *AsyncMockIdpProvider) IssueSession(loginID string) (*domain.AuthInfo, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (m *AsyncMockIdpProvider) InitiateLinkLogin(loginID, returnTo string) (*domain.LinkLoginInit, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (m *AsyncMockIdpProvider) VerifyLoginCode(loginID, flowID, code string) (*domain.AuthInfo, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (m *AsyncMockIdpProvider) GetPasswordPolicy() (*domain.PasswordPolicy, error) {
|
||||
return &domain.PasswordPolicy{MinLength: 12}, nil
|
||||
}
|
||||
@@ -52,6 +61,7 @@ func (m *AsyncMockIdpProvider) InitiatePasswordReset(loginID, redirectUrl string
|
||||
func (m *AsyncMockIdpProvider) VerifyPasswordResetToken(token string) (*domain.AuthInfo, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (m *AsyncMockIdpProvider) UpdateUserPassword(loginID, newPassword string, r *http.Request) error {
|
||||
return nil
|
||||
}
|
||||
@@ -74,15 +84,19 @@ func (m *AsyncMockUserRepo) Update(ctx context.Context, user *domain.User) error
|
||||
func (m *AsyncMockUserRepo) FindByEmail(ctx context.Context, email string) (*domain.User, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (m *AsyncMockUserRepo) FindByID(ctx context.Context, id string) (*domain.User, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (m *AsyncMockUserRepo) FindByIDs(ctx context.Context, ids []string) ([]domain.User, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (m *AsyncMockUserRepo) ListByTenant(ctx context.Context, tenantID string) ([]domain.User, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (m *AsyncMockUserRepo) List(ctx context.Context, offset, limit int, search string) ([]domain.User, int64, error) {
|
||||
return nil, 0, nil
|
||||
}
|
||||
@@ -95,10 +109,12 @@ func (m *AsyncMockRedisRepo) Set(key string, value string, expiration time.Durat
|
||||
args := m.Called(key, value, expiration)
|
||||
return args.Error(0)
|
||||
}
|
||||
|
||||
func (m *AsyncMockRedisRepo) Get(key string) (string, error) {
|
||||
args := m.Called(key)
|
||||
return args.String(0), args.Error(1)
|
||||
}
|
||||
|
||||
func (m *AsyncMockRedisRepo) Delete(key string) error {
|
||||
args := m.Called(key)
|
||||
return args.Error(0)
|
||||
@@ -114,9 +130,11 @@ type AsyncMockTenantService struct {
|
||||
func (m *AsyncMockTenantService) RegisterTenant(ctx context.Context, name, slug, description string, domains []string) (*domain.Tenant, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (m *AsyncMockTenantService) RequestRegistration(ctx context.Context, name, slug, description string, domainName string, adminEmail string) (*domain.Tenant, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (m *AsyncMockTenantService) GetTenantByDomain(ctx context.Context, emailDomain string) (*domain.Tenant, error) {
|
||||
args := m.Called(ctx, emailDomain)
|
||||
if args.Get(0) == nil {
|
||||
@@ -124,23 +142,28 @@ func (m *AsyncMockTenantService) GetTenantByDomain(ctx context.Context, emailDom
|
||||
}
|
||||
return args.Get(0).(*domain.Tenant), args.Error(1)
|
||||
}
|
||||
|
||||
func (m *AsyncMockTenantService) GetTenantBySlug(ctx context.Context, slug string) (*domain.Tenant, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (m *AsyncMockTenantService) GetTenant(ctx context.Context, id string) (*domain.Tenant, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (m *AsyncMockTenantService) ListManageableTenants(ctx context.Context, userID string) ([]domain.Tenant, error) {
|
||||
return nil, nil
|
||||
}
|
||||
func (m *AsyncMockTenantService) ApproveTenant(ctx context.Context, id string) error { return nil }
|
||||
func (m *AsyncMockTenantService) SetKetoService(keto service.KetoService) {}
|
||||
func (m *AsyncMockTenantService) SetKetoService(keto service.KetoService) {}
|
||||
func (m *AsyncMockTenantService) AddTenantAdmin(ctx context.Context, tenantID, userID string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *AsyncMockTenantService) RemoveTenantAdmin(ctx context.Context, tenantID, userID string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *AsyncMockTenantService) ListTenantAdmins(ctx context.Context, tenantID string) ([]string, error) {
|
||||
return nil, nil
|
||||
}
|
||||
@@ -153,15 +176,19 @@ func (m *AsyncMockKetoService) CreateRelation(ctx context.Context, namespace, ob
|
||||
args := m.Called(ctx, namespace, object, relation, subject)
|
||||
return args.Error(0)
|
||||
}
|
||||
|
||||
func (m *AsyncMockKetoService) DeleteRelation(ctx context.Context, namespace, object, relation, subject string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *AsyncMockKetoService) CheckPermission(ctx context.Context, namespace, object, relation, subject string) (bool, error) {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
func (m *AsyncMockKetoService) ListObjects(ctx context.Context, namespace, relation, subject string) ([]string, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (m *AsyncMockKetoService) ListRelations(ctx context.Context, namespace, object, relation, subject string) ([]service.RelationTuple, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@ type DevHandler struct {
|
||||
ConsentRepo repository.ClientConsentRepository
|
||||
}
|
||||
|
||||
func NewDevHandler(redis domain.RedisRepository, secretRepo domain.ClientSecretRepository, consentRepo repository.ClientConsentRepository) *DevHandler {
|
||||
func NewDevHandler(redis domain.RedisRepository, secretRepo domain.ClientSecretRepository, consentRepo repository.ClientConsentRepository, rpSvc service.RelyingPartyService) *DevHandler {
|
||||
return &DevHandler{
|
||||
Hydra: service.NewHydraAdminService(),
|
||||
Redis: redis,
|
||||
|
||||
@@ -9,11 +9,12 @@ import (
|
||||
)
|
||||
|
||||
type RelyingPartyHandler struct {
|
||||
Service service.RelyingPartyService
|
||||
Service service.RelyingPartyService
|
||||
KratosAdmin *service.KratosAdminService
|
||||
}
|
||||
|
||||
func NewRelyingPartyHandler(s service.RelyingPartyService) *RelyingPartyHandler {
|
||||
return &RelyingPartyHandler{Service: s}
|
||||
func NewRelyingPartyHandler(s service.RelyingPartyService, kratos *service.KratosAdminService) *RelyingPartyHandler {
|
||||
return &RelyingPartyHandler{Service: s, KratosAdmin: kratos}
|
||||
}
|
||||
|
||||
func (h *RelyingPartyHandler) Create(c *fiber.Ctx) error {
|
||||
|
||||
@@ -12,12 +12,19 @@ import (
|
||||
)
|
||||
|
||||
type TenantHandler struct {
|
||||
DB *gorm.DB
|
||||
Service service.TenantService
|
||||
DB *gorm.DB
|
||||
Service service.TenantService
|
||||
Keto service.KetoService
|
||||
KratosAdmin *service.KratosAdminService
|
||||
}
|
||||
|
||||
func NewTenantHandler(db *gorm.DB, svc service.TenantService) *TenantHandler {
|
||||
return &TenantHandler{DB: db, Service: svc}
|
||||
func NewTenantHandler(db *gorm.DB, svc service.TenantService, keto service.KetoService, kratos *service.KratosAdminService) *TenantHandler {
|
||||
return &TenantHandler{
|
||||
DB: db,
|
||||
Service: svc,
|
||||
Keto: keto,
|
||||
KratosAdmin: kratos,
|
||||
}
|
||||
}
|
||||
|
||||
type tenantSummary struct {
|
||||
@@ -301,6 +308,85 @@ func (h *TenantHandler) DeleteTenant(c *fiber.Ctx) error {
|
||||
return c.SendStatus(fiber.StatusNoContent)
|
||||
}
|
||||
|
||||
func (h *TenantHandler) ListAdmins(c *fiber.Ctx) error {
|
||||
tenantID := c.Params("id")
|
||||
if tenantID == "" {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "tenant id is required"})
|
||||
}
|
||||
|
||||
// Fetch admins from Keto
|
||||
relations, err := h.Keto.ListRelations(c.Context(), "Tenant", tenantID, "admin", "")
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
|
||||
}
|
||||
|
||||
type adminInfo struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Email string `json:"email"`
|
||||
}
|
||||
admins := []adminInfo{}
|
||||
|
||||
for _, rel := range relations {
|
||||
if !strings.HasPrefix(rel.SubjectID, "User:") {
|
||||
continue
|
||||
}
|
||||
userID := strings.TrimPrefix(rel.SubjectID, "User:")
|
||||
|
||||
// Fetch user details from Kratos
|
||||
identity, err := h.KratosAdmin.GetIdentity(c.Context(), userID)
|
||||
if err != nil {
|
||||
admins = append(admins, adminInfo{ID: userID, Name: "Unknown", Email: "Unknown"})
|
||||
continue
|
||||
}
|
||||
|
||||
name := ""
|
||||
if n, ok := identity.Traits["name"].(string); ok {
|
||||
name = n
|
||||
}
|
||||
email := ""
|
||||
if e, ok := identity.Traits["email"].(string); ok {
|
||||
email = e
|
||||
}
|
||||
|
||||
admins = append(admins, adminInfo{
|
||||
ID: userID,
|
||||
Name: name,
|
||||
Email: email,
|
||||
})
|
||||
}
|
||||
|
||||
return c.JSON(admins)
|
||||
}
|
||||
|
||||
func (h *TenantHandler) AddAdmin(c *fiber.Ctx) error {
|
||||
tenantID := c.Params("id")
|
||||
userID := c.Params("userId")
|
||||
if tenantID == "" || userID == "" {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "tenantId and userId are required"})
|
||||
}
|
||||
|
||||
if err := h.Keto.CreateRelation(c.Context(), "Tenant", tenantID, "admin", "User:"+userID); err != nil {
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
|
||||
}
|
||||
|
||||
return c.SendStatus(fiber.StatusOK)
|
||||
}
|
||||
|
||||
func (h *TenantHandler) RemoveAdmin(c *fiber.Ctx) error {
|
||||
tenantID := c.Params("id")
|
||||
userID := c.Params("userId")
|
||||
if tenantID == "" || userID == "" {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "tenantId and userId are required"})
|
||||
}
|
||||
|
||||
if err := h.Keto.DeleteRelation(c.Context(), "Tenant", tenantID, "admin", "User:"+userID); err != nil {
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
|
||||
}
|
||||
|
||||
return c.SendStatus(fiber.StatusNoContent)
|
||||
}
|
||||
|
||||
func mapTenantSummary(t domain.Tenant) tenantSummary {
|
||||
domains := make([]string, 0, len(t.Domains))
|
||||
for _, d := range t.Domains {
|
||||
|
||||
@@ -23,12 +23,15 @@ type MockUserGroupService struct {
|
||||
func (m *MockUserGroupService) Create(ctx context.Context, group *domain.UserGroup) error {
|
||||
return m.Called(ctx, group).Error(0)
|
||||
}
|
||||
|
||||
func (m *MockUserGroupService) Update(ctx context.Context, group *domain.UserGroup) error {
|
||||
return m.Called(ctx, group).Error(0)
|
||||
}
|
||||
|
||||
func (m *MockUserGroupService) Delete(ctx context.Context, id string) error {
|
||||
return m.Called(ctx, id).Error(0)
|
||||
}
|
||||
|
||||
func (m *MockUserGroupService) Get(ctx context.Context, id string) (*domain.UserGroup, error) {
|
||||
args := m.Called(ctx, id)
|
||||
if args.Get(0) == nil {
|
||||
@@ -36,23 +39,29 @@ func (m *MockUserGroupService) Get(ctx context.Context, id string) (*domain.User
|
||||
}
|
||||
return args.Get(0).(*domain.UserGroup), args.Error(1)
|
||||
}
|
||||
|
||||
func (m *MockUserGroupService) List(ctx context.Context, tenantID string) ([]domain.UserGroup, error) {
|
||||
args := m.Called(ctx, tenantID)
|
||||
return args.Get(0).([]domain.UserGroup), args.Error(1)
|
||||
}
|
||||
|
||||
func (m *MockUserGroupService) AddMember(ctx context.Context, groupID, userID string) error {
|
||||
return m.Called(ctx, groupID, userID).Error(0)
|
||||
}
|
||||
|
||||
func (m *MockUserGroupService) RemoveMember(ctx context.Context, groupID, userID string) error {
|
||||
return m.Called(ctx, groupID, userID).Error(0)
|
||||
}
|
||||
|
||||
func (m *MockUserGroupService) ListRoles(ctx context.Context, groupID string) ([]domain.GroupRole, error) {
|
||||
args := m.Called(ctx, groupID)
|
||||
return args.Get(0).([]domain.GroupRole), args.Error(1)
|
||||
}
|
||||
|
||||
func (m *MockUserGroupService) AssignRoleToTenant(ctx context.Context, groupID, tenantID, relation string) error {
|
||||
return m.Called(ctx, groupID, tenantID, relation).Error(0)
|
||||
}
|
||||
|
||||
func (m *MockUserGroupService) RemoveRoleFromTenant(ctx context.Context, groupID, tenantID, relation string) error {
|
||||
return m.Called(ctx, groupID, tenantID, relation).Error(0)
|
||||
}
|
||||
@@ -106,7 +115,7 @@ func TestUserGroupHandler_AddMember(t *testing.T) {
|
||||
groupID := "g1"
|
||||
userID := "u1"
|
||||
body, _ := json.Marshal(map[string]string{"userId": userID})
|
||||
|
||||
|
||||
mockSvc.On("AddMember", mock.Anything, groupID, userID).Return(nil)
|
||||
|
||||
req := httptest.NewRequest("POST", "/user-groups/g1/members", bytes.NewReader(body))
|
||||
|
||||
@@ -54,6 +54,14 @@ func (m *MockKetoService) ListRelations(ctx context.Context, namespace, object,
|
||||
return args.Get(0).([]service.RelationTuple), args.Error(1)
|
||||
}
|
||||
|
||||
func (m *MockKetoService) ListObjects(ctx context.Context, namespace, relation, subject string) ([]string, error) {
|
||||
args := m.Called(ctx, namespace, relation, subject)
|
||||
if args.Get(0) == nil {
|
||||
return nil, args.Error(1)
|
||||
}
|
||||
return args.Get(0).([]string), args.Error(1)
|
||||
}
|
||||
|
||||
// Fixed MockKetoService to match service.KetoService exactly if possible.
|
||||
// Wait, middleware/rbac.go imports baron-sso-backend/internal/service.
|
||||
// So I should use service.RelationTuple.
|
||||
|
||||
@@ -18,12 +18,15 @@ type MockUserGroupRepository struct {
|
||||
func (m *MockUserGroupRepository) Create(ctx context.Context, group *domain.UserGroup) error {
|
||||
return m.Called(ctx, group).Error(0)
|
||||
}
|
||||
|
||||
func (m *MockUserGroupRepository) Update(ctx context.Context, group *domain.UserGroup) error {
|
||||
return m.Called(ctx, group).Error(0)
|
||||
}
|
||||
|
||||
func (m *MockUserGroupRepository) Delete(ctx context.Context, id string) error {
|
||||
return m.Called(ctx, id).Error(0)
|
||||
}
|
||||
|
||||
func (m *MockUserGroupRepository) FindByID(ctx context.Context, id string) (*domain.UserGroup, error) {
|
||||
args := m.Called(ctx, id)
|
||||
if args.Get(0) == nil {
|
||||
@@ -31,6 +34,7 @@ func (m *MockUserGroupRepository) FindByID(ctx context.Context, id string) (*dom
|
||||
}
|
||||
return args.Get(0).(*domain.UserGroup), args.Error(1)
|
||||
}
|
||||
|
||||
func (m *MockUserGroupRepository) ListByTenantID(ctx context.Context, tenantID string) ([]domain.UserGroup, error) {
|
||||
args := m.Called(ctx, tenantID)
|
||||
return args.Get(0).([]domain.UserGroup), args.Error(1)
|
||||
@@ -45,16 +49,20 @@ func (m *MockUserRepository) Update(ctx context.Context, user *domain.User) erro
|
||||
func (m *MockUserRepository) FindByEmail(ctx context.Context, email string) (*domain.User, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (m *MockUserRepository) FindByID(ctx context.Context, id string) (*domain.User, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (m *MockUserRepository) FindByIDs(ctx context.Context, ids []string) ([]domain.User, error) {
|
||||
args := m.Called(ctx, ids)
|
||||
return args.Get(0).([]domain.User), args.Error(1)
|
||||
}
|
||||
|
||||
func (m *MockUserRepository) ListByTenant(ctx context.Context, tenantID string) ([]domain.User, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (m *MockUserRepository) List(ctx context.Context, offset, limit int, search string) ([]domain.User, int64, error) {
|
||||
return nil, 0, nil
|
||||
}
|
||||
@@ -68,19 +76,24 @@ func (m *MockTenantRepository) Update(ctx context.Context, tenant *domain.Tenant
|
||||
func (m *MockTenantRepository) FindByID(ctx context.Context, id string) (*domain.Tenant, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (m *MockTenantRepository) FindByIDs(ctx context.Context, ids []string) ([]domain.Tenant, error) {
|
||||
args := m.Called(ctx, ids)
|
||||
return args.Get(0).([]domain.Tenant), args.Error(1)
|
||||
}
|
||||
|
||||
func (m *MockTenantRepository) FindBySlug(ctx context.Context, slug string) (*domain.Tenant, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (m *MockTenantRepository) FindByName(ctx context.Context, name string) (*domain.Tenant, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (m *MockTenantRepository) FindByDomain(ctx context.Context, domainName string) (*domain.Tenant, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (m *MockTenantRepository) AddDomain(ctx context.Context, tenantID string, domainName string) error {
|
||||
return nil
|
||||
}
|
||||
@@ -180,17 +193,17 @@ func TestUserGroupService_Get_WithKratosFallback(t *testing.T) {
|
||||
// We need a way to mock KratosAdminService but it's a struct, not an interface.
|
||||
// For this POC test, we'll focus on the Keto and UserRepo parts.
|
||||
// If needed, we can refactor KratosAdminService to an interface.
|
||||
|
||||
|
||||
svc := NewUserGroupService(mockRepo, mockUserRepo, nil, mockKeto, nil)
|
||||
|
||||
groupID := "group-1"
|
||||
mockRepo.On("FindByID", mock.Anything, groupID).Return(&domain.UserGroup{ID: groupID, Name: "Test"}, nil)
|
||||
|
||||
|
||||
tuples := []RelationTuple{
|
||||
{Object: groupID, Relation: "members", SubjectID: "User:u1"},
|
||||
}
|
||||
mockKeto.On("ListRelations", mock.Anything, "UserGroup", groupID, "members", "").Return(tuples, nil)
|
||||
|
||||
|
||||
// User u1 not in local DB
|
||||
mockUserRepo.On("FindByIDs", mock.Anything, []string{"u1"}).Return([]domain.User{}, nil)
|
||||
|
||||
|
||||
@@ -108,10 +108,10 @@ function detectLocale(): Locale {
|
||||
}
|
||||
|
||||
// eslint-disable-next-line import/no-unresolved
|
||||
import enRaw from "../../../locales/en.toml?raw";
|
||||
import enRaw from "../locales/en.toml?raw";
|
||||
// Vite ?raw import는 런타임 상수로 번들됩니다.
|
||||
// eslint-disable-next-line import/no-unresolved
|
||||
import koRaw from "../../../locales/ko.toml?raw";
|
||||
import koRaw from "../locales/ko.toml?raw";
|
||||
|
||||
const translations: Record<Locale, TomlObject> = {
|
||||
ko: parseToml(koRaw),
|
||||
|
||||
@@ -355,8 +355,32 @@ title_with_code = "Title With Code"
|
||||
type = "Type"
|
||||
|
||||
[msg.userfront.error.whitelist]
|
||||
$normalizedCode = "$NormalizedCode"
|
||||
"$normalizedCode" = "{{error}}"
|
||||
settings_disabled = "Account settings are currently unavailable."
|
||||
invalid_session = "Your session has expired. Please sign in again."
|
||||
verification_required = "Additional verification is required. Please follow the instructions."
|
||||
recovery_expired = "The recovery link has expired. Please request a new one."
|
||||
recovery_invalid = "The recovery link is invalid."
|
||||
rate_limited = "Too many requests. Please try again later."
|
||||
not_found = "The requested page could not be found."
|
||||
bad_request = "Please check your input."
|
||||
password_or_email_mismatch = "Email or password does not match."
|
||||
|
||||
[msg.userfront.error.ory]
|
||||
"$normalizedCode" = "{{error}}"
|
||||
access_denied = "The user denied the consent request."
|
||||
consent_required = "Consent is required to continue."
|
||||
interaction_required = "Additional interaction is required. Please try again."
|
||||
invalid_client = "Client authentication failed."
|
||||
invalid_grant = "The authorization grant is invalid or expired."
|
||||
invalid_request = "The request is invalid."
|
||||
invalid_scope = "The requested scope is invalid."
|
||||
login_required = "Login is required."
|
||||
request_forbidden = "The request was forbidden."
|
||||
server_error = "An authentication server error occurred."
|
||||
temporarily_unavailable = "The authentication server is temporarily unavailable."
|
||||
unauthorized_client = "The client is not authorized for this request."
|
||||
unsupported_response_type = "The response type is not supported."
|
||||
|
||||
[msg.userfront.forgot]
|
||||
description = "Description"
|
||||
@@ -372,10 +396,10 @@ link_failed = "Link Failed"
|
||||
link_send_failed = "Link Send Failed"
|
||||
link_sent_email = "Link Sent Email"
|
||||
link_sent_phone = "Link Sent Phone"
|
||||
link_timeout = "Link Timeout"
|
||||
no_account = "No Account"
|
||||
link_timeout = "Time expired."
|
||||
no_account = "New to Baron?"
|
||||
oidc_failed = "OIDC Failed"
|
||||
qr_expired = "QR Expired"
|
||||
qr_expired = "Time expired."
|
||||
qr_init_failed = "QR Init Failed"
|
||||
qr_login_required = "QR Login Required"
|
||||
token_missing = "Token Missing"
|
||||
@@ -383,7 +407,7 @@ verification_failed = "Verification Failed"
|
||||
|
||||
[msg.userfront.login.link]
|
||||
approved = "Approved"
|
||||
helper = "Helper"
|
||||
helper = "Sending you a login link"
|
||||
missing_login_id = "Missing Login Id"
|
||||
missing_phone = "Missing Phone"
|
||||
resend_wait = "Resend Wait"
|
||||
@@ -871,6 +895,9 @@ retry = "Retry"
|
||||
save = "Save"
|
||||
search = "Search"
|
||||
show_more = "Show More"
|
||||
language = "Language"
|
||||
language_ko = "한국어"
|
||||
language_en = "English"
|
||||
theme_dark = "Dark"
|
||||
theme_light = "Light"
|
||||
theme_toggle = "Theme Toggle"
|
||||
@@ -1091,7 +1118,7 @@ subtitle = "Manage your applications"
|
||||
|
||||
|
||||
[ui.userfront]
|
||||
app_title = "App Title"
|
||||
app_title = "Baron SW Portal"
|
||||
|
||||
[ui.userfront.app_label]
|
||||
admin_console = "Admin Console"
|
||||
@@ -1161,7 +1188,7 @@ signup = "Signup"
|
||||
submit = "Submit"
|
||||
|
||||
[ui.userfront.login.field]
|
||||
login_id = "Login Id"
|
||||
login_id = "Emain or Phone Number"
|
||||
password = "Password"
|
||||
|
||||
[ui.userfront.login.link]
|
||||
@@ -1175,7 +1202,7 @@ title = "Title"
|
||||
[ui.userfront.login.qr]
|
||||
expired = "Expired"
|
||||
refresh = "Refresh"
|
||||
remaining = "Remaining"
|
||||
remaining = "Remaining: {{time}}"
|
||||
|
||||
[ui.userfront.login.short_code]
|
||||
digits = "Digits"
|
||||
@@ -1184,9 +1211,9 @@ prefix = "Prefix"
|
||||
submit = "Submit"
|
||||
|
||||
[ui.userfront.login.tabs]
|
||||
link = "Link"
|
||||
link = "Link/Code"
|
||||
password = "Password"
|
||||
qr = "QR"
|
||||
qr = "QR Code"
|
||||
|
||||
[ui.userfront.login.unregistered]
|
||||
action = "Action"
|
||||
@@ -1311,6 +1338,6 @@ logout = "Logout"
|
||||
overview = "Overview"
|
||||
relying_parties = "Apps (RP)"
|
||||
tenant_dashboard = "Tenant Dashboard"
|
||||
tenant_groups = "Tenant Groups"
|
||||
user_groups = "User Groups"
|
||||
tenants = "Tenants"
|
||||
users = "Users"
|
||||
|
||||
@@ -355,8 +355,32 @@ title_with_code = "오류: {{code}}"
|
||||
type = "오류 종류: {{type}}"
|
||||
|
||||
[msg.userfront.error.whitelist]
|
||||
$normalizedCode = "에러가 계속되면 관리자에게 문의해주세요"
|
||||
"$normalizedCode" = "{{error}}"
|
||||
settings_disabled = "현재 계정 설정 화면은 준비 중입니다."
|
||||
invalid_session = "세션이 만료되었습니다. 다시 로그인해 주세요."
|
||||
verification_required = "추가 인증이 필요합니다. 안내에 따라 진행해 주세요."
|
||||
recovery_expired = "재설정 링크가 만료되었습니다. 다시 요청해 주세요."
|
||||
recovery_invalid = "재설정 링크가 유효하지 않습니다."
|
||||
rate_limited = "요청이 많습니다. 잠시 후 다시 시도해 주세요."
|
||||
not_found = "요청한 페이지를 찾을 수 없습니다."
|
||||
bad_request = "입력값을 확인해 주세요."
|
||||
password_or_email_mismatch = "이메일 혹은 비밀번호가 일치하지 않습니다."
|
||||
|
||||
[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.forgot]
|
||||
description = "계정과 연결된 이메일 주소 또는 휴대폰 번호를 입력하시면, 비밀번호를 재설정할 수 있는 링크를 보내드립니다."
|
||||
@@ -372,10 +396,10 @@ link_failed = "오류: {{error}}"
|
||||
link_send_failed = "전송 실패: {{error}}"
|
||||
link_sent_email = "입력하신 이메일로 로그인 링크를 보냈습니다."
|
||||
link_sent_phone = "입력하신 번호로 로그인 링크를 보냈습니다."
|
||||
link_timeout = "로그인 요청 시간이 초과되었습니다."
|
||||
link_timeout = "시간이 경과되었습니다."
|
||||
no_account = "계정이 없으신가요?"
|
||||
oidc_failed = "OIDC 로그인 처리에 실패했습니다. 다시 시도해 주세요."
|
||||
qr_expired = "QR 세션이 만료되었습니다."
|
||||
qr_expired = "시간이 경과되었습니다."
|
||||
qr_init_failed = "QR 초기화에 실패했습니다: {{error}}"
|
||||
qr_login_required = "로그인 한 상태여야 QR 스캔으로 로그인 할 수 있습니다"
|
||||
token_missing = "로그인 토큰을 확인할 수 없습니다."
|
||||
@@ -871,6 +895,9 @@ retry = "다시 시도"
|
||||
save = "저장"
|
||||
search = "검색"
|
||||
show_more = "+ 더보기"
|
||||
language = "언어"
|
||||
language_ko = "한국어"
|
||||
language_en = "English"
|
||||
theme_dark = "Dark"
|
||||
theme_light = "Light"
|
||||
theme_toggle = "테마 전환"
|
||||
@@ -1091,7 +1118,7 @@ subtitle = "Manage your applications"
|
||||
|
||||
|
||||
[ui.userfront]
|
||||
app_title = "Baron 로그인"
|
||||
app_title = "Baron SW 포탈"
|
||||
|
||||
[ui.userfront.app_label]
|
||||
admin_console = "Admin Console"
|
||||
@@ -1311,6 +1338,6 @@ logout = "로그아웃"
|
||||
overview = "개요"
|
||||
relying_parties = "애플리케이션(RP)"
|
||||
tenant_dashboard = "테넌트 대시보드"
|
||||
tenant_groups = "테넌트 그룹"
|
||||
user_groups = "유저 그룹"
|
||||
tenants = "테넌트"
|
||||
users = "사용자"
|
||||
|
||||
@@ -355,8 +355,32 @@ title_with_code = ""
|
||||
type = ""
|
||||
|
||||
[msg.userfront.error.whitelist]
|
||||
$normalizedCode = ""
|
||||
"$normalizedCode" = ""
|
||||
settings_disabled = ""
|
||||
invalid_session = ""
|
||||
verification_required = ""
|
||||
recovery_expired = ""
|
||||
recovery_invalid = ""
|
||||
rate_limited = ""
|
||||
not_found = ""
|
||||
bad_request = ""
|
||||
password_or_email_mismatch = ""
|
||||
|
||||
[msg.userfront.error.ory]
|
||||
"$normalizedCode" = ""
|
||||
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.forgot]
|
||||
description = ""
|
||||
@@ -668,7 +692,7 @@ logout = ""
|
||||
overview = ""
|
||||
relying_parties = ""
|
||||
tenant_dashboard = ""
|
||||
tenant_groups = ""
|
||||
user_groups = ""
|
||||
tenants = ""
|
||||
users = ""
|
||||
|
||||
@@ -883,6 +907,9 @@ retry = ""
|
||||
save = ""
|
||||
search = ""
|
||||
show_more = ""
|
||||
language = ""
|
||||
language_ko = ""
|
||||
language_en = ""
|
||||
theme_dark = ""
|
||||
theme_light = ""
|
||||
theme_toggle = ""
|
||||
@@ -1187,7 +1214,7 @@ title = ""
|
||||
[ui.userfront.login.qr]
|
||||
expired = ""
|
||||
refresh = ""
|
||||
remaining = ""
|
||||
remaining = "Remaining: {{time}}"
|
||||
|
||||
[ui.userfront.login.short_code]
|
||||
digits = ""
|
||||
|
||||
@@ -61,6 +61,7 @@ services:
|
||||
- "${ADMIN_PORT:-5173}:5173"
|
||||
volumes:
|
||||
- ./adminfront:/app
|
||||
- ./locales:/locales
|
||||
- /app/node_modules
|
||||
networks:
|
||||
- baron_net
|
||||
@@ -80,6 +81,7 @@ services:
|
||||
- "${DEVFRONT_PORT:-5174}:5173"
|
||||
volumes:
|
||||
- ./devfront:/app
|
||||
- ./locales:/locales
|
||||
- /app/node_modules
|
||||
networks:
|
||||
- baron_net
|
||||
|
||||
@@ -76,11 +76,16 @@ function parseTomlKeys(filePath) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const key = line.slice(0, eqIndex).trim();
|
||||
let key = line.slice(0, eqIndex).trim();
|
||||
if (!key) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Strip quotes if present
|
||||
if (key.startsWith('"') && key.endsWith('"')) {
|
||||
key = key.slice(1, -1);
|
||||
}
|
||||
|
||||
const fullKey = [...currentSection, key].join('.');
|
||||
keys.add(fullKey);
|
||||
}
|
||||
|
||||
@@ -4,4 +4,8 @@ import 'locale_storage_stub.dart'
|
||||
abstract class LocaleStorage {
|
||||
static String? read() => localeStorage.read();
|
||||
static void write(String locale) => localeStorage.write(locale);
|
||||
static void forceMemoryStorageForTests(bool value) =>
|
||||
localeStorage.forceMemoryStorageForTests(value);
|
||||
static void forceSessionStorageForTests(bool value) =>
|
||||
localeStorage.forceSessionStorageForTests(value);
|
||||
}
|
||||
|
||||
@@ -6,6 +6,14 @@ class LocaleStorageImpl {
|
||||
void write(String locale) {
|
||||
_locale = locale;
|
||||
}
|
||||
|
||||
void forceMemoryStorageForTests(bool value) {
|
||||
// Stub
|
||||
}
|
||||
|
||||
void forceSessionStorageForTests(bool value) {
|
||||
// Stub
|
||||
}
|
||||
}
|
||||
|
||||
final localeStorage = LocaleStorageImpl();
|
||||
|
||||
@@ -11,7 +11,7 @@ class LocaleStorageImpl {
|
||||
static bool _forceSession = false;
|
||||
|
||||
@visibleForTesting
|
||||
static void forceMemoryStorageForTests(bool value) {
|
||||
void forceMemoryStorageForTests(bool value) {
|
||||
_forceMemory = value;
|
||||
if (!value) {
|
||||
_memory.clear();
|
||||
@@ -19,7 +19,7 @@ class LocaleStorageImpl {
|
||||
}
|
||||
|
||||
@visibleForTesting
|
||||
static void forceSessionStorageForTests(bool value) {
|
||||
void forceSessionStorageForTests(bool value) {
|
||||
_forceSession = value;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:userfront/core/i18n/locale_storage.dart';
|
||||
import 'package:userfront/core/i18n/locale_storage_web.dart' as locale_web;
|
||||
|
||||
import 'helpers/web_storage.dart';
|
||||
|
||||
void main() {
|
||||
setUp(() {
|
||||
locale_web.LocaleStorageImpl.forceMemoryStorageForTests(false);
|
||||
locale_web.LocaleStorageImpl.forceSessionStorageForTests(false);
|
||||
LocaleStorage.forceMemoryStorageForTests(false);
|
||||
LocaleStorage.forceSessionStorageForTests(false);
|
||||
if (webStorage.isWeb) {
|
||||
webStorage.clear();
|
||||
webStorage.clearSession();
|
||||
@@ -15,8 +14,8 @@ void main() {
|
||||
});
|
||||
|
||||
tearDown(() {
|
||||
locale_web.LocaleStorageImpl.forceMemoryStorageForTests(false);
|
||||
locale_web.LocaleStorageImpl.forceSessionStorageForTests(false);
|
||||
LocaleStorage.forceMemoryStorageForTests(false);
|
||||
LocaleStorage.forceSessionStorageForTests(false);
|
||||
if (webStorage.isWeb) {
|
||||
webStorage.clear();
|
||||
webStorage.clearSession();
|
||||
@@ -59,7 +58,7 @@ void main() {
|
||||
return;
|
||||
}
|
||||
|
||||
locale_web.LocaleStorageImpl.forceMemoryStorageForTests(true);
|
||||
LocaleStorage.forceMemoryStorageForTests(true);
|
||||
|
||||
LocaleStorage.write('en');
|
||||
expect(webStorage.get('locale'), isNull);
|
||||
@@ -76,7 +75,7 @@ void main() {
|
||||
return;
|
||||
}
|
||||
|
||||
locale_web.LocaleStorageImpl.forceSessionStorageForTests(true);
|
||||
LocaleStorage.forceSessionStorageForTests(true);
|
||||
|
||||
LocaleStorage.write('ko');
|
||||
expect(webStorage.get('locale'), isNull);
|
||||
Reference in New Issue
Block a user