1
0
forked from baron/baron-sso

Merge pull request 'temp-branch' (#464) from temp-branch into dev

Reviewed-on: baron/baron-sso#464
This commit is contained in:
2026-03-27 21:40:38 +09:00
10 changed files with 307 additions and 18 deletions

View File

@@ -19,6 +19,12 @@
"enabled": true
},
"files": {
"ignore": ["dist", "node_modules", "tsconfig*.json"]
"ignore": [
"dist",
"node_modules",
"tsconfig*.json",
"test-results",
"playwright-report"
]
}
}

View File

@@ -0,0 +1,261 @@
import { expect, test } from "@playwright/test";
test.describe("User Management", () => {
test.beforeEach(async ({ page }) => {
await page.addInitScript(() => {
const authority = "http://localhost:5000/oidc";
const client_id = "adminfront";
const key = `oidc.user:${authority}:${client_id}`;
const authData = {
id_token: "fake-id-token",
access_token: "fake-token",
token_type: "Bearer",
scope: "openid profile email",
profile: {
sub: "admin-user",
name: "Admin",
email: "admin@test.com",
role: "super_admin",
},
expires_at: Math.floor(Date.now() / 1000) + 36000,
};
window.localStorage.setItem(key, JSON.stringify(authData));
window.localStorage.setItem("admin_session", "fake-token");
window.localStorage.setItem("locale", "ko");
window.localStorage.setItem("oidc.state", "dummy");
(
window as Window & typeof globalThis & { _IS_TEST_MODE?: boolean }
)._IS_TEST_MODE = true;
});
await page.route("**/oidc/**", async (route) => {
if (route.request().url().includes("/.well-known/openid-configuration")) {
return route.fulfill({
json: {
issuer: "http://localhost:5000/oidc",
authorization_endpoint: "http://localhost:5000/oidc/auth",
token_endpoint: "http://localhost:5000/oidc/token",
userinfo_endpoint: "http://localhost:5000/oidc/userinfo",
jwks_uri: "http://localhost:5000/oidc/jwks",
},
});
}
await route.fulfill({ json: { issuer: "http://localhost:5000/oidc" } });
});
await page.route(/.*\/api\/v1\/.*/, async (route) => {
const url = route.request().url();
const method = route.request().method();
if (url.includes("/user/me")) {
return route.fulfill({
json: {
id: "admin-user",
name: "Admin",
email: "admin@test.com",
role: "super_admin",
manageableTenants: [],
},
});
}
if (url.includes("/admin/tenants") && method === "GET") {
return route.fulfill({
json: {
items: [
{
id: "t-1",
slug: "test-tenant",
name: "Test Tenant",
config: {
userSchema: [],
},
},
],
total: 1,
limit: 100,
offset: 0,
},
});
}
if (url.includes("/admin/users/u-1") && method === "GET") {
return route.fulfill({
json: {
id: "u-1",
name: "John Doe",
email: "john@test.com",
loginId: "johndoe",
tenantSlug: "test-tenant",
role: "user",
status: "active",
},
});
}
if (url.includes("/admin/users") && method === "POST") {
// Parse request payload to simulate validation checks
const postData = route.request().postDataJSON();
if (postData && postData.loginId === "existing_user") {
// Simulate a backend conflict error (409) for an existing loginId
return route.fulfill({
status: 409,
json: {
error: "이미 존재하는 로그인 ID 입니다.",
},
});
}
// Mock successful user creation
return route.fulfill({
json: {
id: "new-user-id",
name: "New User",
email: "newuser@test.com",
loginId: postData?.loginId || "newuser123",
},
});
}
if (url.includes("/admin/users/u-1") && method === "PUT") {
// Parse request payload
const postData = route.request().postDataJSON();
if (postData && postData.loginId === "existing_user") {
// Simulate a backend conflict error (409) for an existing loginId
return route.fulfill({
status: 409,
json: {
error: "이미 존재하는 로그인 ID 입니다.",
},
});
}
// Mock successful user update
return route.fulfill({
json: {
id: "u-1",
name: "John Doe Updated",
email: "john@test.com",
loginId: postData?.loginId || "johndoe_updated",
},
});
}
if (url.includes("/admin/users") && method === "GET") {
return route.fulfill({
json: {
items: [
{
id: "u-1",
name: "John Doe",
email: "john@test.com",
loginId: "johndoe",
role: "user",
status: "active",
},
],
total: 1,
limit: 50,
offset: 0,
},
});
}
return route.fulfill({ json: { items: [], total: 0 } });
});
});
test("should successfully edit a user's Login ID", async ({ page }) => {
await page.goto("/users/u-1");
// Wait for the form to load with the existing login ID
const loginIdInput = page.locator('input[id="loginId"]');
await expect(loginIdInput).toBeVisible();
await expect(loginIdInput).toHaveValue("johndoe");
// Change the Login ID
await loginIdInput.fill("johndoe_updated");
// Submit the form
const saveButton = page.getByRole("button", {
name: /변경사항 저장|Save/i,
});
await saveButton.click();
// Check for success message
await expect(
page.getByText(/사용자 정보가 수정되었습니다/i).first(),
).toBeVisible();
});
test("should show conflict error when updating to an existing Login ID", async ({
page,
}) => {
await page.goto("/users/u-1");
const loginIdInput = page.locator('input[id="loginId"]');
await expect(loginIdInput).toBeVisible();
// Enter a login ID that triggers our mock conflict error
await loginIdInput.fill("existing_user");
const saveButton = page.getByRole("button", {
name: /변경사항 저장|Save/i,
});
await saveButton.click();
// Check for the specific conflict error message from the backend mock
await expect(
page.getByText(/이미 존재하는 로그인 ID 입니다/i),
).toBeVisible();
});
test("should successfully create a new user with a Login ID", async ({
page,
}) => {
await page.goto("/users/new");
// Ensure the page title is loaded
await expect(page.getByText(/사용자 추가/i).first()).toBeVisible();
// Fill required fields
await page.locator('input[name="name"]').fill("New User");
await page.locator('input[name="email"]').fill("newuser@test.com");
// Fill Login ID
const loginIdInput = page.locator('input[name="loginId"]');
await loginIdInput.fill("newuser123");
// Submit the form
const createButton = page.getByRole("button", { name: /생성/i });
await createButton.click();
// Assuming successful creation redirects back to the user list
await expect(page).toHaveURL(/.*\/users$/, { timeout: 10000 });
});
test("should show conflict error when creating with an existing Login ID", async ({
page,
}) => {
await page.goto("/users/new");
await expect(page.getByText(/사용자 추가/i).first()).toBeVisible();
// Fill required fields
await page.locator('input[name="name"]').fill("New User");
await page.locator('input[name="email"]').fill("newuser@test.com");
// Fill Login ID that triggers the mock conflict error
const loginIdInput = page.locator('input[name="loginId"]');
await loginIdInput.fill("existing_user");
// Submit the form
const createButton = page.getByRole("button", { name: /생성/i });
await createButton.click();
// Check for the specific conflict error message from the backend mock
await expect(
page.getByText(/이미 존재하는 로그인 ID 입니다/i),
).toBeVisible();
});
});

