From 06197a13660c588309f62b4413fdd0b6e1ab088c Mon Sep 17 00:00:00 2001 From: kent Date: Fri, 15 Aug 2025 16:32:11 +0900 Subject: [PATCH] =?UTF-8?q?fix=20:=20=ED=85=8C=EB=84=8C=ED=8A=B8=20?= =?UTF-8?q?=EA=B4=80=EB=A6=AC=EC=9E=90=20=EC=82=AC=EC=9A=A9=EC=9E=90=20API?= =?UTF-8?q?=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 사용자 목록 - 사용자 생성 - 사용자 단건 조회 - 사용자 수정 - 사용자 삭제(소프트 삭제) - 활성/비활성 전환 - 삭제 복구 - 비밀번호 초기화 - 수정필요 : 역할부여, 역할 해재 --- .../Controllers/Api/V1/AdminApiController.php | 37 -- .../Controllers/Api/V1/AdminController.php | 82 +++ app/Models/Members/User.php | 21 +- app/Services/AdminService.php | 469 ++++++++++++++++++ app/Services/TenantService.php | 8 + app/Swagger/v1/AdminApi.php | 80 ++- routes/api.php | 92 +--- 7 files changed, 644 insertions(+), 145 deletions(-) delete mode 100644 app/Http/Controllers/Api/V1/AdminApiController.php create mode 100644 app/Http/Controllers/Api/V1/AdminController.php create mode 100644 app/Services/AdminService.php diff --git a/app/Http/Controllers/Api/V1/AdminApiController.php b/app/Http/Controllers/Api/V1/AdminApiController.php deleted file mode 100644 index ea0e012..0000000 --- a/app/Http/Controllers/Api/V1/AdminApiController.php +++ /dev/null @@ -1,37 +0,0 @@ -debug); - }, '관리자 목록 조회 성공', '관리자 목록 조회 실패'); - } -} diff --git a/app/Http/Controllers/Api/V1/AdminController.php b/app/Http/Controllers/Api/V1/AdminController.php new file mode 100644 index 0000000..ee4d82d --- /dev/null +++ b/app/Http/Controllers/Api/V1/AdminController.php @@ -0,0 +1,82 @@ +all()); + }, '테넌트 사용자 목록 조회'); + } + + public function store(Request $request) + { + return ApiResponse::handle(function () use ($request) { + return AdminService::store($request->all()); + }, '테넌트 사용자 추가'); + } + + public function show($userNo) + { + return ApiResponse::handle(function () use ($userNo) { + return AdminService::show($userNo); + }, '테넌트 사용자 단건 조회'); + } + + public function update(Request $request, $userNo) + { + return ApiResponse::handle(function () use ($request, $userNo) { + return AdminService::update($request->all(), $userNo); + }, '테넌트 사용자 수정'); + } + + public function destroy($userNo) + { + return ApiResponse::handle(function () use ($userNo) { + return AdminService::destroy($userNo); + }, '테넌트 사용자 삭제(연결 삭제)'); + } + + public function restore($userNo) + { + return ApiResponse::handle(function () use ($userNo) { + return AdminService::restore($userNo); + }, '테넌트 사용자 삭제 복구'); + } + + public function toggle($userNo) + { + return ApiResponse::handle(function () use ($userNo) { + return AdminService::toggle($userNo); + }, '테넌트 사용자 활성/비활성'); + } + + public function attach(Request $request) + { + return ApiResponse::handle(function () use ($request) { + return AdminService::attach($request->all()); + }, '테넌트 사용자 역할 부여'); + } + + public function detach(Request $request) + { + return ApiResponse::handle(function () use ($request) { + return AdminService::detach($request->all()); + }, '테넌트 사용자 역할 해제'); + } + + public function reset(Request $request, $userNo) + { + return ApiResponse::handle(function () use ($request, $userNo) { + return AdminService::reset($request->all(), $userNo); + }, '테넌트 사용자 비밀번호 초기화'); + } +} diff --git a/app/Models/Members/User.php b/app/Models/Members/User.php index 3d58700..d263876 100644 --- a/app/Models/Members/User.php +++ b/app/Models/Members/User.php @@ -17,34 +17,21 @@ class User extends Authenticatable use HasApiTokens, Notifiable, SoftDeletes, ModelTrait; protected $fillable = [ + 'user_id', 'name', - 'phone', 'email', + 'phone', + 'password', 'options', 'profile_photo_path', ]; - protected $guarded = [ - 'id', - 'user_id', - 'password', - 'remember_token', - 'two_factor_secret', - 'two_factor_recovery_codes', - 'two_factor_confirmed_at', - 'email_verified_at', - 'last_login_at', - 'current_team_id', - 'deleted_at', - 'created_at', - 'updated_at', - ]; - protected $casts = [ 'email_verified_at' => 'datetime', 'last_login_at' => 'datetime', 'options' => 'array', 'deleted_at' => 'datetime', + 'password' => 'hashed', // ← 이걸 쓰면 자동 해싱 ]; protected $hidden = [ diff --git a/app/Services/AdminService.php b/app/Services/AdminService.php new file mode 100644 index 0000000..d80943c --- /dev/null +++ b/app/Services/AdminService.php @@ -0,0 +1,469 @@ +with(['user:id,name,email,phone']) + ->where('tenant_id', $tenantId); + + if ($keyword) { + $q->whereHas('user', function($sub) use ($keyword) { + $sub->where(function($w) use ($keyword) { + $w->where('name', 'like', "%{$keyword}%") + ->orWhere('email', 'like', "%{$keyword}%") + ->orWhere('phone', 'like', "%{$keyword}%"); + }); + }); + } + + if ($active !== null && $active !== '') { + $q->where('is_active', (int)$active); + } + + // 조인 정렬용 + $q->leftJoin('users', 'users.id', '=', 'user_tenants.user_id') + ->select( + 'users.id', + 'users.user_id', + 'users.name', + 'users.email', + 'users.phone', + 'user_tenants.is_active', + 'user_tenants.joined_at', + 'user_tenants.left_at', + 'user_tenants.tenant_id' + ); + + $q->orderBy($sortBy, $sortDir); + + $data = $q->paginate($size, ['*'], 'page', $page); + + return ApiResponse::response('result', $data); + } + + /** + * [POST] 테넌트 사용자 추가 (기존 사용자 연결) + * - 컨트롤러 store()에서 호출 + * - 유저 등록 역할 부여 + */ + public static function store(array $params = []) + { + $tenantId = app('tenant_id'); + if (!$tenantId) { + return ApiResponse::error('활성 테넌트가 없습니다.', 400); + } + + // 신규 회원 생성 + 역할 부여 지원 + $v = Validator::make($params, [ + 'user_id' => 'required|string|max:255|unique:users,user_id', + 'name' => 'required|string|max:255', + 'email' => 'required|email|max:100|unique:users,email', + 'phone' => 'nullable|string|max:30', + 'password' => 'required|string|min:8|max:64', + 'roles' => 'nullable|array', + 'roles.*' => 'string|max:100', // 각각의 역할 이름 + ]); + + if ($v->fails()) { + return ApiResponse::error($v->errors()->first(), 422); + } + + $payload = $v->validated(); + + return DB::transaction(function () use ($payload, $tenantId) { + // 신규 사용자 생성 + $user = User::create([ + 'user_id' => $payload['user_id'], + 'name' => $payload['name'], + 'email' => $payload['email'], + 'phone' => $payload['phone'] ?? null, + 'password' => $payload['password'], // 캐스트가 알아서 해싱 + ]); + + // 현재 테넌트에 활성 연결 + UserTenant::create([ + 'user_id' => $user->id, + 'tenant_id' => $tenantId, + 'is_active' => 1, + 'is_default' => 0, + 'joined_at' => now(), + ]); + + // 역할 부여 (Spatie Permission teams 모드 가정) + if (!empty($payload['roles']) && method_exists($user, 'assignRole')) { + $previousTeam = app()->has('permission.team_id') ? app('permission.team_id') : null; + app()->instance('permission.team_id', $tenantId); + + try { + foreach ($payload['roles'] as $roleName) { + $user->assignRole($roleName); + } + } finally { + app()->instance('permission.team_id', $previousTeam); + } + } + + return ApiResponse::response('result', [ + 'user' => $user->only(['id','user_id','name','email','phone']), + ]); + }); + } + + /** + * [GET] 테넌트 사용자 단건 조회 + * - 컨트롤러 show()에서 호출 + */ + public static function show(int $userNo) + { + $tenantId = app('tenant_id'); + if (!$tenantId) { + return ApiResponse::error('활성 테넌트가 없습니다.', 400); + } + + if (!$userNo) { + return ApiResponse::error('회원 정보가 없습니다.', 422); + } + + $user = User::whereHas('userTenants')->find($userNo); + + if (!$user) { + return ApiResponse::error('해당 사용자를 찾을 수 없습니다.', 404); + } + + return ApiResponse::response('result', $user); + } + + /** + * [PUT/PATCH] 테넌트 사용자 정보 수정 + * - 회원 기본정보(user_id, name, email, phone, password) 변경 + * - 역할(roles) 변경 및 삭제 처리 + */ + public static function update(array $params = [], int $userNo) + { + $tenantId = app('tenant_id'); + if (!$tenantId) { + return ApiResponse::error('활성 테넌트가 없습니다.', 400); + } + if (!$userNo) { + return ApiResponse::error('회원 정보가 없습니다.', 422); + } + + // 1) 유저 존재/테넌트 소속 확인 + $user = User::find($userNo); + if (!$user) { + return ApiResponse::error('해당 회원을 찾을 수 없습니다.', 404); + } + $linked = UserTenant::where('tenant_id', $tenantId) + ->where('user_id', $userNo) + ->exists(); + if (!$linked) { + return ApiResponse::error('이 테넌트에 소속된 회원이 아닙니다.', 403); + } + + // 2) 프로필 + roles만 수정 + $v = Validator::make($params, [ + 'user_id' => ['nullable','string','max:255', Rule::unique('users','user_id')->ignore($userNo)], + 'name' => 'nullable|string|max:255', + 'email' => ['nullable','email','max:100', Rule::unique('users','email')->ignore($userNo)], + 'phone' => 'nullable|string|max:30', + 'password' => 'nullable|string|min:8|max:64', + + 'roles' => 'nullable|array', + 'roles.*' => 'string|max:100', + ]); + if ($v->fails()) { + return ApiResponse::error($v->errors()->first(), 422); + } + $payload = $v->validated(); + + // 아무 필드도 없으면 방어 + $updatableKeys = ['user_id','name','email','phone','password']; + $hasProfileInput = (bool) array_intersect(array_keys($payload), $updatableKeys); + $hasRolesInput = array_key_exists('roles', $payload); + if (!$hasProfileInput && !$hasRolesInput) { + return ApiResponse::error('수정할 항목이 없습니다.', 422); + } + + return DB::transaction(function () use ($user, $payload, $tenantId, $updatableKeys) { + + // 3) 프로필 업데이트 (제공된 키만 반영) + $updateData = []; + foreach ($updatableKeys as $k) { + if (array_key_exists($k, $payload)) { + $updateData[$k] = $payload[$k]; + } + } + + // 비밀번호 처리 + if (array_key_exists('password', $updateData)) { + if ($updateData['password'] === null || $updateData['password'] === '') { + unset($updateData['password']); // 빈 값 들어오면 무시 + } + } + + if (!empty($updateData)) { + $user->fill($updateData); + $user->save(); + } + + // 4) 역할 수정 (teams 모드: 테넌트 컨텍스트로 sync) + if (array_key_exists('roles', $payload) && method_exists($user, 'syncRoles')) { + $previousTeam = app()->has('permission.team_id') ? app('permission.team_id') : null; + app()->instance('permission.team_id', $tenantId); + try { + // roles 키가 있으면 그 값으로 덮어쓰기 (빈 배열이면 모두 제거) + $roles = $payload['roles'] ?? []; + $user->syncRoles($roles); + } finally { + app()->instance('permission.team_id', $previousTeam); + } + } + + return ApiResponse::response('result', [ + 'user' => $user->only(['id','user_id','name','email','phone']), + 'roles' => method_exists($user, 'getRoleNames') ? $user->getRoleNames() : [], + ]); + }); + } + + /** + * [DELETE] 테넌트 사용자 삭제(연결 해제) + * - soft delete + left_at 기록 + */ + public static function destroy(int $userNo) + { + $tenantId = app('tenant_id'); + if (!$tenantId) { + return ApiResponse::error('활성 테넌트가 없습니다.', 400); + } + if (!$userNo) { + return ApiResponse::error('회원 정보가 없습니다.', 422); + } + + $ut = UserTenant::where('user_id',$userNo) + ->where('tenant_id', $tenantId) + ->first(); + + if (!$ut) { + return ApiResponse::error('해당 사용자를 찾을 수 없습니다.', 404); + } + + $ut->left_at = now(); + $ut->save(); + $ut->delete(); // SoftDeletes 가정 + + return ApiResponse::response('success'); + } + + /** + * [POST] 삭제 복구 + */ + public static function restore(int $userNo) + { + $tenantId = app('tenant_id'); + if (!$tenantId) { + return ApiResponse::error('활성 테넌트가 없습니다.', 400); + } + if (!$userNo) { + return ApiResponse::error('회원 정보가 없습니다.', 422); + } + + $ut = UserTenant::withTrashed() + ->where('tenant_id', $tenantId) + ->where('user_id', $userNo) + ->first(); + + if (!$ut) { + return ApiResponse::error('해당 사용자를 찾을 수 없습니다.', 404); + } + + if ($ut->trashed()) { + $ut->restore(); + $ut->left_at = null; + $ut->save(); + } + + return ApiResponse::response('success'); + } + + /** + * [PATCH] 활성/비활성 토글 + */ + public static function toggle(int $userNo) + { + $tenantId = app('tenant_id'); + if (!$tenantId) { + return ApiResponse::error('활성 테넌트가 없습니다.', 400); + } + if (!$userNo) { + return ApiResponse::error('회원 정보가 없습니다.', 422); + } + + $ut = UserTenant::where('tenant_id', $tenantId) + ->where('user_id', $userNo) + ->first(); + + if (!$ut) { + return ApiResponse::error('해당 사용자를 찾을 수 없습니다.', 404); + } + + $ut->is_active = $ut->is_active ? 0 : 1; + $ut->save(); + + return ApiResponse::response('result',['is_active' => $ut->is_active]); + } + + /** + * [POST] 역할 부여 (Spatie Permission - teams 사용 가정) + * - params: user_id, role_name + */ + public static function attach(array $params = []) + { + $tenantId = app('tenant_id'); + if (!$tenantId) { + return ApiResponse::error('활성 테넌트가 없습니다.', 400); + } + + $v = Validator::make($params, [ + 'user_id' => 'required|integer|exists:users,id', + 'role_name' => 'required|string|max:100', + ]); + if ($v->fails()) { + return ApiResponse::error($v->errors()->first(), 422); + } + + $user = User::find($params['user_id']); + if (!method_exists($user, 'assignRole')) { + // Spatie 미사용 환경 방어 + return ApiResponse::error('역할 시스템이 활성화되어 있지 않습니다.', 501); + } + + // teams(tenant) 스코프 + $previousTeam = app()->has('permission.team_id') ? app('permission.team_id') : null; + app()->instance('permission.team_id', $tenantId); + + try { + $user->assignRole($params['role_name']); + } finally { + // 원복 + app()->instance('permission.team_id', $previousTeam); + } + + return ApiResponse::response('success'); + } + + /** + * [POST] 역할 해제 (Spatie Permission - teams 사용 가정) + * - params: user_id, role_name + */ + public static function detach(array $params = []) + { + $tenantId = app('tenant_id'); + if (!$tenantId) { + return ApiResponse::error('활성 테넌트가 없습니다.', 400); + } + + $v = Validator::make($params, [ + 'user_id' => 'required|integer|exists:users,id', + 'role_name' => 'required|string|max:100', + ]); + if ($v->fails()) { + return ApiResponse::error($v->errors()->first(), 422); + } + + $user = User::find($params['user_id']); + if (!method_exists($user, 'removeRole')) { + return ApiResponse::error('역할 시스템이 활성화되어 있지 않습니다.', 501); + } + + $previousTeam = app()->has('permission.team_id') ? app('permission.team_id') : null; + app()->instance('permission.team_id', $tenantId); + + try { + $user->removeRole($params['role_name']); + } finally { + app()->instance('permission.team_id', $previousTeam); + } + + return ApiResponse::response('success'); + } + + /** + * [POST] 테넌트 사용자 비밀번호 초기화 + * - (보안) 관리자 권한 확인은 미들웨어/가드에서 처리 가정 + * - 새 임시 비밀번호를 설정(응답으로 직접 노출 X 권장) + * - 여기서는 옵션에 따라 노출/미노출 선택 가능하도록 구현 + */ + public static function reset(array $params = [],int $userNo) + { + $tenantId = app('tenant_id'); + if (!$tenantId) { + return ApiResponse::error('활성 테넌트가 없습니다.', 400); + } + if (!$userNo) { + return ApiResponse::error('회원 정보가 없습니다.', 422); + } + + $v = Validator::make($params, [ + 'new_password' => 'nullable|string|min:8|max:64', + 'return_password' => 'nullable|in:0,1', // 1이면 응답에 임시 비번 포함(개발용) + ]); + if ($v->fails()) { + return ApiResponse::error($v->errors()->first(), 422); + } + $payload = $v->validated(); + + $user = User::find($userNo); + if (!$user) { + return ApiResponse::error('유저를 찾을 수 없습니다.', 404); + } + + $new = $payload['new_password'] ?? Str::random(12); + $user->password = $new; + $user->save(); + + // (선택) 기존 토큰 무효화 + // if (method_exists($user, 'tokens')) { $user->tokens()->delete(); } + + $resp = ['status' => 'ok']; + if (!empty($payload['return_password'])) { + // 운영에선 반환하지 말고 메일/문자 발송을 권장 + $resp['temp_password'] = $new; + } + + return ApiResponse::response('result', $resp); + } +} diff --git a/app/Services/TenantService.php b/app/Services/TenantService.php index 262b4fe..db023b0 100644 --- a/app/Services/TenantService.php +++ b/app/Services/TenantService.php @@ -257,6 +257,14 @@ public static function storeTenants(array $params = []) $tenant = Tenant::create($payload); + // 기존 기본값(is_default=1) 해제 + $apiUser = app('api_user'); + UserTenant::withoutGlobalScopes() + ->where('user_id', $apiUser) + ->where('is_default', 1) + ->update(['is_default' => 0]); + + // 성성된 테넌트를 나의 테넌트로 셋팅 $apiUser = app('api_user'); UserTenant::create([ diff --git a/app/Swagger/v1/AdminApi.php b/app/Swagger/v1/AdminApi.php index e55706e..e410eb1 100644 --- a/app/Swagger/v1/AdminApi.php +++ b/app/Swagger/v1/AdminApi.php @@ -17,7 +17,7 @@ class AdminApi * @OA\Parameter(name="q", in="query", description="이름/이메일 검색어", @OA\Schema(type="string")), * @OA\Parameter(name="tenant_id", in="query", description="특정 테넌트로 필터", @OA\Schema(type="integer", example=1)), * @OA\Parameter(name="role", in="query", description="역할 코드", @OA\Schema(type="string", example="manager")), - * @OA\Parameter(name="is_active", in="query", description="활성여부", @OA\Schema(type="boolean", example=true)), + * @OA\Parameter(name="is_active", in="query", description="활성여부", @OA\Schema(type="boolean", example=1)), * @OA\Parameter(ref="#/components/parameters/Page"), * @OA\Parameter(ref="#/components/parameters/Size"), * @OA\Response( @@ -57,7 +57,7 @@ public function index() {} * @OA\JsonContent( * type="object", * required={"name","email","password"}, - * @OA\Property(property="tenant_id", type="integer", example=1), + * @OA\Property(property="user_id", type="string", example="test001"), * @OA\Property(property="name", type="string", example="김관리"), * @OA\Property(property="email", type="string", example="admin@kdcorp.co.kr"), * @OA\Property(property="password", type="string", example="Init!2345"), @@ -124,7 +124,7 @@ public function show() {} * type="object", * @OA\Property(property="name", type="string", example="김관리"), * @OA\Property(property="phone", type="string", example="010-3333-4444"), - * @OA\Property(property="is_active", type="boolean", example=true), + * @OA\Property(property="is_active", type="integer", example=1), * @OA\Property(property="roles", type="array", @OA\Items(type="string"), example={"manager","staff"}) * ) * ), @@ -144,25 +144,29 @@ public function update() {} * path="/api/v1/admin/users/{id}/status", * tags={"Admin-Users"}, * summary="활성/비활성 전환", - * description="is_active 토글", + * description="지정된 사용자의 is_active 상태를 변경합니다.", * security={{"ApiKeyAuth":{}},{"BearerAuth":{}}}, - * @OA\Parameter(name="id", in="path", required=true, @OA\Schema(type="integer")), - * @OA\RequestBody(required=true, - * @OA\JsonContent(type="object", - * required={"is_active"}, - * @OA\Property(property="is_active", type="boolean", example=false) + * @OA\Parameter( + * name="id", + * in="path", + * required=true, + * description="사용자 고유 ID", + * @OA\Schema(type="integer") + * ), + * @OA\Response( + * response=200, + * description="변경 성공", + * @OA\JsonContent( + * type="object", + * @OA\Property(property="success", type="boolean", example=true), + * @OA\Property(property="message", type="string", example="테넌트 사용자 활성/비활성 성공"), + * @OA\Property( + * property="data", + * type="object", + * @OA\Property(property="is_active", type="integer", example=1) + * ) * ) * ), - * - * @OA\Response( - * response=204, - * description="변경 성공(콘텐츠 없음)", - * @OA\JsonContent( - * @OA\Property(property="success", type="boolean", example=true), - * @OA\Property(property="message", type="string", example="변경 성공"), - * @OA\Property(property="data", type="object", nullable=true, example=null) - * ) - * ), * @OA\Response(response=400, description="필수 파라미터 누락", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")), * @OA\Response(response=401, description="인증 실패", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")), * @OA\Response(response=403, description="권한 없음", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")), @@ -187,7 +191,7 @@ public function toggleStatus() {} * @OA\JsonContent( * @OA\Property(property="success", type="boolean", example=true), * @OA\Property(property="message", type="string", example="변경 성공"), - * @OA\Property(property="data", type="object", nullable=true, example=null) + * @OA\Property(property="data", type="string", example="Success") * ) * ), * @OA\Response(response=401, description="인증 실패", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")), @@ -213,7 +217,7 @@ public function destroy() {} * @OA\JsonContent( * @OA\Property(property="success", type="boolean", example=true), * @OA\Property(property="message", type="string", example="변경 성공"), - * @OA\Property(property="data", type="object", nullable=true, example=null) + * @OA\Property(property="data", type="string", example="Success") * ) * ), * @OA\Response(response=401, description="인증 실패", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")), @@ -293,16 +297,40 @@ public function detachRole() {} * path="/api/v1/admin/users/{id}/reset-password", * tags={"Admin-Users"}, * summary="비밀번호 초기화", - * description="임시 비밀번호 발급(또는 링크 전송)", + * description="지정된 사용자의 비밀번호를 새 임시 비밀번호로 초기화합니다. + * - 관리자 권한 확인은 미들웨어/가드에서 처리됩니다. + * - 기본적으로 응답에 비밀번호를 노출하지 않으며, return_password=1일 때만 임시 비밀번호를 반환합니다(운영 환경에서는 노출 비권장).", * security={{"ApiKeyAuth":{}},{"BearerAuth":{}}}, - * @OA\Parameter(name="id", in="path", required=true, @OA\Schema(type="integer")), + * @OA\Parameter( + * name="id", + * in="path", + * required=true, + * description="사용자 고유 ID", + * @OA\Schema(type="integer") + * ), * @OA\RequestBody( * required=false, - * @OA\JsonContent(type="object", - * @OA\Property(property="temp_password", type="string", example="Temp!1234", description="미지정 시 서버에서 생성") + * @OA\JsonContent( + * type="object", + * @OA\Property(property="new_password", type="string", minLength=8, maxLength=64, example="Temp!1234", description="지정 시 해당 값으로 비밀번호 초기화, 미지정 시 서버에서 랜덤 생성"), + * @OA\Property(property="return_password", type="integer", enum={0,1}, example=0, description="1이면 응답에 임시 비밀번호 포함(개발/테스트용)") + * ) + * ), + * @OA\Response( + * response=200, + * description="초기화 성공", + * @OA\JsonContent( + * type="object", + * @OA\Property(property="success", type="boolean", example=true), + * @OA\Property(property="message", type="string", example="테넌트 사용자 비밀번호 초기화 성공"), + * @OA\Property( + * property="data", + * type="object", + * @OA\Property(property="status", type="string", example="ok"), + * @OA\Property(property="temp_password", type="string", example="Temp!1234", nullable=true, description="return_password=1일 때만 포함") + * ) * ) * ), - * @OA\Response(response=200, description="초기화 성공", @OA\JsonContent(ref="#/components/schemas/ApiResponse")), * @OA\Response(response=400, description="필수 파라미터 누락", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")), * @OA\Response(response=401, description="인증 실패", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")), * @OA\Response(response=403, description="권한 없음", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")), diff --git a/routes/api.php b/routes/api.php index aaddf4a..ed75cd4 100644 --- a/routes/api.php +++ b/routes/api.php @@ -12,6 +12,7 @@ use App\Http\Controllers\Api\V1\BomController; use App\Http\Controllers\Api\V1\UserController; use App\Http\Controllers\Api\V1\TenantController; +use App\Http\Controllers\Api\V1\AdminController; // error test Route::get('/test-error', function () { @@ -48,6 +49,32 @@ }); + // Tenant Admin API + Route::prefix('admin')->group(function () { + // 목록/생성 + Route::get('users', [AdminController::class, 'index'])->name('v1.admin.users.index'); // 테넌트 사용자 목록 조회 + Route::post('users', [AdminController::class, 'store'])->name('v1.admin.users.store'); // 테넌트 사용자 생성 + + // 단건 + Route::get('users/{id}', [AdminController::class, 'show'])->name('v1.admin.users.show'); // 테넌트 사용자 단건 조회 + Route::put('users/{id}', [AdminController::class, 'update'])->name('v1.admin.users.update'); // 테넌트 사용자 수정 + + // 소프트 삭제 복구 + Route::delete('users/{id}', [AdminController::class, 'destroy'])->name('v1.admin.users.destroy'); // 테넌트 사용자 삭제(연결 삭제) + Route::post('users/{id}/restore', [AdminController::class, 'restore'])->name('v1.admin.users.restore'); // 테넌트 사용자 삭제 복구 + + // 상태 토글 + Route::patch('users/{id}/status', [AdminController::class, 'toggle'])->name('v1.admin.users.status.toggle'); // 테넌트 사용자 활성/비활성 + + // 역할 부여/해제 + Route::post('users/{id}/roles', [AdminController::class, 'attach'])->name('v1.admin.users.roles.attach'); // 테넌트 사용자 역할 부여 + Route::delete('users/{id}/roles/{role}', [AdminController::class, 'detach'])->name('v1.admin.users.roles.detach'); // 테넌트 사용자 역할 해제 + + // 비밀번호 초기화 + Route::post('users/{id}/reset-password', [AdminController::class, 'reset'])->name('v1.admin.users.password.reset'); // 테넌트 사용자 비밀번호 초기화 + }); + + // Member API Route::prefix('users')->group(function () { Route::get('index', [UserController::class, 'index'])->name('v1.users.index'); // 회원 목록 조회 @@ -91,51 +118,6 @@ }); -// ───────────────────────────────────────────────────────────── -// 공통 미들웨어 메모: -// - 'apikey' : X-API-KEY 검사 미들웨어 (커스텀) -// - 'auth:sanctum' : Bearer 토큰(Sanctum) 인증 -// 필요 시 app/Http/Kernel.php 의 $routeMiddleware 에 별칭 등록 -// ───────────────────────────────────────────────────────────── - -/* -|-------------------------------------------------------------------------- -| V1 - User 영역 (본인 계정) -|-------------------------------------------------------------------------- -| Swagger: UserApi.php -| - POST /api/v1/auth/login -| - POST /api/v1/auth/logout -| - GET /api/v1/users/me -| - PUT /api/v1/users/me -| - PUT /api/v1/users/me/password -| - GET /api/v1/users/me/tenants -| - PATCH /api/v1/users/me/tenants/switch -*/ -Route::prefix('v1_DEV') - ->middleware(['apikey']) // 모든 엔드포인트는 X-API-KEY 필요 - ->group(function () { - - // Auth (User) - Route::prefix('auth')->group(function () { - Route::post('login', [\App\Http\Controllers\Api\V1\AuthController::class, 'login']) - ->name('v1.auth.login'); // Bearer 불필요(로그인) - - Route::post('logout', [\App\Http\Controllers\Api\V1\AuthController::class, 'logout']) - ->middleware('auth:sanctum') - ->name('v1.auth.logout'); - }); - - // Users (me) - Route::prefix('users')->middleware('auth:sanctum')->group(function () { - Route::get('me', [\App\Http\Controllers\Api\V1\User\MeController::class, 'show'])->name('v1.users.me.show'); - Route::put('me', [\App\Http\Controllers\Api\V1\User\MeController::class, 'update'])->name('v1.users.me.update'); - Route::put('me/password', [\App\Http\Controllers\Api\V1\User\MeController::class, 'changePassword'])->name('v1.users.me.password'); - - Route::get('me/tenants', [\App\Http\Controllers\Api\V1\User\TenantController::class, 'index'])->name('v1.users.me.tenants.index'); - Route::patch('me/tenants/switch', [\App\Http\Controllers\Api\V1\User\TenantController::class, 'switch'])->name('v1.users.me.tenants.switch'); - }); - }); - /* |-------------------------------------------------------------------------- @@ -157,25 +139,5 @@ ->middleware(['apikey', 'auth:sanctum', 'can:admin']) // 예: 'can:admin' 또는 커스텀 'is_admin' ->group(function () { - // 목록/생성 - Route::get('users', [\App\Http\Controllers\Api\V1\Admin\UserController::class, 'index'])->name('v1.admin.users.index'); - Route::post('users', [\App\Http\Controllers\Api\V1\Admin\UserController::class, 'store'])->name('v1.admin.users.store'); - // 단건 - Route::get('users/{id}', [\App\Http\Controllers\Api\V1\Admin\UserController::class, 'show'])->name('v1.admin.users.show'); - Route::put('users/{id}', [\App\Http\Controllers\Api\V1\Admin\UserController::class, 'update'])->name('v1.admin.users.update'); - Route::delete('users/{id}', [\App\Http\Controllers\Api\V1\Admin\UserController::class, 'destroy'])->name('v1.admin.users.destroy'); - - // 상태 토글 - Route::patch('users/{id}/status', [\App\Http\Controllers\Api\V1\Admin\UserStatusController::class, 'toggle'])->name('v1.admin.users.status.toggle'); - - // 소프트 삭제 복구 - Route::post('users/{id}/restore', [\App\Http\Controllers\Api\V1\Admin\UserRestoreController::class, 'restore'])->name('v1.admin.users.restore'); - - // 역할 부여/해제 - Route::post('users/{id}/roles', [\App\Http\Controllers\Api\V1\Admin\UserRoleController::class, 'attach'])->name('v1.admin.users.roles.attach'); - Route::delete('users/{id}/roles/{role}', [\App\Http\Controllers\Api\V1\Admin\UserRoleController::class, 'detach'])->name('v1.admin.users.roles.detach'); - - // 비밀번호 초기화 - Route::post('users/{id}/reset-password', [\App\Http\Controllers\Api\V1\Admin\UserPasswordController::class, 'reset'])->name('v1.admin.users.password.reset'); });