middleware('auth:api'); $this->middleware('permission:platform.servers.manage,api')->except(['updateBoundPassword']); } #[Apidoc\Title('修改当前用户绑定服务器账号密码'), Apidoc\Method('PATCH'), Apidoc\Url('/servers/{id}/bound-system-user/password')] public function updateBoundPassword(Request $request, int $id): JsonResponse { $validated = $request->validate([ 'password' => ['required', 'string', 'min:6', 'max:255'], ]); $server = $this->server($id); $resource = ServerResource::query()->with('parent')->findOrFail($id); /** @var User|null $user */ $user = auth('api')->user(); if (! $user || ! $this->canUpdateBoundPassword($user, $resource)) { return response()->json([ 'code' => 403, 'message' => '无权限修改该服务器账号密码', 'data' => null, ], 403); } $binding = ServerUserBinding::query() ->where('user_id', $user->id) ->where('server_resource_id', $server->id) ->first(); if (! $binding || trim((string) $binding->username) === '') { throw ValidationException::withMessages([ 'server' => ['当前用户未绑定该服务器账号。'], ]); } $result = $this->client->updatePassword($server, (string) $binding->username, $this->linuxPasswordHash((string) $validated['password'])); $binding->update([ 'remote_exists' => true, 'last_synced_at' => now(), ]); $this->auditLog($request, 'server_bound_user_password_update', [ 'server_resource_id' => $server->id, 'metadata' => ['username' => $binding->username], ]); return response()->json(['code' => 0, 'message' => 'ok', 'data' => $result]); } #[Apidoc\Title('服务器用户管理元数据'), Apidoc\Method('GET'), Apidoc\Url('/servers/{id}/system-users/meta')] public function meta(int $id): JsonResponse { $server = $this->server($id); $users = $this->client->users($server); $groups = $this->client->groups($server); $userGroups = []; foreach ($users as $user) { $username = (string) ($user['username'] ?? ''); if ($username === '') { continue; } try { $userGroups[$username] = $this->client->userGroups($server, $username)['groups'] ?? []; } catch (ValidationException) { $userGroups[$username] = []; } } $bindings = ServerUserBinding::query() ->with('user:id,nickname,email,phone') ->where('server_resource_id', $server->id) ->orderBy('username') ->get(); return response()->json([ 'code' => 0, 'message' => 'ok', 'data' => [ 'server' => $server, 'users' => $users, 'groups' => $groups, 'user_groups' => $userGroups, 'bindings' => $bindings, ], ]); } #[Apidoc\Title('创建服务器用户'), Apidoc\Method('POST'), Apidoc\Url('/servers/{id}/system-users')] public function storeUser(Request $request, int $id): JsonResponse { $validated = $request->validate([ 'username' => ['required', 'string', 'max:32', 'regex:/^[a-z_][a-z0-9_-]{0,31}$/'], 'password_hash' => ['nullable', 'string', 'min:10', 'max:512'], 'password' => ['nullable', 'string', 'min:6', 'max:255'], 'primary_group' => ['nullable', 'string', 'max:32', 'regex:/^[a-z_][a-z0-9_-]{0,31}$/'], 'groups' => ['sometimes', 'array'], 'groups.*' => ['string', 'max:32', 'regex:/^[a-z_][a-z0-9_-]{0,31}$/'], 'shell' => ['nullable', 'string', 'max:128'], 'home_dir' => ['nullable', 'string', 'max:255'], 'user_id' => ['nullable', 'integer', 'exists:users,id'], ]); $server = $this->server($id); $passwordHash = (string) ($validated['password_hash'] ?? ''); if ($passwordHash === '') { $passwordHash = $this->linuxPasswordHash((string) ($validated['password'] ?? '')); } $payload = [ 'username' => $validated['username'], 'password_hash' => $passwordHash, 'primary_group' => $validated['primary_group'] ?? null, 'groups' => array_values(array_unique($validated['groups'] ?? [])), 'shell' => $validated['shell'] ?? '/bin/bash', 'home_dir' => $validated['home_dir'] ?? null, ]; $result = $this->client->createUser($server, $payload); $this->upsertBinding((int) ($validated['user_id'] ?? 0), $server, (string) $validated['username'], true); $this->auditLog($request, 'server_system_user_create', [ 'server_resource_id' => $server->id, 'metadata' => ['username' => $validated['username']], ]); return response()->json(['code' => 0, 'message' => 'ok', 'data' => $result], 201); } #[Apidoc\Title('删除服务器用户'), Apidoc\Method('DELETE'), Apidoc\Url('/servers/{id}/system-users/{username}')] public function destroyUser(Request $request, int $id, string $username): JsonResponse { $server = $this->server($id); $result = $this->client->deleteUser($server, $username); ServerUserBinding::query() ->where('server_resource_id', $server->id) ->where('username', $username) ->update(['remote_exists' => false, 'last_synced_at' => now()]); $this->auditLog($request, 'server_system_user_delete', [ 'server_resource_id' => $server->id, 'metadata' => ['username' => $username], ]); return response()->json(['code' => 0, 'message' => 'ok', 'data' => $result]); } #[Apidoc\Title('修改服务器用户密码'), Apidoc\Method('PATCH'), Apidoc\Url('/servers/{id}/system-users/{username}/password')] public function updatePassword(Request $request, int $id, string $username): JsonResponse { $validated = $request->validate([ 'password_hash' => ['nullable', 'string', 'min:10', 'max:512'], 'password' => ['nullable', 'string', 'min:6', 'max:255'], ]); $server = $this->server($id); $passwordHash = (string) ($validated['password_hash'] ?? ''); if ($passwordHash === '') { $passwordHash = $this->linuxPasswordHash((string) ($validated['password'] ?? '')); } $result = $this->client->updatePassword($server, $username, $passwordHash); $this->auditLog($request, 'server_system_user_password_update', [ 'server_resource_id' => $server->id, 'metadata' => ['username' => $username], ]); return response()->json(['code' => 0, 'message' => 'ok', 'data' => $result]); } #[Apidoc\Title('创建服务器用户组'), Apidoc\Method('POST'), Apidoc\Url('/servers/{id}/system-groups')] public function storeGroup(Request $request, int $id): JsonResponse { $validated = $request->validate([ 'groupname' => ['required', 'string', 'max:32', 'regex:/^[a-z_][a-z0-9_-]{0,31}$/'], ]); $server = $this->server($id); $result = $this->client->createGroup($server, (string) $validated['groupname']); $this->auditLog($request, 'server_system_group_create', [ 'server_resource_id' => $server->id, 'metadata' => ['groupname' => $validated['groupname']], ]); return response()->json(['code' => 0, 'message' => 'ok', 'data' => $result], 201); } #[Apidoc\Title('删除服务器用户组'), Apidoc\Method('DELETE'), Apidoc\Url('/servers/{id}/system-groups/{groupname}')] public function destroyGroup(Request $request, int $id, string $groupname): JsonResponse { $server = $this->server($id); $result = $this->client->deleteGroup($server, $groupname); $this->auditLog($request, 'server_system_group_delete', [ 'server_resource_id' => $server->id, 'metadata' => ['groupname' => $groupname], ]); return response()->json(['code' => 0, 'message' => 'ok', 'data' => $result]); } #[Apidoc\Title('同步服务器用户组'), Apidoc\Method('PUT'), Apidoc\Url('/servers/{id}/system-users/{username}/groups')] public function syncUserGroups(Request $request, int $id, string $username): JsonResponse { $validated = $request->validate([ 'groups' => ['present', 'array'], 'groups.*' => ['string', 'max:32', 'regex:/^[a-z_][a-z0-9_-]{0,31}$/'], 'mode' => ['nullable', 'string', 'in:append,replace'], ]); $server = $this->server($id); $result = $this->client->syncUserGroups( $server, $username, array_values(array_unique($validated['groups'])), (string) ($validated['mode'] ?? 'replace'), ); $this->auditLog($request, 'server_system_user_groups_update', [ 'server_resource_id' => $server->id, 'metadata' => ['username' => $username, 'groups' => $validated['groups']], ]); return response()->json(['code' => 0, 'message' => 'ok', 'data' => $result]); } private function server(int $id): ServerResource { $server = ServerResource::query()->with('parent')->findOrFail($id); return $server->parent_id ? $server->parent()->firstOrFail() : $server; } private function upsertBinding(int $userId, ServerResource $server, string $username, bool $remoteExists): void { if ($userId <= 0) { return; } ServerUserBinding::query()->updateOrCreate( ['user_id' => $userId, 'server_resource_id' => $server->id], ['username' => $username, 'remote_exists' => $remoteExists, 'last_synced_at' => now()], ); } private function canUpdateBoundPassword(User $user, ServerResource $resource): bool { if ($user->can('platform.servers.view')) { return true; } $serverId = (int) ($resource->parent_id ?: $resource->id); $hasBinding = ServerUserBinding::query() ->where('user_id', $user->id) ->where('server_resource_id', $serverId) ->exists(); if (! $hasBinding) { return false; } if (! $resource->parent_id) { return true; } $pivot = $resource->users() ->where('users.id', $user->id) ->first()?->pivot; if ($pivot && (bool) ($pivot->can_ssh || $pivot->can_sftp || $pivot->can_rdp)) { return true; } return $user->can('resource.servers.use'); } private function linuxPasswordHash(string $password): string { if ($password === '') { throw ValidationException::withMessages([ 'password' => ['请输入服务器账号密码。'], ]); } $salt = substr(strtr(base64_encode(random_bytes(12)), '+', '.'), 0, 16); $hash = crypt($password, '$6$rounds=5000$'.$salt.'$'); if (! is_string($hash) || strlen($hash) < 10) { throw ValidationException::withMessages([ 'password' => ['服务器账号密码 Hash 生成失败。'], ]); } return $hash; } }