View File

@@ -460,7 +460,7 @@ func (h *AuthHandler) Signup(c *fiber.Ctx) error {
} else {
// If companyCode provided but not found, we should probably reject if we want strictness,
// or just treat as GENERAL user. Given the risk "존재하지 않는 테넌트도 저장됨", we should reject.
return errorJSON(c, fiber.StatusBadRequest, "Invalid company code.")
return errorJSON(c, fiber.StatusBadRequest, "해당하는 가족사(테넌트)를 찾을 수 없습니다.")
}
}
@@ -473,6 +473,8 @@ func (h *AuthHandler) Signup(c *fiber.Ctx) error {
normalizedPhone = "+" + normalizedPhone
}
slog.Info("[Signup] Phone normalization", "raw", req.Phone, "normalized", normalizedPhone)
// IDP에 전달할 BrokerUser 스키마 구성
attributes := map[string]interface{}{
"department": req.Department,
@@ -517,7 +519,8 @@ func (h *AuthHandler) Signup(c *fiber.Ctx) error {
if strings.Contains(err.Error(), "already exists") {
return errorJSON(c, fiber.StatusConflict, "User already exists")
}
return errorJSON(c, fiber.StatusInternalServerError, "Failed to create user")
// Include the actual error message in the response for debugging
return errorJSON(c, fiber.StatusInternalServerError, fmt.Sprintf("Failed to create user: %v", err))
}
// 4. Cleanup Redis

View File

@@ -119,7 +119,7 @@ func TestSignup_CompanyCodeValidation(t *testing.T) {
assert.Equal(t, http.StatusBadRequest, resp.StatusCode)
var res map[string]interface{}
json.NewDecoder(resp.Body).Decode(&res)
assert.Equal(t, "Invalid company code.", res["error"])
assert.Equal(t, "해당하는 가족사(테넌트)를 찾을 수 없습니다.", res["error"])
})
t.Run("Active Company Code", func(t *testing.T) {

View File

@@ -3,6 +3,7 @@ package repository
import (
"baron-sso-backend/internal/domain"
"context"
"strings"
"gorm.io/gorm"
)
@@ -45,7 +46,7 @@ func (r *tenantRepository) FindByID(ctx context.Context, id string) (*domain.Ten
func (r *tenantRepository) FindBySlug(ctx context.Context, slug string) (*domain.Tenant, error) {
var tenant domain.Tenant
if err := r.db.WithContext(ctx).Preload("Domains").Where("slug = ?", slug).First(&tenant).Error; err != nil {
if err := r.db.WithContext(ctx).Preload("Domains").Where("slug = ?", strings.ToLower(slug)).First(&tenant).Error; err != nil {
return nil, err
}
return &tenant, nil

View File

@@ -18,6 +18,12 @@
"enabled": true
},
"files": {
"ignore": ["dist", "node_modules", "tsconfig*.json"]
"ignore": [
"dist",
"node_modules",
"tsconfig*.json",
"test-results",
"playwright-report"
]
}
}

View File

@@ -9,9 +9,9 @@ import {
Search,
} from "lucide-react";
import * as React from "react";
import { ForbiddenMessage } from "../../components/common/ForbiddenMessage";
import { Badge } from "../../components/ui/badge";
import { Button } from "../../components/ui/button";
import { ForbiddenMessage } from "../../components/common/ForbiddenMessage";
import {
Card,
CardContent,

View File

@@ -11,6 +11,7 @@ import {
import { useState } from "react";
import { useAuth } from "react-oidc-context";
import { Link, useNavigate } from "react-router-dom";
import { ForbiddenMessage } from "../../components/common/ForbiddenMessage";
import {
Avatar,
AvatarFallback,
@@ -18,7 +19,6 @@ import {
} from "../../components/ui/avatar";
import { Badge } from "../../components/ui/badge";
import { Button } from "../../components/ui/button";
import { ForbiddenMessage } from "../../components/common/ForbiddenMessage";
import {
Card,
CardContent,

View File

@@ -17,8 +17,8 @@ import {
CardTitle,
} from "../../components/ui/card";
import { t } from "../../lib/i18n";
import ProfileTenantSwitcher from "./ProfileTenantSwitcher";
import { fetchMe } from "../auth/authApi";
import ProfileTenantSwitcher from "./ProfileTenantSwitcher";
function ProfilePage() {
const auth = useAuth();

View File

@@ -6,6 +6,7 @@ import 'package:go_router/go_router.dart';
import 'package:userfront/i18n.dart';
import '../../../core/i18n/locale_utils.dart';
import '../../../core/services/auth_proxy_service.dart';
import '../../../core/ui/toast_service.dart';
class SignupScreen extends StatefulWidget {
const SignupScreen({super.key});
@@ -328,24 +329,29 @@ class _SignupScreenState extends State<SignupScreen> {
} catch (e) {
String eStr = e.toString().toLowerCase();
setState(() {
if (eStr.contains('uppercase')) {
if (eStr.contains('password') && eStr.contains('uppercase')) {
_passwordError = tr(
'msg.userfront.signup.password.uppercase_required',
);
} else if (eStr.contains('lowercase')) {
} else if (eStr.contains('password') && eStr.contains('lowercase')) {
_passwordError = tr(
'msg.userfront.signup.password.lowercase_required',
);
} else if (eStr.contains('digit') || eStr.contains('number')) {
} else if (eStr.contains('password') &&
(eStr.contains('digit') || eStr.contains('number'))) {
_passwordError = tr('msg.userfront.signup.password.number_required');
} else if (eStr.contains('symbol') || eStr.contains('special')) {
} else if (eStr.contains('password') &&
(eStr.contains('symbol') || eStr.contains('special'))) {
_passwordError = tr('msg.userfront.signup.password.symbol_required');
} else if (eStr.contains('length') || eStr.contains('12 characters')) {
} else if (eStr.contains('password') &&
(eStr.contains('length') || eStr.contains('12 characters'))) {
_passwordError = tr('msg.userfront.signup.password.length_required');
} else {
_passwordError = tr(
'msg.userfront.signup.failed',
params: {'error': e.toString()},
_showError(
tr(
'msg.userfront.signup.failed',
params: {'error': e.toString().replaceFirst('Exception: ', '')},
),
);
}
});
@@ -354,6 +360,11 @@ class _SignupScreenState extends State<SignupScreen> {
}
}
void _showError(String message) {
if (!mounted) return;
ToastService.error(message);
}
void _showSuccessDialog() {
showDialog(
context: context,
@@ -1488,8 +1499,9 @@ class _SignupScreenState extends State<SignupScreen> {
.replaceAll('Exception: ', ''),
);
} finally {
if (mounted)
if (mounted) {
setState(() => _isLoading = false);
}
}
},
child: const Text('중복 확인'),