feat: 完成基础工程编写
This commit is contained in:
commit
6afa6d7169
18
.editorconfig
Normal file
18
.editorconfig
Normal file
@ -0,0 +1,18 @@
|
||||
root = true
|
||||
|
||||
[*]
|
||||
charset = utf-8
|
||||
end_of_line = lf
|
||||
indent_size = 4
|
||||
indent_style = space
|
||||
insert_final_newline = true
|
||||
trim_trailing_whitespace = true
|
||||
|
||||
[*.md]
|
||||
trim_trailing_whitespace = false
|
||||
|
||||
[*.{yml,yaml}]
|
||||
indent_size = 2
|
||||
|
||||
[docker-compose.yml]
|
||||
indent_size = 4
|
||||
71
.env.example
Normal file
71
.env.example
Normal file
@ -0,0 +1,71 @@
|
||||
APP_NAME=Laravel
|
||||
APP_ENV=local
|
||||
APP_KEY=
|
||||
APP_DEBUG=true
|
||||
APP_TIMEZONE=UTC
|
||||
APP_URL=http://localhost
|
||||
|
||||
APP_LOCALE=en
|
||||
APP_FALLBACK_LOCALE=en
|
||||
APP_FAKER_LOCALE=en_US
|
||||
|
||||
APP_MAINTENANCE_DRIVER=file
|
||||
APP_MAINTENANCE_STORE=database
|
||||
|
||||
BCRYPT_ROUNDS=12
|
||||
|
||||
LOG_CHANNEL=stack
|
||||
LOG_STACK=single
|
||||
LOG_DEPRECATIONS_CHANNEL=null
|
||||
LOG_LEVEL=debug
|
||||
|
||||
DB_CONNECTION=sqlite
|
||||
# DB_HOST=127.0.0.1
|
||||
# DB_PORT=3306
|
||||
# DB_DATABASE=laravel
|
||||
# DB_USERNAME=root
|
||||
# DB_PASSWORD=
|
||||
|
||||
SESSION_DRIVER=database
|
||||
SESSION_LIFETIME=120
|
||||
SESSION_ENCRYPT=false
|
||||
SESSION_PATH=/
|
||||
SESSION_DOMAIN=null
|
||||
|
||||
BROADCAST_CONNECTION=log
|
||||
FILESYSTEM_DISK=local
|
||||
QUEUE_CONNECTION=database
|
||||
|
||||
CACHE_STORE=database
|
||||
CACHE_PREFIX=
|
||||
|
||||
MEMCACHED_HOST=127.0.0.1
|
||||
|
||||
REDIS_CLIENT=phpredis
|
||||
REDIS_HOST=127.0.0.1
|
||||
REDIS_PASSWORD=null
|
||||
REDIS_PORT=6379
|
||||
|
||||
MAIL_MAILER=log
|
||||
MAIL_HOST=127.0.0.1
|
||||
MAIL_PORT=2525
|
||||
MAIL_USERNAME=null
|
||||
MAIL_PASSWORD=null
|
||||
MAIL_ENCRYPTION=null
|
||||
MAIL_FROM_ADDRESS="hello@example.com"
|
||||
MAIL_FROM_NAME="${APP_NAME}"
|
||||
|
||||
AWS_ACCESS_KEY_ID=
|
||||
AWS_SECRET_ACCESS_KEY=
|
||||
AWS_DEFAULT_REGION=us-east-1
|
||||
AWS_BUCKET=
|
||||
AWS_USE_PATH_STYLE_ENDPOINT=false
|
||||
|
||||
VITE_APP_NAME="${APP_NAME}"
|
||||
|
||||
BASTION_TOKEN_API_BASE_URL=http://127.0.0.1:8000
|
||||
BASTION_TOKEN_SUBMIT_ENDPOINT=/bastion_token
|
||||
BASTION_TOKEN_STATUS_ENDPOINT=/bastion_token/{task_id}
|
||||
BASTION_TOKEN_TIMEOUT=30
|
||||
BASTION_TOKEN_POLL_ATTEMPTS=20
|
||||
BASTION_TOKEN_POLL_INTERVAL_MS=500
|
||||
11
.gitattributes
vendored
Normal file
11
.gitattributes
vendored
Normal file
@ -0,0 +1,11 @@
|
||||
* text=auto eol=lf
|
||||
|
||||
*.blade.php diff=html
|
||||
*.css diff=css
|
||||
*.html diff=html
|
||||
*.md diff=markdown
|
||||
*.php diff=php
|
||||
|
||||
/.github export-ignore
|
||||
CHANGELOG.md export-ignore
|
||||
.styleci.yml export-ignore
|
||||
31
.gitignore
vendored
Normal file
31
.gitignore
vendored
Normal file
@ -0,0 +1,31 @@
|
||||
/.phpunit.cache
|
||||
/node_modules
|
||||
/public/build
|
||||
/public/hot
|
||||
/public/storage
|
||||
/storage/*.key
|
||||
/vendor
|
||||
.env
|
||||
.env.backup
|
||||
.env.production
|
||||
.phpunit.result.cache
|
||||
Homestead.json
|
||||
Homestead.yaml
|
||||
auth.json
|
||||
npm-debug.log
|
||||
yarn-error.log
|
||||
/.fleet
|
||||
/.idea
|
||||
/.vscode
|
||||
/.agents
|
||||
/.codex
|
||||
/bastion_sso
|
||||
/bastion_sso/*
|
||||
app/Http/Controllers/Api/优化.md
|
||||
apidoc_document.md
|
||||
BACKAPI.md
|
||||
REQUIRE.MD
|
||||
AGENTS.md
|
||||
CLAUDE.md
|
||||
.mcp.json
|
||||
boost.json
|
||||
124
README.md
Normal file
124
README.md
Normal file
@ -0,0 +1,124 @@
|
||||
# Bastion SSO API
|
||||
|
||||
统一 SSO 登录系统后端(Laravel 11),用于团队内部集中管理堡垒机访问授权、服务器资源与访问审计。
|
||||
|
||||
## 主要模块
|
||||
|
||||
- 登录模块:基于 `tymon/jwt-auth` 生成 JWT。
|
||||
- 用户及权限模块:基于 `spatie/laravel-permission` 实现角色与权限管理。
|
||||
- 服务器资源模块:维护服务器 `asset_id/account_id/protocols` 等信息。
|
||||
- 日志模块:记录用户访问服务器资源行为。
|
||||
- 堡垒机授权账号模块:维护授权账号并定时刷新 `USM-AUTHENTICATION` 与 `USM`。
|
||||
|
||||
## 安装与启动
|
||||
|
||||
```bash
|
||||
composer install
|
||||
cp .env.example .env
|
||||
php artisan key:generate
|
||||
php artisan jwt:secret --force
|
||||
```
|
||||
|
||||
按需将 `.env` 的数据库配置改为 MySQL:
|
||||
|
||||
```env
|
||||
DB_CONNECTION=mysql
|
||||
DB_HOST=127.0.0.1
|
||||
DB_PORT=3306
|
||||
DB_DATABASE=bastion_sso
|
||||
DB_USERNAME=root
|
||||
DB_PASSWORD=secret
|
||||
```
|
||||
|
||||
然后执行:
|
||||
|
||||
```bash
|
||||
php artisan migrate
|
||||
php artisan serve
|
||||
```
|
||||
|
||||
## RBAC 初始化(推荐)
|
||||
|
||||
首次部署建议初始化权限点和默认角色:
|
||||
|
||||
```bash
|
||||
php artisan user:manage init-rbac
|
||||
```
|
||||
|
||||
将某个用户设为管理员(拥有全部权限):
|
||||
|
||||
```bash
|
||||
php artisan user:manage set-admin --email=admin@example.com
|
||||
```
|
||||
|
||||
取消管理员:
|
||||
|
||||
```bash
|
||||
php artisan user:manage unset-admin --email=admin@example.com
|
||||
```
|
||||
|
||||
## 接口说明
|
||||
|
||||
- 认证:
|
||||
- `POST /auth/login`
|
||||
- `POST /auth/logout`
|
||||
- `GET /auth/me`
|
||||
- 用户:`/users`(GET/POST/GET{id}/PUT/DELETE)
|
||||
- 用户权限同步:`PUT /users/{id}/permissions`
|
||||
- 角色:`/roles`(GET/POST/GET{id}/PUT/DELETE)
|
||||
- 角色权限同步:`PUT /roles/{id}/permissions`
|
||||
- 权限:`/permissions`(GET/POST/GET{id}/PUT/DELETE)
|
||||
- 服务器:`/servers`(GET/POST/GET{id}/PUT/DELETE)
|
||||
- 日志:`/logs`(GET/POST)
|
||||
- 授权账号:`/accounts`(GET/POST/GET{id}/PUT/DELETE)
|
||||
|
||||
控制器使用 `hg/apidoc` 注解声明,并启用了注解自动注册路由。
|
||||
|
||||
## 权限控制说明
|
||||
|
||||
- 所有管理接口均通过中间件 `auth:api` + `permission:*` 控制。
|
||||
- 管理员角色:`admin`(guard: `api`)默认拥有所有平台权限。
|
||||
- 平台权限粒度示例:
|
||||
- `platform.users.view/manage`
|
||||
- `platform.roles.view/manage`
|
||||
- `platform.permissions.view/manage`
|
||||
- `platform.servers.view/manage`
|
||||
- `platform.accounts.view/manage`
|
||||
- `platform.logs.view/manage`
|
||||
- `resource.servers.use`
|
||||
|
||||
## 统一错误响应
|
||||
|
||||
- 未认证返回 `401`:
|
||||
|
||||
```json
|
||||
{"code":401,"message":"未认证或登录已过期","data":null}
|
||||
```
|
||||
|
||||
- 无权限返回 `403`:
|
||||
|
||||
```json
|
||||
{"code":403,"message":"无权限执行此操作","data":null}
|
||||
```
|
||||
|
||||
## 命令
|
||||
|
||||
- 刷新堡垒机 token:`php artisan bastion:refresh-tokens`
|
||||
- 用户与权限管理:
|
||||
|
||||
```bash
|
||||
php artisan user:manage {create|reset-password|list|init-rbac|create-role|assign-role|remove-role|assign-permission|remove-permission|grant-server|set-admin|unset-admin}
|
||||
```
|
||||
|
||||
常见示例:
|
||||
|
||||
```bash
|
||||
php artisan user:manage create --email=user@example.com --nickname=user --password=secret123
|
||||
php artisan user:manage assign-role --email=user@example.com --role=operator
|
||||
php artisan user:manage assign-permission --email=user@example.com --permission=platform.logs.view
|
||||
php artisan user:manage grant-server --email=user@example.com --server-id=1 --ssh=1 --sftp=1 --rdp=0
|
||||
```
|
||||
|
||||
## 定时任务
|
||||
|
||||
调度任务在 `routes/console.php` 中配置,每 10 分钟刷新一次堡垒机 token。
|
||||
31
app/Console/Commands/BastionRefreshTokensCommand.php
Normal file
31
app/Console/Commands/BastionRefreshTokensCommand.php
Normal file
@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Models\BastionAccount;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class BastionRefreshTokensCommand extends Command
|
||||
{
|
||||
protected $signature = 'bastion:refresh-tokens';
|
||||
|
||||
protected $description = 'Refresh USM and USM-AUTHENTICATION for active bastion accounts';
|
||||
|
||||
public function handle(): int
|
||||
{
|
||||
BastionAccount::query()->where('is_active', true)->chunkById(100, function ($accounts): void {
|
||||
foreach ($accounts as $account) {
|
||||
$account->update([
|
||||
'usm_authentication' => Str::uuid()->toString(),
|
||||
'usm' => hash('sha256', Str::uuid()->toString()),
|
||||
'last_token_refreshed_at' => now(),
|
||||
]);
|
||||
}
|
||||
});
|
||||
|
||||
$this->info('Bastion tokens refreshed.');
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
}
|
||||
372
app/Console/Commands/UserManageCommand.php
Normal file
372
app/Console/Commands/UserManageCommand.php
Normal file
@ -0,0 +1,372 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Models\User;
|
||||
use App\Models\UserServerPermission;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Spatie\Permission\Models\Permission;
|
||||
use Spatie\Permission\Models\Role;
|
||||
|
||||
class UserManageCommand extends Command
|
||||
{
|
||||
protected $signature = 'user:manage
|
||||
{action : create|reset-password|list|init-rbac|create-role|assign-role|remove-role|assign-permission|remove-permission|grant-server|set-admin|unset-admin}
|
||||
{--email= : User email}
|
||||
{--nickname= : User nickname}
|
||||
{--phone= : User phone}
|
||||
{--password= : User password}
|
||||
{--role= : Role name}
|
||||
{--permission= : Permission name}
|
||||
{--server-id= : Server resource id}
|
||||
{--ssh=0 : Grant ssh 1|0}
|
||||
{--sftp=0 : Grant sftp 1|0}
|
||||
{--rdp=0 : Grant rdp 1|0}';
|
||||
|
||||
protected $description = 'User and permission management command';
|
||||
|
||||
public function handle(): int
|
||||
{
|
||||
return match ($this->argument('action')) {
|
||||
'create' => $this->createUser(),
|
||||
'reset-password' => $this->resetPassword(),
|
||||
'list' => $this->listUsers(),
|
||||
'init-rbac' => $this->initRbac(),
|
||||
'create-role' => $this->createRole(),
|
||||
'assign-role' => $this->assignRole(),
|
||||
'remove-role' => $this->removeRole(),
|
||||
'assign-permission' => $this->assignPermission(),
|
||||
'remove-permission' => $this->removePermission(),
|
||||
'grant-server' => $this->grantServerPermission(),
|
||||
'set-admin' => $this->setAdmin(),
|
||||
'unset-admin' => $this->unsetAdmin(),
|
||||
default => $this->invalidAction(),
|
||||
};
|
||||
}
|
||||
|
||||
private function createUser(): int
|
||||
{
|
||||
$email = (string) $this->option('email');
|
||||
$nickname = (string) $this->option('nickname');
|
||||
$password = (string) $this->option('password');
|
||||
|
||||
if ($email === '' || $nickname === '' || $password === '') {
|
||||
$this->error('email, nickname, password are required.');
|
||||
|
||||
return self::FAILURE;
|
||||
}
|
||||
|
||||
$user = User::query()->create([
|
||||
'email' => $email,
|
||||
'nickname' => $nickname,
|
||||
'phone' => $this->option('phone') ?: null,
|
||||
'password' => $password,
|
||||
]);
|
||||
|
||||
if ($this->option('role')) {
|
||||
$role = Role::query()->firstOrCreate([
|
||||
'name' => (string) $this->option('role'),
|
||||
'guard_name' => 'api',
|
||||
]);
|
||||
$user->assignRole($role);
|
||||
}
|
||||
|
||||
$this->info("User created: {$user->id}");
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
|
||||
private function resetPassword(): int
|
||||
{
|
||||
$user = $this->findUserByEmail();
|
||||
if (! $user) {
|
||||
return self::FAILURE;
|
||||
}
|
||||
|
||||
$password = (string) $this->option('password');
|
||||
|
||||
if ($password === '') {
|
||||
$this->error('password is required.');
|
||||
|
||||
return self::FAILURE;
|
||||
}
|
||||
|
||||
$user->password = Hash::make($password);
|
||||
$user->save();
|
||||
|
||||
$this->info('Password reset success.');
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
|
||||
private function listUsers(): int
|
||||
{
|
||||
$rows = User::query()->with(['roles', 'permissions'])->latest()->get()->map(fn (User $user): array => [
|
||||
'id' => $user->id,
|
||||
'nickname' => $user->nickname,
|
||||
'email' => $user->email,
|
||||
'roles' => $user->roles->pluck('name')->implode(','),
|
||||
'permissions' => $user->permissions->pluck('name')->implode(','),
|
||||
])->toArray();
|
||||
|
||||
$this->table(['ID', 'Nickname', 'Email', 'Roles', 'Permissions'], $rows);
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
|
||||
private function initRbac(): int
|
||||
{
|
||||
$permissions = [
|
||||
'platform.users.view',
|
||||
'platform.users.manage',
|
||||
'platform.roles.view',
|
||||
'platform.roles.manage',
|
||||
'platform.permissions.view',
|
||||
'platform.permissions.manage',
|
||||
'platform.servers.view',
|
||||
'platform.servers.manage',
|
||||
'platform.accounts.view',
|
||||
'platform.accounts.manage',
|
||||
'platform.logs.view',
|
||||
'platform.logs.manage',
|
||||
'resource.servers.use',
|
||||
];
|
||||
|
||||
foreach ($permissions as $permissionName) {
|
||||
Permission::query()->firstOrCreate([
|
||||
'name' => $permissionName,
|
||||
'guard_name' => 'api',
|
||||
]);
|
||||
}
|
||||
|
||||
$adminRole = Role::query()->firstOrCreate([
|
||||
'name' => 'admin',
|
||||
'guard_name' => 'api',
|
||||
]);
|
||||
|
||||
$userRole = Role::query()->firstOrCreate([
|
||||
'name' => 'user',
|
||||
'guard_name' => 'api',
|
||||
]);
|
||||
|
||||
$adminRole->syncPermissions($permissions);
|
||||
$userRole->syncPermissions([
|
||||
'resource.servers.use',
|
||||
]);
|
||||
|
||||
Role::query()->where('guard_name', 'api')->whereIn('name', ['operator', 'member'])->delete();
|
||||
|
||||
$this->info('RBAC initialized.');
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
|
||||
private function createRole(): int
|
||||
{
|
||||
$roleName = (string) $this->option('role');
|
||||
|
||||
if ($roleName === '') {
|
||||
$this->error('role is required.');
|
||||
|
||||
return self::FAILURE;
|
||||
}
|
||||
|
||||
Role::query()->firstOrCreate([
|
||||
'name' => $roleName,
|
||||
'guard_name' => 'api',
|
||||
]);
|
||||
|
||||
$this->info('Role created.');
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
|
||||
private function assignRole(): int
|
||||
{
|
||||
$user = $this->findUserByEmail();
|
||||
if (! $user) {
|
||||
return self::FAILURE;
|
||||
}
|
||||
|
||||
$roleName = (string) $this->option('role');
|
||||
|
||||
if ($roleName === '') {
|
||||
$this->error('role is required.');
|
||||
|
||||
return self::FAILURE;
|
||||
}
|
||||
|
||||
$user->assignRole($roleName);
|
||||
$this->info('Role assigned.');
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
|
||||
private function removeRole(): int
|
||||
{
|
||||
$user = $this->findUserByEmail();
|
||||
if (! $user) {
|
||||
return self::FAILURE;
|
||||
}
|
||||
|
||||
$roleName = (string) $this->option('role');
|
||||
|
||||
if ($roleName === '') {
|
||||
$this->error('role is required.');
|
||||
|
||||
return self::FAILURE;
|
||||
}
|
||||
|
||||
$user->removeRole($roleName);
|
||||
$this->info('Role removed.');
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
|
||||
private function assignPermission(): int
|
||||
{
|
||||
$user = $this->findUserByEmail();
|
||||
if (! $user) {
|
||||
return self::FAILURE;
|
||||
}
|
||||
|
||||
$permission = (string) $this->option('permission');
|
||||
|
||||
if ($permission === '') {
|
||||
$this->error('permission is required.');
|
||||
|
||||
return self::FAILURE;
|
||||
}
|
||||
|
||||
$user->givePermissionTo($permission);
|
||||
$this->info('Permission assigned.');
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
|
||||
private function removePermission(): int
|
||||
{
|
||||
$user = $this->findUserByEmail();
|
||||
if (! $user) {
|
||||
return self::FAILURE;
|
||||
}
|
||||
|
||||
$permission = (string) $this->option('permission');
|
||||
|
||||
if ($permission === '') {
|
||||
$this->error('permission is required.');
|
||||
|
||||
return self::FAILURE;
|
||||
}
|
||||
|
||||
$user->revokePermissionTo($permission);
|
||||
$this->info('Permission removed.');
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
|
||||
private function grantServerPermission(): int
|
||||
{
|
||||
$user = $this->findUserByEmail();
|
||||
if (! $user) {
|
||||
return self::FAILURE;
|
||||
}
|
||||
|
||||
$serverId = (int) $this->option('server-id');
|
||||
|
||||
if ($serverId <= 0) {
|
||||
$this->error('server-id is required and must be > 0.');
|
||||
|
||||
return self::FAILURE;
|
||||
}
|
||||
|
||||
UserServerPermission::query()->updateOrCreate(
|
||||
['user_id' => $user->id, 'server_resource_id' => $serverId],
|
||||
[
|
||||
'can_ssh' => (bool) ((int) $this->option('ssh')),
|
||||
'can_sftp' => (bool) ((int) $this->option('sftp')),
|
||||
'can_rdp' => (bool) ((int) $this->option('rdp')),
|
||||
]
|
||||
);
|
||||
|
||||
$this->info('Server permission updated.');
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
|
||||
private function setAdmin(): int
|
||||
{
|
||||
$user = $this->findUserByEmail();
|
||||
if (! $user) {
|
||||
return self::FAILURE;
|
||||
}
|
||||
|
||||
$adminRole = Role::query()->firstOrCreate([
|
||||
'name' => 'admin',
|
||||
'guard_name' => 'api',
|
||||
]);
|
||||
|
||||
$allPermissions = Permission::query()
|
||||
->where('guard_name', 'api')
|
||||
->pluck('name')
|
||||
->all();
|
||||
|
||||
$adminRole->syncPermissions($allPermissions);
|
||||
|
||||
$user->syncRoles([$adminRole->name]);
|
||||
$user->syncPermissions([]);
|
||||
|
||||
$this->info('User set as admin with all permissions.');
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
|
||||
private function unsetAdmin(): int
|
||||
{
|
||||
$user = $this->findUserByEmail();
|
||||
if (! $user) {
|
||||
return self::FAILURE;
|
||||
}
|
||||
|
||||
if (! $user->hasRole('admin', 'api')) {
|
||||
$this->warn('User is not admin.');
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
|
||||
$user->removeRole('admin');
|
||||
|
||||
$this->info('Admin role removed from user.');
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
|
||||
private function findUserByEmail(): ?User
|
||||
{
|
||||
$email = (string) $this->option('email');
|
||||
|
||||
if ($email === '') {
|
||||
$this->error('email is required.');
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
$user = User::query()->where('email', $email)->first();
|
||||
|
||||
if (! $user) {
|
||||
$this->error('User not found.');
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
return $user;
|
||||
}
|
||||
|
||||
private function invalidAction(): int
|
||||
{
|
||||
$this->error('Invalid action.');
|
||||
|
||||
return self::FAILURE;
|
||||
}
|
||||
}
|
||||
136
app/Http/Controllers/Api/AccessLogController.php
Normal file
136
app/Http/Controllers/Api/AccessLogController.php
Normal file
@ -0,0 +1,136 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\LogQueryRequest;
|
||||
use App\Http\Requests\StoreAccessLogRequest;
|
||||
use App\Models\AccessLog;
|
||||
use App\Models\ServerResource;
|
||||
use App\Models\User;
|
||||
use hg\apidoc\annotation as Apidoc;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
|
||||
#[Apidoc\Title('访问日志管理')]
|
||||
class AccessLogController extends Controller
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
$this->middleware('auth:api');
|
||||
$this->middleware('permission:platform.logs.view,api')->only(['index']);
|
||||
$this->middleware('permission:platform.logs.manage,api')->only(['store']);
|
||||
}
|
||||
|
||||
#[Apidoc\Title('日志查询'), Apidoc\Method('GET'), Apidoc\Url('/logs')]
|
||||
public function index(LogQueryRequest $request): JsonResponse
|
||||
{
|
||||
$sortBy = $request->string('sort_by')->toString() ?: 'requested_at';
|
||||
$sortOrder = $request->string('sort_order')->toString() ?: 'desc';
|
||||
|
||||
$query = AccessLog::query()->with(['user', 'serverResource', 'bastionAccount']);
|
||||
|
||||
if ($request->filled('from')) {
|
||||
$query->where('requested_at', '>=', $request->date('from'));
|
||||
}
|
||||
|
||||
if ($request->filled('to')) {
|
||||
$query->where('requested_at', '<=', $request->date('to'));
|
||||
}
|
||||
|
||||
if ($request->filled('action')) {
|
||||
$query->where('action', 'like', '%'.$request->string('action')->toString().'%');
|
||||
}
|
||||
if ($request->filled('actions')) {
|
||||
$query->whereIn('action', $request->array('actions'));
|
||||
}
|
||||
|
||||
if ($request->filled('user_id')) {
|
||||
$query->where('user_id', $request->integer('user_id'));
|
||||
}
|
||||
if ($request->filled('user_ids')) {
|
||||
$query->whereIn('user_id', $request->array('user_ids'));
|
||||
}
|
||||
|
||||
if ($request->filled('server_resource_id')) {
|
||||
$query->where('server_resource_id', $request->integer('server_resource_id'));
|
||||
}
|
||||
if ($request->filled('server_resource_ids')) {
|
||||
$query->whereIn('server_resource_id', $request->array('server_resource_ids'));
|
||||
}
|
||||
|
||||
if ($request->filled('protocol')) {
|
||||
$query->where('protocol', $request->string('protocol')->toString());
|
||||
}
|
||||
|
||||
if ($sortBy === 'user') {
|
||||
$query->leftJoin('users', 'users.id', '=', 'access_logs.user_id')
|
||||
->select('access_logs.*')
|
||||
->orderBy('users.nickname', $sortOrder);
|
||||
} elseif ($sortBy === 'resource') {
|
||||
$query->leftJoin('server_resources', 'server_resources.id', '=', 'access_logs.server_resource_id')
|
||||
->select('access_logs.*')
|
||||
->orderBy('server_resources.name', $sortOrder);
|
||||
} elseif ($sortBy === 'account') {
|
||||
$query->leftJoin('bastion_accounts', 'bastion_accounts.id', '=', 'access_logs.bastion_account_id')
|
||||
->select('access_logs.*')
|
||||
->orderBy('bastion_accounts.name', $sortOrder);
|
||||
} else {
|
||||
$column = in_array($sortBy, ['id', 'requested_at', 'action', 'protocol'], true) ? $sortBy : 'requested_at';
|
||||
$query->orderBy('access_logs.'.$column, $sortOrder);
|
||||
}
|
||||
|
||||
$logs = $query->paginate($request->integer('per_page', 20));
|
||||
$userIds = AccessLog::query()
|
||||
->whereNotNull('user_id')
|
||||
->distinct()
|
||||
->pluck('user_id')
|
||||
->map(fn (int $id): int => (int) $id)
|
||||
->all();
|
||||
$serverResourceIds = AccessLog::query()
|
||||
->whereNotNull('server_resource_id')
|
||||
->distinct()
|
||||
->pluck('server_resource_id')
|
||||
->map(fn (int $id): int => (int) $id)
|
||||
->all();
|
||||
|
||||
return response()->json([
|
||||
'code' => 0,
|
||||
'message' => 'ok',
|
||||
'data' => $logs,
|
||||
'filter_options' => [
|
||||
'actions' => AccessLog::query()
|
||||
->whereNotNull('action')
|
||||
->where('action', '!=', '')
|
||||
->distinct()
|
||||
->orderBy('action')
|
||||
->pluck('action')
|
||||
->values(),
|
||||
'users' => User::query()
|
||||
->select(['id', 'nickname', 'email'])
|
||||
->whereIn('id', $userIds)
|
||||
->orderBy('id')
|
||||
->get(),
|
||||
'server_resources' => ServerResource::query()
|
||||
->select(['id', 'name', 'internal_ip'])
|
||||
->whereIn('id', $serverResourceIds)
|
||||
->orderBy('id')
|
||||
->get(),
|
||||
'protocols' => AccessLog::query()
|
||||
->whereNotNull('protocol')
|
||||
->where('protocol', '!=', '')
|
||||
->distinct()
|
||||
->orderBy('protocol')
|
||||
->pluck('protocol')
|
||||
->values(),
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
#[Apidoc\Title('新增日志'), Apidoc\Method('POST'), Apidoc\Url('/logs')]
|
||||
public function store(StoreAccessLogRequest $request): JsonResponse
|
||||
{
|
||||
$log = AccessLog::query()->create($request->validated());
|
||||
|
||||
return response()->json(['code' => 0, 'message' => 'ok', 'data' => $log], 201);
|
||||
}
|
||||
}
|
||||
121
app/Http/Controllers/Api/AuthController.php
Normal file
121
app/Http/Controllers/Api/AuthController.php
Normal file
@ -0,0 +1,121 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\User;
|
||||
use hg\apidoc\annotation as Apidoc;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Validation\Rules\Password;
|
||||
|
||||
#[Apidoc\Title('认证模块')]
|
||||
class AuthController extends Controller
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
$this->middleware('auth:api')->except('login');
|
||||
}
|
||||
|
||||
#[Apidoc\Title('登录'), Apidoc\Method('POST'), Apidoc\Url('/auth/login')]
|
||||
public function login(Request $request): JsonResponse
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'email' => ['nullable', 'email', 'required_without:phone'],
|
||||
'phone' => ['nullable', 'string', 'max:32', 'required_without:email'],
|
||||
'password' => ['required', 'string'],
|
||||
]);
|
||||
$credentials = ['password' => (string) $validated['password']];
|
||||
$accountType = '';
|
||||
|
||||
if (! empty($validated['email'])) {
|
||||
$credentials['email'] = (string) $validated['email'];
|
||||
$accountType = 'email';
|
||||
}
|
||||
|
||||
if (! empty($validated['phone'])) {
|
||||
$credentials['phone'] = (string) $validated['phone'];
|
||||
$accountType = 'phone';
|
||||
}
|
||||
|
||||
$token = Auth::guard('api')->attempt($credentials);
|
||||
|
||||
if (! $token) {
|
||||
return response()->json(['code' => 401, 'message' => '账号或密码错误', 'data' => null], 401);
|
||||
}
|
||||
|
||||
$this->auditLog($request, 'login', ['metadata' => ['account_type' => $accountType]]);
|
||||
|
||||
return response()->json([
|
||||
'code' => 0,
|
||||
'message' => 'ok',
|
||||
'data' => [
|
||||
'token' => $token,
|
||||
'type' => 'bearer',
|
||||
'expires_in' => Auth::guard('api')->factory()->getTTL() * 60,
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
#[Apidoc\Title('当前用户信息'), Apidoc\Method('GET'), Apidoc\Url('/auth/me')]
|
||||
public function me(): JsonResponse
|
||||
{
|
||||
/** @var User $user */
|
||||
$user = Auth::guard('api')->user();
|
||||
|
||||
return response()->json([
|
||||
'code' => 0,
|
||||
'message' => 'ok',
|
||||
'data' => [
|
||||
'user' => $user?->load('roles'),
|
||||
'permissions' => $user?->getAllPermissions()->pluck('name')->values(),
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
#[Apidoc\Title('更新个人信息'), Apidoc\Method('PUT'), Apidoc\Url('/auth/profile')]
|
||||
public function updateProfile(Request $request): JsonResponse
|
||||
{
|
||||
/** @var User $user */
|
||||
$user = Auth::guard('api')->user();
|
||||
|
||||
$validated = $request->validate([
|
||||
'nickname' => ['sometimes', 'required', 'string', 'max:255'],
|
||||
'email' => ['sometimes', 'required', 'email', 'max:255', 'unique:users,email,'.$user->id],
|
||||
'phone' => ['nullable', 'string', 'max:32', 'unique:users,phone,'.$user->id],
|
||||
]);
|
||||
|
||||
$user->update($validated);
|
||||
$this->auditLog($request, 'profile_update');
|
||||
|
||||
return response()->json(['code' => 0, 'message' => 'ok', 'data' => $user->fresh(['roles'])]);
|
||||
}
|
||||
|
||||
#[Apidoc\Title('修改密码'), Apidoc\Method('PUT'), Apidoc\Url('/auth/password')]
|
||||
public function updatePassword(Request $request): JsonResponse
|
||||
{
|
||||
/** @var User $user */
|
||||
$user = Auth::guard('api')->user();
|
||||
|
||||
$validated = $request->validate([
|
||||
'current_password' => ['required', 'current_password:api'],
|
||||
'password' => ['required', 'confirmed', Password::min(6)],
|
||||
]);
|
||||
|
||||
$user->password = $validated['password'];
|
||||
$user->save();
|
||||
$this->auditLog($request, 'password_update');
|
||||
|
||||
return response()->json(['code' => 0, 'message' => 'ok', 'data' => null]);
|
||||
}
|
||||
|
||||
#[Apidoc\Title('登出'), Apidoc\Method('POST'), Apidoc\Url('/auth/logout')]
|
||||
public function logout(Request $request): JsonResponse
|
||||
{
|
||||
$this->auditLog($request, 'logout');
|
||||
Auth::guard('api')->logout();
|
||||
|
||||
return response()->json(['code' => 0, 'message' => 'ok', 'data' => null]);
|
||||
}
|
||||
}
|
||||
245
app/Http/Controllers/Api/BastionAccountController.php
Normal file
245
app/Http/Controllers/Api/BastionAccountController.php
Normal file
@ -0,0 +1,245 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\StoreBastionAccountRequest;
|
||||
use App\Http\Requests\UpdateBastionAccountRequest;
|
||||
use App\Models\BastionAccount;
|
||||
use hg\apidoc\annotation as Apidoc;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Client\ConnectionException;
|
||||
use Illuminate\Http\Client\RequestException;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
#[Apidoc\Title('堡垒机授权账号管理')]
|
||||
class BastionAccountController extends Controller
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
$this->middleware('auth:api');
|
||||
$this->middleware('permission:platform.accounts.view,api')->only(['index', 'show']);
|
||||
$this->middleware('permission:platform.accounts.manage,api')->only(['store', 'update', 'destroy', 'refreshToken', 'refreshTokenStatus']);
|
||||
}
|
||||
|
||||
#[Apidoc\Title('账号列表'), Apidoc\Method('GET'), Apidoc\Url('/accounts')]
|
||||
public function index(): JsonResponse
|
||||
{
|
||||
return response()->json(['code' => 0, 'message' => 'ok', 'data' => BastionAccount::query()->latest()->paginate(20)]);
|
||||
}
|
||||
|
||||
#[Apidoc\Title('创建账号'), Apidoc\Method('POST'), Apidoc\Url('/accounts')]
|
||||
public function store(StoreBastionAccountRequest $request): JsonResponse
|
||||
{
|
||||
$account = BastionAccount::query()->create($request->validated());
|
||||
$this->auditLog($request, 'account_create', ['bastion_account_id' => $account->id]);
|
||||
|
||||
return response()->json(['code' => 0, 'message' => 'ok', 'data' => $account], 201);
|
||||
}
|
||||
|
||||
#[Apidoc\Title('账号详情'), Apidoc\Method('GET'), Apidoc\Url('/accounts/{id}')]
|
||||
public function show(int $id): JsonResponse
|
||||
{
|
||||
return response()->json(['code' => 0, 'message' => 'ok', 'data' => BastionAccount::query()->findOrFail($id)]);
|
||||
}
|
||||
|
||||
#[Apidoc\Title('更新账号'), Apidoc\Method('PUT'), Apidoc\Url('/accounts/{id}')]
|
||||
public function update(UpdateBastionAccountRequest $request, int $id): JsonResponse
|
||||
{
|
||||
$account = BastionAccount::query()->findOrFail($id);
|
||||
$account->update($request->validated());
|
||||
$this->auditLog($request, 'account_update', ['bastion_account_id' => $account->id]);
|
||||
|
||||
return response()->json(['code' => 0, 'message' => 'ok', 'data' => $account]);
|
||||
}
|
||||
|
||||
#[Apidoc\Title('删除账号'), Apidoc\Method('DELETE'), Apidoc\Url('/accounts/{id}')]
|
||||
public function destroy(Request $request, int $id): JsonResponse
|
||||
{
|
||||
$account = BastionAccount::query()->findOrFail($id);
|
||||
$this->auditLog($request, 'account_delete', ['bastion_account_id' => $account->id]);
|
||||
$account->delete();
|
||||
|
||||
return response()->json(['code' => 0, 'message' => 'ok', 'data' => null]);
|
||||
}
|
||||
|
||||
#[Apidoc\Title('刷新账号token'), Apidoc\Method('POST'), Apidoc\Url('/accounts/{id}/refresh-token')]
|
||||
public function refreshToken(Request $request, int $id): JsonResponse
|
||||
{
|
||||
$account = BastionAccount::query()->findOrFail($id);
|
||||
$baseUrl = (string) config('services.bastion_token.base_url');
|
||||
|
||||
if ($baseUrl === '') {
|
||||
return response()->json([
|
||||
'code' => 500,
|
||||
'message' => '未配置堡垒机 Token 服务地址,请先在 .env 中配置 BASTION_TOKEN_API_BASE_URL',
|
||||
'data' => null,
|
||||
], 500);
|
||||
}
|
||||
|
||||
$submitEndpoint = (string) config('services.bastion_token.submit_endpoint', '/bastion_token');
|
||||
$timeout = (int) config('services.bastion_token.timeout', 30);
|
||||
$taskTtlSeconds = (int) config('services.bastion_token.task_ttl_seconds', 1800);
|
||||
|
||||
try {
|
||||
$submitResponse = Http::baseUrl($baseUrl)
|
||||
->acceptJson()
|
||||
->timeout($timeout)
|
||||
->retry(2, 300, throw: false)
|
||||
->post($submitEndpoint, [
|
||||
'username' => $account->username,
|
||||
'password' => $account->password,
|
||||
]);
|
||||
|
||||
if (! $submitResponse->successful()) {
|
||||
return response()->json([
|
||||
'code' => 502,
|
||||
'message' => '提交 Token 刷新任务失败',
|
||||
'data' => ['response' => $submitResponse->json()],
|
||||
], 502);
|
||||
}
|
||||
|
||||
$taskId = (string) data_get($submitResponse->json(), 'task_id', '');
|
||||
|
||||
if ($taskId === '') {
|
||||
return response()->json([
|
||||
'code' => 502,
|
||||
'message' => 'Token 服务返回任务ID为空',
|
||||
'data' => ['response' => $submitResponse->json()],
|
||||
], 502);
|
||||
}
|
||||
} catch (ConnectionException|RequestException $exception) {
|
||||
return response()->json([
|
||||
'code' => 502,
|
||||
'message' => '调用 Token 服务失败:'.$exception->getMessage(),
|
||||
'data' => null,
|
||||
], 502);
|
||||
}
|
||||
|
||||
$cacheKey = $this->tokenTaskCacheKey($taskId);
|
||||
Cache::put($cacheKey, [
|
||||
'task_id' => $taskId,
|
||||
'account_id' => $account->id,
|
||||
'finished' => false,
|
||||
], now()->addSeconds(max(60, $taskTtlSeconds)));
|
||||
|
||||
$this->auditLog($request, 'account_refresh_token_submit', ['bastion_account_id' => $account->id, 'task_id' => $taskId]);
|
||||
|
||||
return response()->json([
|
||||
'code' => 0,
|
||||
'message' => 'Token 刷新任务已提交',
|
||||
'data' => [
|
||||
'task_id' => $taskId,
|
||||
'status' => 'pending',
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
#[Apidoc\Title('查询刷新账号token状态'), Apidoc\Method('GET'), Apidoc\Url('/accounts/{id}/refresh-token/{taskId}')]
|
||||
public function refreshTokenStatus(Request $request, int $id, string $taskId): JsonResponse
|
||||
{
|
||||
$account = BastionAccount::query()->findOrFail($id);
|
||||
$baseUrl = (string) config('services.bastion_token.base_url');
|
||||
|
||||
if ($baseUrl === '') {
|
||||
return response()->json([
|
||||
'code' => 500,
|
||||
'message' => '未配置堡垒机 Token 服务地址,请先在 .env 中配置 BASTION_TOKEN_API_BASE_URL',
|
||||
'data' => null,
|
||||
], 500);
|
||||
}
|
||||
|
||||
$cacheKey = $this->tokenTaskCacheKey($taskId);
|
||||
$taskMeta = Cache::get($cacheKey);
|
||||
|
||||
if (! is_array($taskMeta) || (int) ($taskMeta['account_id'] ?? 0) !== $account->id) {
|
||||
return response()->json([
|
||||
'code' => 404,
|
||||
'message' => '任务不存在或已过期',
|
||||
'data' => ['task_id' => $taskId],
|
||||
], 404);
|
||||
}
|
||||
|
||||
$statusEndpoint = (string) config('services.bastion_token.status_endpoint', '/bastion_token/{task_id}');
|
||||
$timeout = (int) config('services.bastion_token.timeout', 30);
|
||||
$statusUrl = str_replace('{task_id}', $taskId, $statusEndpoint);
|
||||
|
||||
try {
|
||||
$statusResponse = Http::baseUrl($baseUrl)
|
||||
->acceptJson()
|
||||
->timeout($timeout)
|
||||
->retry(2, 300, throw: false)
|
||||
->get($statusUrl);
|
||||
} catch (ConnectionException|RequestException $exception) {
|
||||
return response()->json([
|
||||
'code' => 502,
|
||||
'message' => '查询 Token 任务状态失败:'.$exception->getMessage(),
|
||||
'data' => ['task_id' => $taskId],
|
||||
], 502);
|
||||
}
|
||||
|
||||
if (! $statusResponse->successful()) {
|
||||
return response()->json([
|
||||
'code' => 502,
|
||||
'message' => '查询 Token 任务状态失败',
|
||||
'data' => ['task_id' => $taskId, 'response' => $statusResponse->json()],
|
||||
], 502);
|
||||
}
|
||||
|
||||
$taskResult = $statusResponse->json();
|
||||
$status = Str::lower((string) data_get($taskResult, 'status', 'pending'));
|
||||
|
||||
if ($status !== 'success') {
|
||||
if ($status === 'error') {
|
||||
return response()->json([
|
||||
'code' => 502,
|
||||
'message' => 'Token 刷新失败:'.((string) data_get($taskResult, 'message', '未知错误')),
|
||||
'data' => ['task_id' => $taskId, 'status' => $status, 'result' => $taskResult],
|
||||
], 502);
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
'code' => 0,
|
||||
'message' => 'Token 刷新任务执行中',
|
||||
'data' => ['task_id' => $taskId, 'status' => 'pending'],
|
||||
]);
|
||||
}
|
||||
|
||||
if (! (bool) ($taskMeta['finished'] ?? false)) {
|
||||
$usmAuthentication = (string) data_get($taskResult, 'data.USM-AUTHENTICATION', '');
|
||||
$usm = (string) data_get($taskResult, 'data.USM', '');
|
||||
|
||||
if ($usmAuthentication === '' || $usm === '') {
|
||||
return response()->json([
|
||||
'code' => 502,
|
||||
'message' => 'Token 服务返回数据缺失',
|
||||
'data' => ['task_id' => $taskId, 'result' => $taskResult],
|
||||
], 502);
|
||||
}
|
||||
|
||||
$account->update([
|
||||
'usm_authentication' => $usmAuthentication,
|
||||
'usm' => $usm,
|
||||
'last_token_refreshed_at' => now(),
|
||||
]);
|
||||
|
||||
$taskMeta['finished'] = true;
|
||||
Cache::put($cacheKey, $taskMeta, now()->addMinutes(10));
|
||||
$this->auditLog($request, 'account_refresh_token', ['bastion_account_id' => $account->id, 'task_id' => $taskId]);
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
'code' => 0,
|
||||
'message' => 'Token 刷新成功',
|
||||
'data' => ['task_id' => $taskId, 'status' => 'success', 'account' => $account->fresh()],
|
||||
]);
|
||||
}
|
||||
|
||||
private function tokenTaskCacheKey(string $taskId): string
|
||||
{
|
||||
return 'bastion_token_task:'.$taskId;
|
||||
}
|
||||
}
|
||||
10
app/Http/Controllers/Api/Definitions.php
Normal file
10
app/Http/Controllers/Api/Definitions.php
Normal file
@ -0,0 +1,10 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api;
|
||||
|
||||
use hg\apidoc\annotation as Apidoc;
|
||||
|
||||
#[Apidoc\Title('通用定义')]
|
||||
class Definitions
|
||||
{
|
||||
}
|
||||
311
app/Http/Controllers/Api/OpsClientController.php
Normal file
311
app/Http/Controllers/Api/OpsClientController.php
Normal file
@ -0,0 +1,311 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\AccessLog;
|
||||
use App\Models\OpsProtocol;
|
||||
use App\Models\OpsSoftware;
|
||||
use App\Models\User;
|
||||
use App\Models\UserOpsSoftwarePreference;
|
||||
use hg\apidoc\annotation as Apidoc;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
|
||||
#[Apidoc\Title('运维协议与软件管理')]
|
||||
class OpsClientController extends Controller
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
$this->middleware('auth:api');
|
||||
$this->middleware('permission:platform.servers.manage,api')->only([
|
||||
'storeProtocol',
|
||||
'updateProtocol',
|
||||
'destroyProtocol',
|
||||
'storeSoftware',
|
||||
'updateSoftware',
|
||||
'destroySoftware',
|
||||
]);
|
||||
}
|
||||
|
||||
#[Apidoc\Title('运维协议与软件列表'), Apidoc\Method('GET'), Apidoc\Url('/ops-clients/meta')]
|
||||
public function meta(Request $request): JsonResponse
|
||||
{
|
||||
/** @var User|null $user */
|
||||
$user = auth('api')->user();
|
||||
$isManager = (bool) ($user?->can('platform.servers.manage'));
|
||||
|
||||
$protocolQuery = OpsProtocol::query()->orderBy('sort')->orderBy('id');
|
||||
if (! $isManager) {
|
||||
$protocolQuery->where('is_active', true);
|
||||
}
|
||||
|
||||
$protocols = $protocolQuery->with(['softwares' => function ($query) use ($isManager) {
|
||||
if (! $isManager) {
|
||||
$query->where('is_active', true);
|
||||
}
|
||||
}])->get();
|
||||
|
||||
$preferences = UserOpsSoftwarePreference::query()
|
||||
->where('user_id', $user?->id)
|
||||
->get()
|
||||
->mapWithKeys(fn (UserOpsSoftwarePreference $item): array => [
|
||||
(string) $item->ops_protocol_id => $item->ops_software_id,
|
||||
]);
|
||||
|
||||
return response()->json([
|
||||
'code' => 0,
|
||||
'message' => 'ok',
|
||||
'data' => [
|
||||
'protocols' => $protocols,
|
||||
'preferences' => $preferences,
|
||||
'is_manager' => $isManager,
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
#[Apidoc\Title('新增运维协议'), Apidoc\Method('POST'), Apidoc\Url('/ops-clients/protocols')]
|
||||
public function storeProtocol(Request $request): JsonResponse
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'name' => ['required', 'string', 'max:64', 'unique:ops_protocols,name'],
|
||||
'bastion_protocol_id' => ['required', 'integer', 'min:1'],
|
||||
'description' => ['nullable', 'string', 'max:255'],
|
||||
'sort' => ['sometimes', 'integer', 'min:0'],
|
||||
'is_active' => ['sometimes', 'boolean'],
|
||||
]);
|
||||
|
||||
$protocol = OpsProtocol::query()->create($validated);
|
||||
$this->auditLog($request, 'ops_protocol_create', ['metadata' => ['ops_protocol_id' => $protocol->id]]);
|
||||
|
||||
return response()->json(['code' => 0, 'message' => 'ok', 'data' => $protocol], 201);
|
||||
}
|
||||
|
||||
#[Apidoc\Title('修改运维协议'), Apidoc\Method('PUT'), Apidoc\Url('/ops-clients/protocols/{id}')]
|
||||
public function updateProtocol(Request $request, int $id): JsonResponse
|
||||
{
|
||||
$protocol = OpsProtocol::query()->findOrFail($id);
|
||||
$validated = $request->validate([
|
||||
'name' => ['required', 'string', 'max:64', 'unique:ops_protocols,name,'.$protocol->id],
|
||||
'bastion_protocol_id' => ['required', 'integer', 'min:1'],
|
||||
'description' => ['nullable', 'string', 'max:255'],
|
||||
'sort' => ['sometimes', 'integer', 'min:0'],
|
||||
'is_active' => ['sometimes', 'boolean'],
|
||||
]);
|
||||
|
||||
$protocol->update($validated);
|
||||
$this->auditLog($request, 'ops_protocol_update', ['metadata' => ['ops_protocol_id' => $protocol->id]]);
|
||||
|
||||
return response()->json(['code' => 0, 'message' => 'ok', 'data' => $protocol]);
|
||||
}
|
||||
|
||||
#[Apidoc\Title('删除运维协议'), Apidoc\Method('DELETE'), Apidoc\Url('/ops-clients/protocols/{id}')]
|
||||
public function destroyProtocol(Request $request, int $id): JsonResponse
|
||||
{
|
||||
$protocol = OpsProtocol::query()->findOrFail($id);
|
||||
$this->auditLog($request, 'ops_protocol_delete', ['metadata' => ['ops_protocol_id' => $protocol->id]]);
|
||||
$protocol->delete();
|
||||
|
||||
return response()->json(['code' => 0, 'message' => 'ok', 'data' => null]);
|
||||
}
|
||||
|
||||
#[Apidoc\Title('新增运维软件'), Apidoc\Method('POST'), Apidoc\Url('/ops-clients/protocols/{id}/softwares')]
|
||||
public function storeSoftware(Request $request, int $id): JsonResponse
|
||||
{
|
||||
OpsProtocol::query()->findOrFail($id);
|
||||
$validated = $request->validate([
|
||||
'name' => ['required', 'string', 'max:100'],
|
||||
'client_path' => ['nullable', 'string', 'max:255'],
|
||||
'sort' => ['sometimes', 'integer', 'min:0'],
|
||||
'is_active' => ['sometimes', 'boolean'],
|
||||
]);
|
||||
|
||||
$exists = OpsSoftware::query()
|
||||
->where('ops_protocol_id', $id)
|
||||
->where('name', $validated['name'])
|
||||
->exists();
|
||||
if ($exists) {
|
||||
throw ValidationException::withMessages([
|
||||
'name' => ['同一协议下软件名称不能重复。'],
|
||||
]);
|
||||
}
|
||||
|
||||
$software = OpsSoftware::query()->create([
|
||||
...$validated,
|
||||
'ops_protocol_id' => $id,
|
||||
]);
|
||||
$this->auditLog($request, 'ops_software_create', ['metadata' => ['ops_software_id' => $software->id, 'ops_protocol_id' => $id]]);
|
||||
|
||||
return response()->json(['code' => 0, 'message' => 'ok', 'data' => $software], 201);
|
||||
}
|
||||
|
||||
#[Apidoc\Title('修改运维软件'), Apidoc\Method('PUT'), Apidoc\Url('/ops-clients/softwares/{id}')]
|
||||
public function updateSoftware(Request $request, int $id): JsonResponse
|
||||
{
|
||||
$software = OpsSoftware::query()->findOrFail($id);
|
||||
$validated = $request->validate([
|
||||
'name' => ['required', 'string', 'max:100'],
|
||||
'client_path' => ['nullable', 'string', 'max:255'],
|
||||
'sort' => ['sometimes', 'integer', 'min:0'],
|
||||
'is_active' => ['sometimes', 'boolean'],
|
||||
]);
|
||||
|
||||
$exists = OpsSoftware::query()
|
||||
->where('ops_protocol_id', $software->ops_protocol_id)
|
||||
->where('name', $validated['name'])
|
||||
->where('id', '!=', $software->id)
|
||||
->exists();
|
||||
if ($exists) {
|
||||
throw ValidationException::withMessages([
|
||||
'name' => ['同一协议下软件名称不能重复。'],
|
||||
]);
|
||||
}
|
||||
|
||||
$software->update($validated);
|
||||
$this->auditLog($request, 'ops_software_update', ['metadata' => ['ops_software_id' => $software->id]]);
|
||||
|
||||
return response()->json(['code' => 0, 'message' => 'ok', 'data' => $software]);
|
||||
}
|
||||
|
||||
#[Apidoc\Title('删除运维软件'), Apidoc\Method('DELETE'), Apidoc\Url('/ops-clients/softwares/{id}')]
|
||||
public function destroySoftware(Request $request, int $id): JsonResponse
|
||||
{
|
||||
$software = OpsSoftware::query()->findOrFail($id);
|
||||
$this->auditLog($request, 'ops_software_delete', ['metadata' => ['ops_software_id' => $software->id]]);
|
||||
$software->delete();
|
||||
|
||||
return response()->json(['code' => 0, 'message' => 'ok', 'data' => null]);
|
||||
}
|
||||
|
||||
#[Apidoc\Title('保存我的运维软件偏好'), Apidoc\Method('PUT'), Apidoc\Url('/ops-clients/preferences')]
|
||||
public function savePreferences(Request $request): JsonResponse
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'items' => ['required', 'array'],
|
||||
'items.*.protocol_id' => ['required', 'integer', 'exists:ops_protocols,id'],
|
||||
'items.*.software_id' => ['nullable', 'integer', 'exists:ops_softwares,id'],
|
||||
]);
|
||||
|
||||
/** @var User $user */
|
||||
$user = auth('api')->user();
|
||||
|
||||
foreach ($validated['items'] as $item) {
|
||||
$protocolId = (int) $item['protocol_id'];
|
||||
$softwareId = isset($item['software_id']) ? (int) $item['software_id'] : null;
|
||||
|
||||
if ($softwareId !== null) {
|
||||
$software = OpsSoftware::query()->findOrFail($softwareId);
|
||||
if ((int) $software->ops_protocol_id !== $protocolId) {
|
||||
throw ValidationException::withMessages([
|
||||
'items' => ['软件与协议不匹配。'],
|
||||
]);
|
||||
}
|
||||
|
||||
UserOpsSoftwarePreference::query()->updateOrCreate(
|
||||
['user_id' => $user->id, 'ops_protocol_id' => $protocolId],
|
||||
['ops_software_id' => $softwareId]
|
||||
);
|
||||
} else {
|
||||
UserOpsSoftwarePreference::query()
|
||||
->where('user_id', $user->id)
|
||||
->where('ops_protocol_id', $protocolId)
|
||||
->delete();
|
||||
}
|
||||
}
|
||||
|
||||
$this->auditLog($request, 'ops_preference_update', ['metadata' => ['user_id' => $user->id]]);
|
||||
|
||||
return response()->json(['code' => 0, 'message' => 'ok', 'data' => null]);
|
||||
}
|
||||
|
||||
#[Apidoc\Title('生成运维连接'), Apidoc\Method('POST'), Apidoc\Url('/ops-clients/link')]
|
||||
public function generateLink(Request $request): JsonResponse
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'protocol_id' => ['required', 'integer', 'exists:ops_protocols,id'],
|
||||
'software_id' => ['nullable', 'integer', 'exists:ops_softwares,id'],
|
||||
]);
|
||||
|
||||
/** @var User $user */
|
||||
$user = auth('api')->user();
|
||||
$protocol = OpsProtocol::query()->findOrFail((int) $validated['protocol_id']);
|
||||
$softwareId = isset($validated['software_id']) ? (int) $validated['software_id'] : null;
|
||||
|
||||
if ($softwareId === null) {
|
||||
$softwareId = (int) UserOpsSoftwarePreference::query()
|
||||
->where('user_id', $user->id)
|
||||
->where('ops_protocol_id', $protocol->id)
|
||||
->value('ops_software_id');
|
||||
}
|
||||
|
||||
$software = OpsSoftware::query()->find($softwareId ?: 0);
|
||||
if (! $software || (int) $software->ops_protocol_id !== (int) $protocol->id) {
|
||||
throw ValidationException::withMessages([
|
||||
'software_id' => ['请先选择该协议对应的软件。'],
|
||||
]);
|
||||
}
|
||||
|
||||
$payload = [
|
||||
'NODE_COMMON' => [
|
||||
'Mode' => '1',
|
||||
'IsGlobalSetting' => '0',
|
||||
'IPv4' => (string) config('services.ops_client.ipv4', '172.16.1.2'),
|
||||
'AssetIPv4' => (string) config('services.ops_client.asset_ipv4', '0.0.0.0'),
|
||||
'Port' => '',
|
||||
'AssetPort' => '',
|
||||
'Protocol' => (string) $protocol->name,
|
||||
'ClientName' => (string) $software->name,
|
||||
'ClientPath' => (string) ($software->client_path ?? ''),
|
||||
'Username' => '',
|
||||
'SSOToken' => '',
|
||||
'Title' => '',
|
||||
],
|
||||
'NODE_SFTP' => [
|
||||
'NotUsed' => '1',
|
||||
'Char-Set' => 'Utf=1',
|
||||
],
|
||||
];
|
||||
|
||||
$encoded = base64_encode((string) json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES));
|
||||
$link = 'dasusm://'.$encoded;
|
||||
|
||||
UserOpsSoftwarePreference::query()->updateOrCreate(
|
||||
['user_id' => $user->id, 'ops_protocol_id' => $protocol->id],
|
||||
['ops_software_id' => $software->id]
|
||||
);
|
||||
|
||||
AccessLog::query()->create([
|
||||
'user_id' => $user->id,
|
||||
'server_resource_id' => null,
|
||||
'bastion_account_id' => null,
|
||||
'protocol' => (string) $protocol->name,
|
||||
'action' => 'ops_client_link',
|
||||
'requested_at' => now(),
|
||||
'metadata' => [
|
||||
'ops_protocol_id' => $protocol->id,
|
||||
'ops_software_id' => $software->id,
|
||||
'ops_software_name' => $software->name,
|
||||
],
|
||||
]);
|
||||
|
||||
$this->auditLog($request, 'ops_client_link_generate', [
|
||||
'metadata' => [
|
||||
'ops_protocol_id' => $protocol->id,
|
||||
'ops_software_id' => $software->id,
|
||||
],
|
||||
]);
|
||||
|
||||
return response()->json([
|
||||
'code' => 0,
|
||||
'message' => 'ok',
|
||||
'data' => [
|
||||
'link' => $link,
|
||||
'encoded' => $encoded,
|
||||
'payload' => $payload,
|
||||
'protocol' => $protocol,
|
||||
'software' => $software,
|
||||
],
|
||||
]);
|
||||
}
|
||||
}
|
||||
157
app/Http/Controllers/Api/PermissionController.php
Normal file
157
app/Http/Controllers/Api/PermissionController.php
Normal file
@ -0,0 +1,157 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\StorePermissionRequest;
|
||||
use App\Http\Requests\UpdatePermissionRequest;
|
||||
use hg\apidoc\annotation as Apidoc;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Spatie\Permission\Models\Permission;
|
||||
use Spatie\Permission\Models\Role;
|
||||
|
||||
#[Apidoc\Title('权限管理')]
|
||||
class PermissionController extends Controller
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
$this->middleware('auth:api');
|
||||
$this->middleware('permission:platform.permissions.view,api')->only(['index', 'show']);
|
||||
$this->middleware('permission:platform.permissions.manage,api')->only(['store', 'update', 'destroy', 'syncRolePermissions']);
|
||||
}
|
||||
|
||||
#[Apidoc\Title('权限列表'), Apidoc\Method('GET'), Apidoc\Url('/permissions')]
|
||||
public function index(): JsonResponse
|
||||
{
|
||||
$permissions = Permission::query()->latest()->paginate(200);
|
||||
|
||||
$permissions->getCollection()->transform(function (Permission $permission) {
|
||||
return $this->applyDefaultMeta($permission);
|
||||
});
|
||||
|
||||
return response()->json(['code' => 0, 'message' => 'ok', 'data' => $permissions]);
|
||||
}
|
||||
|
||||
#[Apidoc\Title('创建权限'), Apidoc\Method('POST'), Apidoc\Url('/permissions')]
|
||||
public function store(StorePermissionRequest $request): JsonResponse
|
||||
{
|
||||
$defaultMeta = $this->defaultPermissionMeta($request->string('name')->toString());
|
||||
|
||||
$permission = Permission::query()->create([
|
||||
...$request->safe()->except(['guard_name']),
|
||||
'guard_name' => 'api',
|
||||
'category' => $request->string('category', $defaultMeta['category'])->toString(),
|
||||
'description' => $request->string('description', $defaultMeta['description'])->toString(),
|
||||
]);
|
||||
|
||||
$this->auditLog($request, 'permission_create', ['metadata' => ['target_permission_id' => $permission->id]]);
|
||||
|
||||
return response()->json(['code' => 0, 'message' => 'ok', 'data' => $permission], 201);
|
||||
}
|
||||
|
||||
#[Apidoc\Title('权限详情'), Apidoc\Method('GET'), Apidoc\Url('/permissions/{id}')]
|
||||
public function show(int $id): JsonResponse
|
||||
{
|
||||
$permission = Permission::query()->findOrFail($id);
|
||||
|
||||
return response()->json(['code' => 0, 'message' => 'ok', 'data' => $this->applyDefaultMeta($permission)]);
|
||||
}
|
||||
|
||||
#[Apidoc\Title('更新权限'), Apidoc\Method('PUT'), Apidoc\Url('/permissions/{id}')]
|
||||
public function update(UpdatePermissionRequest $request, int $id): JsonResponse
|
||||
{
|
||||
$permission = Permission::query()->findOrFail($id);
|
||||
$permission->update([
|
||||
...$request->safe()->except(['guard_name']),
|
||||
'guard_name' => 'api',
|
||||
]);
|
||||
|
||||
$this->auditLog($request, 'permission_update', ['metadata' => ['target_permission_id' => $permission->id]]);
|
||||
|
||||
return response()->json(['code' => 0, 'message' => 'ok', 'data' => $this->applyDefaultMeta($permission->fresh())]);
|
||||
}
|
||||
|
||||
#[Apidoc\Title('删除权限'), Apidoc\Method('DELETE'), Apidoc\Url('/permissions/{id}')]
|
||||
public function destroy(Request $request, int $id): JsonResponse
|
||||
{
|
||||
$permission = Permission::query()->findOrFail($id);
|
||||
$this->auditLog($request, 'permission_delete', ['metadata' => ['target_permission_id' => $permission->id]]);
|
||||
$permission->delete();
|
||||
|
||||
return response()->json(['code' => 0, 'message' => 'ok', 'data' => null]);
|
||||
}
|
||||
|
||||
#[Apidoc\Title('同步角色权限'), Apidoc\Method('PUT'), Apidoc\Url('/roles/{id}/permissions')]
|
||||
public function syncRolePermissions(Request $request, int $id): JsonResponse
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'permission_ids' => ['present', 'array'],
|
||||
'permission_ids.*' => ['integer', 'exists:permissions,id'],
|
||||
]);
|
||||
|
||||
$role = Role::query()->findOrFail($id);
|
||||
$role->syncPermissions($validated['permission_ids']);
|
||||
|
||||
$this->auditLog($request, 'role_permissions_update', ['metadata' => ['target_role_id' => $role->id]]);
|
||||
|
||||
return response()->json(['code' => 0, 'message' => 'ok', 'data' => $role->load('permissions')]);
|
||||
}
|
||||
|
||||
private function applyDefaultMeta(Permission $permission): Permission
|
||||
{
|
||||
$defaults = $this->defaultPermissionMeta((string) $permission->name);
|
||||
$dirty = false;
|
||||
|
||||
if (empty($permission->category) || $permission->category === 'general') {
|
||||
$permission->category = $defaults['category'];
|
||||
$dirty = true;
|
||||
}
|
||||
|
||||
if (empty($permission->description)) {
|
||||
$permission->description = $defaults['description'];
|
||||
$dirty = true;
|
||||
}
|
||||
|
||||
if ($dirty) {
|
||||
$permission->save();
|
||||
}
|
||||
|
||||
return $permission;
|
||||
}
|
||||
|
||||
private function defaultPermissionMeta(string $name): array
|
||||
{
|
||||
$map = [
|
||||
'platform.users.view' => ['category' => '用户管理', 'description' => '查看用户列表与详情'],
|
||||
'platform.users.manage' => ['category' => '用户管理', 'description' => '创建、修改、删除用户并分配权限'],
|
||||
'platform.roles.view' => ['category' => '角色管理', 'description' => '查看角色列表与角色权限'],
|
||||
'platform.roles.manage' => ['category' => '角色管理', 'description' => '创建、修改、删除角色'],
|
||||
'platform.permissions.view' => ['category' => '权限管理', 'description' => '查看权限配置'],
|
||||
'platform.permissions.manage' => ['category' => '权限管理', 'description' => '创建、修改、删除权限规则'],
|
||||
'platform.servers.view' => ['category' => '资源管理', 'description' => '查看服务器与资源信息'],
|
||||
'platform.servers.manage' => ['category' => '资源管理', 'description' => '维护服务器与资源信息'],
|
||||
'platform.accounts.view' => ['category' => '堡垒机账号', 'description' => '查看堡垒机授权账号'],
|
||||
'platform.accounts.manage' => ['category' => '堡垒机账号', 'description' => '维护堡垒机授权账号与刷新令牌'],
|
||||
'platform.logs.view' => ['category' => '日志审计', 'description' => '查看访问与操作日志'],
|
||||
'platform.logs.manage' => ['category' => '日志审计', 'description' => '新增或维护日志数据'],
|
||||
'resource.servers.use' => ['category' => '资源使用', 'description' => '发起服务器资源访问与连接操作'],
|
||||
];
|
||||
|
||||
if (isset($map[$name])) {
|
||||
return $map[$name];
|
||||
}
|
||||
|
||||
if (str_starts_with($name, 'resource.servers.use.')) {
|
||||
return [
|
||||
'category' => '资源使用',
|
||||
'description' => '服务器资源访问权限',
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
'category' => '通用',
|
||||
'description' => '系统权限:'.$name,
|
||||
];
|
||||
}
|
||||
}
|
||||
84
app/Http/Controllers/Api/RoleController.php
Normal file
84
app/Http/Controllers/Api/RoleController.php
Normal file
@ -0,0 +1,84 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\StoreRoleRequest;
|
||||
use App\Http\Requests\UpdateRoleRequest;
|
||||
use hg\apidoc\annotation as Apidoc;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Spatie\Permission\Models\Role;
|
||||
|
||||
#[Apidoc\Title('角色管理')]
|
||||
class RoleController extends Controller
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
$this->middleware('auth:api');
|
||||
$this->middleware('permission:platform.roles.view,api')->only(['index', 'show']);
|
||||
$this->middleware('permission:platform.roles.manage,api')->only(['store', 'update', 'destroy']);
|
||||
}
|
||||
|
||||
#[Apidoc\Title('角色列表'), Apidoc\Method('GET'), Apidoc\Url('/roles')]
|
||||
public function index(): JsonResponse
|
||||
{
|
||||
$roles = Role::query()->with('permissions')->latest()->paginate(20);
|
||||
|
||||
return response()->json(['code' => 0, 'message' => 'ok', 'data' => $roles]);
|
||||
}
|
||||
|
||||
#[Apidoc\Title('创建角色'), Apidoc\Method('POST'), Apidoc\Url('/roles')]
|
||||
public function store(StoreRoleRequest $request): JsonResponse
|
||||
{
|
||||
$role = Role::query()->create([
|
||||
'name' => $request->string('name')->toString(),
|
||||
'guard_name' => 'api',
|
||||
]);
|
||||
|
||||
if ($request->has('permission_ids')) {
|
||||
$role->syncPermissions($request->input('permission_ids', []));
|
||||
}
|
||||
|
||||
$this->auditLog($request, 'role_create', ['metadata' => ['target_role_id' => $role->id]]);
|
||||
|
||||
return response()->json(['code' => 0, 'message' => 'ok', 'data' => $role->load('permissions')], 201);
|
||||
}
|
||||
|
||||
#[Apidoc\Title('角色详情'), Apidoc\Method('GET'), Apidoc\Url('/roles/{id}')]
|
||||
public function show(int $id): JsonResponse
|
||||
{
|
||||
$role = Role::query()->with('permissions')->findOrFail($id);
|
||||
|
||||
return response()->json(['code' => 0, 'message' => 'ok', 'data' => $role]);
|
||||
}
|
||||
|
||||
#[Apidoc\Title('更新角色'), Apidoc\Method('PUT'), Apidoc\Url('/roles/{id}')]
|
||||
public function update(UpdateRoleRequest $request, int $id): JsonResponse
|
||||
{
|
||||
$role = Role::query()->findOrFail($id);
|
||||
|
||||
$role->update([
|
||||
'name' => $request->string('name', (string) $role->name)->toString(),
|
||||
'guard_name' => 'api',
|
||||
]);
|
||||
|
||||
if ($request->has('permission_ids')) {
|
||||
$role->syncPermissions($request->input('permission_ids', []));
|
||||
}
|
||||
|
||||
$this->auditLog($request, 'role_update', ['metadata' => ['target_role_id' => $role->id]]);
|
||||
|
||||
return response()->json(['code' => 0, 'message' => 'ok', 'data' => $role->load('permissions')]);
|
||||
}
|
||||
|
||||
#[Apidoc\Title('删除角色'), Apidoc\Method('DELETE'), Apidoc\Url('/roles/{id}')]
|
||||
public function destroy(Request $request, int $id): JsonResponse
|
||||
{
|
||||
$role = Role::query()->findOrFail($id);
|
||||
$this->auditLog($request, 'role_delete', ['metadata' => ['target_role_id' => $role->id]]);
|
||||
$role->delete();
|
||||
|
||||
return response()->json(['code' => 0, 'message' => 'ok', 'data' => null]);
|
||||
}
|
||||
}
|
||||
676
app/Http/Controllers/Api/ServerResourceController.php
Normal file
676
app/Http/Controllers/Api/ServerResourceController.php
Normal file
@ -0,0 +1,676 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\StoreServerResourceRequest;
|
||||
use App\Http\Requests\UpdateServerResourceRequest;
|
||||
use App\Models\AccessLog;
|
||||
use App\Models\BastionAccount;
|
||||
use App\Models\OpsProtocol;
|
||||
use App\Models\ServerResource;
|
||||
use App\Models\User;
|
||||
use App\Models\UserServerPermission;
|
||||
use hg\apidoc\annotation as Apidoc;
|
||||
use Illuminate\Http\Client\ConnectionException;
|
||||
use Illuminate\Http\Client\RequestException;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
use Spatie\Permission\Models\Permission;
|
||||
|
||||
#[Apidoc\Title('服务器资源管理')]
|
||||
class ServerResourceController extends Controller
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
$this->middleware('auth:api');
|
||||
$this->middleware('permission:platform.servers.view|resource.servers.use,api')->only(['index', 'show']);
|
||||
$this->middleware('permission:platform.servers.manage,api')->only(['store', 'update', 'destroy', 'syncUserPermissions', 'userPermissions']);
|
||||
}
|
||||
|
||||
#[Apidoc\Title('资源列表'), Apidoc\Method('GET'), Apidoc\Url('/servers')]
|
||||
public function index(): JsonResponse
|
||||
{
|
||||
$this->syncAllResourcePermissions();
|
||||
$query = ServerResource::query()->with('parent')->latest();
|
||||
$user = auth('api')->user();
|
||||
|
||||
if ($user && ! $user->can('platform.servers.view')) {
|
||||
$resourceIds = $user->serverResources()
|
||||
->where(function ($pivotQuery) {
|
||||
$pivotQuery->where('can_ssh', true)
|
||||
->orWhere('can_sftp', true)
|
||||
->orWhere('can_rdp', true);
|
||||
})
|
||||
->pluck('server_resources.id')
|
||||
->values();
|
||||
|
||||
$parentIds = ServerResource::query()
|
||||
->whereIn('id', $resourceIds)
|
||||
->pluck('parent_id')
|
||||
->filter()
|
||||
->values();
|
||||
|
||||
$query->where(function ($scope) use ($resourceIds, $parentIds) {
|
||||
$scope->whereIn('id', $resourceIds)->orWhereIn('id', $parentIds);
|
||||
});
|
||||
}
|
||||
|
||||
return response()->json(['code' => 0, 'message' => 'ok', 'data' => $query->paginate(500)]);
|
||||
}
|
||||
|
||||
#[Apidoc\Title('创建资源'), Apidoc\Method('POST'), Apidoc\Url('/servers')]
|
||||
public function store(StoreServerResourceRequest $request): JsonResponse
|
||||
{
|
||||
$data = $request->validated();
|
||||
$isResource = ! empty($data['parent_id']);
|
||||
$targetParentId = $isResource ? (int) $data['parent_id'] : null;
|
||||
|
||||
$this->ensureUniqueNameUnderParent((string) $data['name'], $targetParentId, null);
|
||||
|
||||
if ($isResource) {
|
||||
$parent = ServerResource::query()->findOrFail($targetParentId);
|
||||
$data['internal_ip'] = $data['internal_ip'] ?? $parent->internal_ip;
|
||||
$data['protocols'] = [$this->resolveResourceProtocol((string) ($data['protocol'] ?? ''), null)];
|
||||
$data['display_name'] = $data['display_name'] ?? $data['name'];
|
||||
$data['asset_id'] = $parent->asset_id;
|
||||
|
||||
if (empty($data['asset_id'])) {
|
||||
throw ValidationException::withMessages([
|
||||
'parent_id' => ['所属服务器未配置 asset_id,请先配置服务器的 asset_id。'],
|
||||
]);
|
||||
}
|
||||
|
||||
if (empty($data['account_id'])) {
|
||||
throw ValidationException::withMessages([
|
||||
'account_id' => ['资源必须填写 account_id。'],
|
||||
]);
|
||||
}
|
||||
} else {
|
||||
$data['protocols'] = [];
|
||||
$data['account_id'] = null;
|
||||
$data['display_name'] = $data['display_name'] ?? $data['name'];
|
||||
}
|
||||
|
||||
unset($data['protocol']);
|
||||
|
||||
$server = ServerResource::query()->create($data);
|
||||
$this->syncResourcePermission($server->load('parent'));
|
||||
$this->auditLog($request, $isResource ? 'resource_create' : 'server_create', ['server_resource_id' => $server->id]);
|
||||
|
||||
return response()->json(['code' => 0, 'message' => 'ok', 'data' => $server], 201);
|
||||
}
|
||||
|
||||
#[Apidoc\Title('资源详情'), Apidoc\Method('GET'), Apidoc\Url('/servers/{id}')]
|
||||
public function show(int $id): JsonResponse
|
||||
{
|
||||
return response()->json(['code' => 0, 'message' => 'ok', 'data' => ServerResource::query()->with('parent')->findOrFail($id)]);
|
||||
}
|
||||
|
||||
#[Apidoc\Title('更新资源'), Apidoc\Method('PUT'), Apidoc\Url('/servers/{id}')]
|
||||
public function update(UpdateServerResourceRequest $request, int $id): JsonResponse
|
||||
{
|
||||
$server = ServerResource::query()->findOrFail($id);
|
||||
$server->load('children');
|
||||
$data = $request->validated();
|
||||
$isResource = ! empty($data['parent_id']) || ! empty($server->parent_id);
|
||||
$targetParentId = $isResource ? (int) ($data['parent_id'] ?? $server->parent_id) : null;
|
||||
|
||||
$this->ensureUniqueNameUnderParent((string) $data['name'], $targetParentId, (int) $server->id);
|
||||
|
||||
if ($isResource) {
|
||||
$parent = ServerResource::query()->findOrFail($targetParentId);
|
||||
$data['internal_ip'] = $data['internal_ip'] ?? $parent->internal_ip;
|
||||
$data['protocols'] = [$this->resolveResourceProtocol((string) ($data['protocol'] ?? ''), (string) ($server->protocols[0] ?? ''))];
|
||||
$data['display_name'] = $data['display_name'] ?? ($server->display_name ?: $data['name']);
|
||||
$data['asset_id'] = $parent->asset_id;
|
||||
|
||||
if (empty($data['asset_id'])) {
|
||||
throw ValidationException::withMessages([
|
||||
'parent_id' => ['所属服务器未配置 asset_id,请先配置服务器的 asset_id。'],
|
||||
]);
|
||||
}
|
||||
|
||||
if (empty($data['account_id'])) {
|
||||
throw ValidationException::withMessages([
|
||||
'account_id' => ['资源必须填写 account_id。'],
|
||||
]);
|
||||
}
|
||||
} else {
|
||||
$data['protocols'] = [];
|
||||
$data['account_id'] = null;
|
||||
$data['display_name'] = $data['display_name'] ?? ($server->display_name ?: $data['name']);
|
||||
}
|
||||
|
||||
unset($data['protocol']);
|
||||
$server->update($data);
|
||||
$server->refresh()->load(['parent', 'children.parent']);
|
||||
$this->syncResourcePermission($server);
|
||||
if (! $server->parent_id) {
|
||||
ServerResource::query()
|
||||
->where('parent_id', $server->id)
|
||||
->update(['asset_id' => $server->asset_id]);
|
||||
foreach ($server->children as $childResource) {
|
||||
$this->syncResourcePermission($childResource->load('parent'));
|
||||
}
|
||||
}
|
||||
|
||||
$this->auditLog($request, $isResource ? 'resource_update' : 'server_update', ['server_resource_id' => $server->id]);
|
||||
|
||||
return response()->json(['code' => 0, 'message' => 'ok', 'data' => $server]);
|
||||
}
|
||||
|
||||
#[Apidoc\Title('删除资源'), Apidoc\Method('DELETE'), Apidoc\Url('/servers/{id}')]
|
||||
public function destroy(Request $request, int $id): JsonResponse
|
||||
{
|
||||
$server = ServerResource::query()->with('children')->findOrFail($id);
|
||||
$this->auditLog($request, $server->parent_id ? 'resource_delete' : 'server_delete', ['server_resource_id' => $server->id]);
|
||||
if (! $server->parent_id) {
|
||||
$descendantIds = $this->collectDescendantResourceIds((int) $server->id);
|
||||
|
||||
foreach ($descendantIds as $descendantId) {
|
||||
$this->deleteResourcePermission($descendantId);
|
||||
}
|
||||
|
||||
ServerResource::query()
|
||||
->whereIn('id', $descendantIds)
|
||||
->delete();
|
||||
}
|
||||
$this->deleteResourcePermission((int) $server->id);
|
||||
$server->delete();
|
||||
|
||||
return response()->json(['code' => 0, 'message' => 'ok', 'data' => null]);
|
||||
}
|
||||
|
||||
#[Apidoc\Title('资源用户权限'), Apidoc\Method('GET'), Apidoc\Url('/servers/{id}/user-permissions')]
|
||||
public function userPermissions(Request $request, int $id): JsonResponse
|
||||
{
|
||||
$server = ServerResource::query()->findOrFail($id);
|
||||
$assignedOnly = $request->boolean('assigned_only', false);
|
||||
|
||||
$users = User::query()->select(['id', 'nickname', 'email'])->with(['serverResources' => function ($query) use ($id) {
|
||||
$query->where('server_resource_id', $id);
|
||||
}])->orderBy('id')->get()->map(function (User $user) {
|
||||
$pivot = $user->serverResources->first()?->pivot;
|
||||
|
||||
return [
|
||||
'id' => $user->id,
|
||||
'nickname' => $user->nickname,
|
||||
'email' => $user->email,
|
||||
'can_ssh' => (bool) ($pivot->can_ssh ?? false),
|
||||
'can_sftp' => (bool) ($pivot->can_sftp ?? false),
|
||||
'can_rdp' => (bool) ($pivot->can_rdp ?? false),
|
||||
];
|
||||
})->filter(function (array $userItem) use ($assignedOnly) {
|
||||
if (! $assignedOnly) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return (bool) ($userItem['can_ssh'] || $userItem['can_sftp'] || $userItem['can_rdp']);
|
||||
})->values();
|
||||
|
||||
return response()->json(['code' => 0, 'message' => 'ok', 'data' => ['server' => $server, 'users' => $users]]);
|
||||
}
|
||||
|
||||
#[Apidoc\Title('同步资源用户权限'), Apidoc\Method('PUT'), Apidoc\Url('/servers/{id}/user-permissions')]
|
||||
public function syncUserPermissions(Request $request, int $id): JsonResponse
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'users' => ['required', 'array'],
|
||||
'users.*.id' => ['required', 'integer', 'exists:users,id'],
|
||||
'users.*.can_ssh' => ['required', 'boolean'],
|
||||
'users.*.can_sftp' => ['required', 'boolean'],
|
||||
'users.*.can_rdp' => ['required', 'boolean'],
|
||||
'partial' => ['sometimes', 'boolean'],
|
||||
]);
|
||||
|
||||
$server = ServerResource::query()->with('parent')->findOrFail($id);
|
||||
$syncData = [];
|
||||
|
||||
foreach ($validated['users'] as $userItem) {
|
||||
$syncData[(int) $userItem['id']] = [
|
||||
'can_ssh' => (bool) $userItem['can_ssh'],
|
||||
'can_sftp' => (bool) $userItem['can_sftp'],
|
||||
'can_rdp' => (bool) $userItem['can_rdp'],
|
||||
];
|
||||
}
|
||||
|
||||
$partial = (bool) ($validated['partial'] ?? false);
|
||||
if ($partial) {
|
||||
foreach ($syncData as $userId => $permissionItem) {
|
||||
$server->users()->updateExistingPivot($userId, $permissionItem);
|
||||
}
|
||||
} else {
|
||||
$server->users()->sync($syncData);
|
||||
}
|
||||
|
||||
$permission = $this->syncResourcePermission($server);
|
||||
$this->syncDirectPermissionsByPivot($server, $permission, $syncData);
|
||||
|
||||
$this->auditLog($request, 'resource_user_permissions_update', ['server_resource_id' => $server->id]);
|
||||
|
||||
return response()->json(['code' => 0, 'message' => 'ok', 'data' => null]);
|
||||
}
|
||||
|
||||
#[Apidoc\Title('使用服务器资源'), Apidoc\Method('POST'), Apidoc\Url('/servers/{id}/use')]
|
||||
public function useResource(Request $request, int $id): JsonResponse
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'account_name' => ['nullable', 'string', 'max:255'],
|
||||
'password' => ['nullable', 'string', 'max:255'],
|
||||
'protocol' => ['required', 'string', 'max:64'],
|
||||
]);
|
||||
|
||||
$resource = ServerResource::query()->with('parent')->findOrFail($id);
|
||||
if (! $resource->parent_id) {
|
||||
throw ValidationException::withMessages([
|
||||
'id' => ['请选择具体资源后再发起访问。'],
|
||||
]);
|
||||
}
|
||||
|
||||
if (! $resource->is_active) {
|
||||
throw ValidationException::withMessages([
|
||||
'id' => ['该资源已停用,无法访问。'],
|
||||
]);
|
||||
}
|
||||
|
||||
$assetId = (int) ($resource->asset_id ?: ($resource->parent?->asset_id ?: 0));
|
||||
$accountId = (int) ($resource->account_id ?: 0);
|
||||
|
||||
if ($assetId <= 0 || $accountId <= 0) {
|
||||
throw ValidationException::withMessages([
|
||||
'id' => ['该资源缺少 asset_id 或 account_id,无法访问。'],
|
||||
]);
|
||||
}
|
||||
|
||||
$requestedProtocol = (string) $validated['protocol'];
|
||||
$resourceProtocols = collect($resource->protocols ?? [])
|
||||
->map(fn ($item): string => (string) $item)
|
||||
->filter()
|
||||
->mapWithKeys(fn (string $item): array => [mb_strtolower($item) => $item])
|
||||
->toArray();
|
||||
$protocol = $resourceProtocols[mb_strtolower($requestedProtocol)] ?? '';
|
||||
if ($protocol === '') {
|
||||
throw ValidationException::withMessages([
|
||||
'protocol' => ['该资源不支持所选协议。'],
|
||||
]);
|
||||
}
|
||||
|
||||
/** @var User|null $user */
|
||||
$user = auth('api')->user();
|
||||
if (! $user || ! $this->canUseResource($user, $resource, $protocol)) {
|
||||
return response()->json([
|
||||
'code' => 403,
|
||||
'message' => '无权限使用该资源',
|
||||
'data' => null,
|
||||
], 403);
|
||||
}
|
||||
|
||||
$bastionAccount = BastionAccount::query()
|
||||
->where('is_active', true)
|
||||
->whereNotNull('usm')
|
||||
->where('usm', '!=', '')
|
||||
->whereNotNull('usm_authentication')
|
||||
->where('usm_authentication', '!=', '')
|
||||
->inRandomOrder()
|
||||
->first();
|
||||
|
||||
if (! $bastionAccount) {
|
||||
return response()->json([
|
||||
'code' => 422,
|
||||
'message' => '当前没有可用的堡垒机授权账号,请先刷新账号 Token',
|
||||
'data' => null,
|
||||
], 422);
|
||||
}
|
||||
|
||||
$baseUrl = (string) config('services.bastion_access.base_url', 'https://172.16.254.2');
|
||||
$endpoint = (string) config('services.bastion_access.sso_endpoint', '/usmapi/v1/operation/custom/sso');
|
||||
$timeout = (int) config('services.bastion_access.timeout', 30);
|
||||
$verifySsl = (bool) config('services.bastion_access.verify_ssl', false);
|
||||
$protocolId = $this->resolveProtocolId($protocol);
|
||||
$accountName = (string) ($validated['account_name'] ?? '');
|
||||
$password = (string) ($validated['password'] ?? '');
|
||||
|
||||
try {
|
||||
$response = Http::baseUrl($baseUrl)
|
||||
->acceptJson()
|
||||
->timeout($timeout)
|
||||
->retry(2, 300, throw: false)
|
||||
->withOptions(['verify' => $verifySsl])
|
||||
->withHeaders([
|
||||
'Cookie' => sprintf(
|
||||
'USM=%s; USM-AUTHENTICATION=%s',
|
||||
(string) $bastionAccount->usm,
|
||||
(string) $bastionAccount->usm_authentication
|
||||
),
|
||||
])
|
||||
->post($endpoint, [
|
||||
'accounts' => [[
|
||||
'account_name' => $accountName,
|
||||
'protocol_id' => $protocolId,
|
||||
'asset_id' => $assetId,
|
||||
'account_id' => $accountId,
|
||||
'rule_id' => 0,
|
||||
'password' => $password,
|
||||
]],
|
||||
'op_type' => 'bs',
|
||||
'description' => '',
|
||||
'second_verify_token' => '',
|
||||
'use_taibao_password' => 0,
|
||||
'use_om_password_cache' => 0,
|
||||
]);
|
||||
} catch (ConnectionException|RequestException $exception) {
|
||||
return response()->json([
|
||||
'code' => 502,
|
||||
'message' => '堡垒机接口调用失败:'.$exception->getMessage(),
|
||||
'data' => null,
|
||||
], 502);
|
||||
}
|
||||
|
||||
if (! $response->successful()) {
|
||||
return response()->json([
|
||||
'code' => 502,
|
||||
'message' => '堡垒机接口返回异常',
|
||||
'data' => [
|
||||
'status' => $response->status(),
|
||||
'response' => $response->json(),
|
||||
],
|
||||
], 502);
|
||||
}
|
||||
|
||||
$result = $response->json();
|
||||
if (strtoupper((string) data_get($result, 'code')) !== 'SUCCESS') {
|
||||
return response()->json([
|
||||
'code' => 502,
|
||||
'message' => '堡垒机接口返回失败:'.((string) data_get($result, 'msg', '未知错误')),
|
||||
'data' => ['response' => $result],
|
||||
], 502);
|
||||
}
|
||||
|
||||
$ssoUrl = (string) data_get($result, 'data.items.0.url', '');
|
||||
if ($ssoUrl === '') {
|
||||
return response()->json([
|
||||
'code' => 502,
|
||||
'message' => '堡垒机未返回可用的 SSO 地址',
|
||||
'data' => ['response' => $result],
|
||||
], 502);
|
||||
}
|
||||
|
||||
AccessLog::query()->create([
|
||||
'user_id' => $user->id,
|
||||
'server_resource_id' => $resource->id,
|
||||
'bastion_account_id' => $bastionAccount->id,
|
||||
'protocol' => $protocol,
|
||||
'action' => 'resource_use',
|
||||
'requested_at' => now(),
|
||||
'metadata' => [
|
||||
'resource_name' => $resource->display_name ?: $resource->name,
|
||||
'account_name' => $accountName,
|
||||
'has_password' => $password !== '',
|
||||
'protocol_id' => $protocolId,
|
||||
'bastion_response_code' => data_get($result, 'code'),
|
||||
],
|
||||
]);
|
||||
|
||||
$this->auditLog($request, 'resource_use', [
|
||||
'server_resource_id' => $resource->id,
|
||||
'bastion_account_id' => $bastionAccount->id,
|
||||
'metadata' => [
|
||||
'protocol' => $protocol,
|
||||
'protocol_id' => $protocolId,
|
||||
'account_name' => $accountName,
|
||||
],
|
||||
]);
|
||||
|
||||
return response()->json([
|
||||
'code' => 0,
|
||||
'message' => 'ok',
|
||||
'data' => [
|
||||
'url' => $ssoUrl,
|
||||
'protocol' => $protocol,
|
||||
'resource_id' => $resource->id,
|
||||
'resource_name' => $resource->display_name ?: $resource->name,
|
||||
'bastion_account_id' => $bastionAccount->id,
|
||||
'client_type' => (string) data_get($result, 'data.client_type', ''),
|
||||
'response' => $result,
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
private function syncDirectPermissionsByPivot(ServerResource $resource, Permission $permission, array $syncData): void
|
||||
{
|
||||
if (! $resource->parent_id) {
|
||||
return;
|
||||
}
|
||||
|
||||
foreach ($syncData as $userId => $permissionItem) {
|
||||
$canUseResource = (bool) ($permissionItem['can_ssh'] || $permissionItem['can_sftp'] || $permissionItem['can_rdp']);
|
||||
$user = User::query()->find((int) $userId);
|
||||
|
||||
if (! $user) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($canUseResource) {
|
||||
if (! $user->hasDirectPermission($permission->name)) {
|
||||
$user->givePermissionTo($permission);
|
||||
}
|
||||
} else {
|
||||
$user->revokePermissionTo($permission->name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static function buildResourcePermissionName(ServerResource $resource): string
|
||||
{
|
||||
$serverName = $resource->parent_id ? (string) ($resource->parent?->name ?: 'unknown-server') : (string) ($resource->name ?: 'unknown-server');
|
||||
$resourceName = (string) ($resource->name ?: 'unknown-resource');
|
||||
|
||||
return sprintf(
|
||||
'resource.servers.use.%s.%s',
|
||||
trim($serverName),
|
||||
trim($resourceName)
|
||||
);
|
||||
}
|
||||
|
||||
public static function resourcePermissionDescription(ServerResource $resource): string
|
||||
{
|
||||
return '服务器资源访问权限(资源ID: '.$resource->id.')';
|
||||
}
|
||||
|
||||
private function syncResourcePermission(ServerResource $resource): Permission
|
||||
{
|
||||
$description = self::resourcePermissionDescription($resource);
|
||||
$permission = Permission::query()
|
||||
->where('guard_name', 'api')
|
||||
->where('description', $description)
|
||||
->first();
|
||||
$basePermissionName = self::buildResourcePermissionName($resource);
|
||||
|
||||
if (! $permission) {
|
||||
$permission = Permission::query()->firstOrCreate(
|
||||
[
|
||||
'guard_name' => 'api',
|
||||
'name' => $this->resolvePermissionNameConflict($basePermissionName, null, (int) $resource->id),
|
||||
],
|
||||
[
|
||||
'category' => '资源使用',
|
||||
'description' => $description,
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
$permission->update([
|
||||
'name' => $this->resolvePermissionNameConflict($basePermissionName, (int) $permission->id, (int) $resource->id),
|
||||
'category' => '资源使用',
|
||||
'description' => $description,
|
||||
]);
|
||||
|
||||
return $permission;
|
||||
}
|
||||
|
||||
private function deleteResourcePermission(int $resourceId): void
|
||||
{
|
||||
Permission::query()
|
||||
->where('guard_name', 'api')
|
||||
->where('description', '服务器资源访问权限(资源ID: '.$resourceId.')')
|
||||
->delete();
|
||||
|
||||
UserServerPermission::query()
|
||||
->where('server_resource_id', $resourceId)
|
||||
->delete();
|
||||
}
|
||||
|
||||
private function ensureUniqueNameUnderParent(string $name, ?int $parentId, ?int $ignoreId): void
|
||||
{
|
||||
$query = ServerResource::query()->where('name', $name);
|
||||
|
||||
if ($parentId === null) {
|
||||
$query->whereNull('parent_id');
|
||||
} else {
|
||||
$query->where('parent_id', $parentId);
|
||||
}
|
||||
|
||||
if ($ignoreId !== null) {
|
||||
$query->where('id', '!=', $ignoreId);
|
||||
}
|
||||
|
||||
if ($query->exists()) {
|
||||
throw ValidationException::withMessages([
|
||||
'name' => ['同一服务器层级下名称必须唯一。'],
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
private function resolvePermissionNameConflict(string $baseName, ?int $ignorePermissionId, int $resourceId): string
|
||||
{
|
||||
$query = Permission::query()
|
||||
->where('guard_name', 'api')
|
||||
->where('name', $baseName);
|
||||
|
||||
if ($ignorePermissionId !== null) {
|
||||
$query->where('id', '!=', $ignorePermissionId);
|
||||
}
|
||||
|
||||
if (! $query->exists()) {
|
||||
return $baseName;
|
||||
}
|
||||
|
||||
return $baseName.'.'.$resourceId;
|
||||
}
|
||||
|
||||
private function syncAllResourcePermissions(): void
|
||||
{
|
||||
$resources = ServerResource::query()->with('parent')->get();
|
||||
|
||||
foreach ($resources as $resource) {
|
||||
$this->syncResourcePermission($resource);
|
||||
}
|
||||
}
|
||||
|
||||
private function canUseResource(User $user, ServerResource $resource, string $protocol): bool
|
||||
{
|
||||
if ($user->can('platform.servers.view') || $user->can('resource.servers.use')) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$resourcePermission = Permission::query()
|
||||
->where('guard_name', 'api')
|
||||
->where('description', self::resourcePermissionDescription($resource))
|
||||
->first();
|
||||
if ($resourcePermission && $user->hasPermissionTo($resourcePermission->name)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$pivot = $resource->users()
|
||||
->where('users.id', $user->id)
|
||||
->first()?->pivot;
|
||||
if (! $pivot) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return match ($protocol) {
|
||||
'SFTP' => (bool) $pivot->can_sftp,
|
||||
'RDP' => (bool) $pivot->can_rdp,
|
||||
default => (bool) $pivot->can_ssh,
|
||||
};
|
||||
}
|
||||
|
||||
private function resolveProtocolId(string $protocol): int
|
||||
{
|
||||
$managed = OpsProtocol::query()
|
||||
->where('name', $protocol)
|
||||
->first();
|
||||
if ($managed) {
|
||||
return (int) ($managed->bastion_protocol_id ?: 2);
|
||||
}
|
||||
|
||||
return match (strtoupper($protocol)) {
|
||||
'SFTP' => (int) config('services.bastion_access.protocol_ids.sftp', 4),
|
||||
'RDP' => (int) config('services.bastion_access.protocol_ids.rdp', 3),
|
||||
default => (int) config('services.bastion_access.protocol_ids.ssh', 2),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int[]
|
||||
*/
|
||||
private function collectDescendantResourceIds(int $parentId): array
|
||||
{
|
||||
$descendantIds = [];
|
||||
$queue = [$parentId];
|
||||
|
||||
while (! empty($queue)) {
|
||||
$children = ServerResource::query()
|
||||
->whereIn('parent_id', $queue)
|
||||
->pluck('id')
|
||||
->map(fn (int $id): int => (int) $id)
|
||||
->all();
|
||||
|
||||
if (empty($children)) {
|
||||
break;
|
||||
}
|
||||
|
||||
$descendantIds = array_values(array_unique([...$descendantIds, ...$children]));
|
||||
$queue = $children;
|
||||
}
|
||||
|
||||
return $descendantIds;
|
||||
}
|
||||
|
||||
private function resolveResourceProtocol(string $requestedProtocol, ?string $fallbackProtocol): string
|
||||
{
|
||||
$normalizedRequested = trim($requestedProtocol);
|
||||
if ($normalizedRequested !== '') {
|
||||
$exists = OpsProtocol::query()->where('name', $normalizedRequested)->exists();
|
||||
if (! $exists) {
|
||||
throw ValidationException::withMessages([
|
||||
'protocol' => ['协议不存在,请先在运维协议中配置。'],
|
||||
]);
|
||||
}
|
||||
|
||||
return $normalizedRequested;
|
||||
}
|
||||
|
||||
$normalizedFallback = trim((string) $fallbackProtocol);
|
||||
if ($normalizedFallback !== '') {
|
||||
return $normalizedFallback;
|
||||
}
|
||||
|
||||
$firstProtocol = (string) OpsProtocol::query()
|
||||
->where('is_active', true)
|
||||
->orderBy('sort')
|
||||
->orderBy('id')
|
||||
->value('name');
|
||||
|
||||
if ($firstProtocol === '') {
|
||||
throw ValidationException::withMessages([
|
||||
'protocol' => ['请先在运维协议中新增并启用至少一个协议。'],
|
||||
]);
|
||||
}
|
||||
|
||||
return $firstProtocol;
|
||||
}
|
||||
}
|
||||
188
app/Http/Controllers/Api/UserController.php
Normal file
188
app/Http/Controllers/Api/UserController.php
Normal file
@ -0,0 +1,188 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\StoreUserRequest;
|
||||
use App\Http\Requests\UpdateUserRequest;
|
||||
use App\Models\ServerResource;
|
||||
use App\Models\User;
|
||||
use App\Models\UserServerPermission;
|
||||
use hg\apidoc\annotation as Apidoc;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Spatie\Permission\Models\Permission;
|
||||
|
||||
#[Apidoc\Title('用户管理')]
|
||||
class UserController extends Controller
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
$this->middleware('auth:api');
|
||||
$this->middleware('permission:platform.users.view,api')->only(['index', 'show']);
|
||||
$this->middleware('permission:platform.users.manage,api')->only(['store', 'update', 'destroy', 'syncPermissions']);
|
||||
}
|
||||
|
||||
#[Apidoc\Title('用户列表'), Apidoc\Method('GET'), Apidoc\Url('/users')]
|
||||
public function index(Request $request): JsonResponse
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'per_page' => ['nullable', 'integer', 'min:1', 'max:100'],
|
||||
'sort_by' => ['nullable', 'string', 'in:id,nickname,email,phone,created_at'],
|
||||
'sort_order' => ['nullable', 'string', 'in:asc,desc'],
|
||||
]);
|
||||
|
||||
$sortBy = $validated['sort_by'] ?? 'created_at';
|
||||
$sortOrder = $validated['sort_order'] ?? 'desc';
|
||||
$perPage = (int) ($validated['per_page'] ?? 20);
|
||||
|
||||
$users = User::query()
|
||||
->with('roles')
|
||||
->orderBy($sortBy, $sortOrder)
|
||||
->paginate($perPage);
|
||||
|
||||
return response()->json(['code' => 0, 'message' => 'ok', 'data' => $users]);
|
||||
}
|
||||
|
||||
#[Apidoc\Title('创建用户'), Apidoc\Method('POST'), Apidoc\Url('/users')]
|
||||
public function store(StoreUserRequest $request): JsonResponse
|
||||
{
|
||||
$user = User::query()->create($request->validated());
|
||||
|
||||
if ($request->filled('role_ids')) {
|
||||
$user->syncRoles($request->validated('role_ids'));
|
||||
}
|
||||
|
||||
$this->auditLog($request, 'user_create', ['metadata' => ['target_user_id' => $user->id]]);
|
||||
|
||||
return response()->json(['code' => 0, 'message' => 'ok', 'data' => $user->load('roles')], 201);
|
||||
}
|
||||
|
||||
#[Apidoc\Title('用户详情'), Apidoc\Method('GET'), Apidoc\Url('/users/{id}')]
|
||||
public function show(int $id): JsonResponse
|
||||
{
|
||||
$user = User::query()->with(['roles', 'permissions', 'serverResources'])->findOrFail($id);
|
||||
|
||||
return response()->json(['code' => 0, 'message' => 'ok', 'data' => $user]);
|
||||
}
|
||||
|
||||
#[Apidoc\Title('更新用户'), Apidoc\Method('PUT'), Apidoc\Url('/users/{id}')]
|
||||
public function update(UpdateUserRequest $request, int $id): JsonResponse
|
||||
{
|
||||
$user = User::query()->findOrFail($id);
|
||||
$user->fill($request->safe()->except(['role_ids']));
|
||||
|
||||
if ($request->filled('password')) {
|
||||
$user->password = $request->validated('password');
|
||||
}
|
||||
|
||||
$user->save();
|
||||
|
||||
if ($request->has('role_ids')) {
|
||||
$user->syncRoles($request->validated('role_ids'));
|
||||
}
|
||||
|
||||
$this->auditLog($request, 'user_update', ['metadata' => ['target_user_id' => $user->id]]);
|
||||
|
||||
return response()->json(['code' => 0, 'message' => 'ok', 'data' => $user->load('roles')]);
|
||||
}
|
||||
|
||||
#[Apidoc\Title('同步用户权限'), Apidoc\Method('PUT'), Apidoc\Url('/users/{id}/permissions')]
|
||||
public function syncPermissions(Request $request, int $id): JsonResponse
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'permission_ids' => ['present', 'array'],
|
||||
'permission_ids.*' => ['integer', 'exists:permissions,id'],
|
||||
]);
|
||||
|
||||
$user = User::query()->findOrFail($id);
|
||||
$user->syncPermissions($validated['permission_ids']);
|
||||
$this->syncServerResourcePermissionsByDirectPermissions($user, $validated['permission_ids']);
|
||||
|
||||
$this->auditLog($request, 'user_permissions_update', ['metadata' => ['target_user_id' => $user->id, 'permission_ids' => $validated['permission_ids']]]);
|
||||
|
||||
return response()->json(['code' => 0, 'message' => 'ok', 'data' => $user->load(['roles', 'permissions'])]);
|
||||
}
|
||||
|
||||
#[Apidoc\Title('删除用户'), Apidoc\Method('DELETE'), Apidoc\Url('/users/{id}')]
|
||||
public function destroy(Request $request, int $id): JsonResponse
|
||||
{
|
||||
$user = User::query()->findOrFail($id);
|
||||
$this->auditLog($request, 'user_delete', ['metadata' => ['target_user_id' => $user->id]]);
|
||||
$user->delete();
|
||||
|
||||
return response()->json(['code' => 0, 'message' => 'ok', 'data' => null]);
|
||||
}
|
||||
|
||||
private function syncServerResourcePermissionsByDirectPermissions(User $user, array $permissionIds): void
|
||||
{
|
||||
$resourcePermissions = Permission::query()
|
||||
->select(['id', 'name', 'description'])
|
||||
->whereIn('id', $permissionIds)
|
||||
->where('guard_name', 'api')
|
||||
->where('name', 'like', 'resource.servers.use.%')
|
||||
->get();
|
||||
|
||||
$grantedResourceIds = $resourcePermissions
|
||||
->map(fn (Permission $permission): ?int => $this->resourceIdFromPermissionDescription((string) $permission->description))
|
||||
->filter(fn (?int $resourceId): bool => $resourceId !== null)
|
||||
->map(fn (int $resourceId): int => (int) $resourceId)
|
||||
->values()
|
||||
->all();
|
||||
|
||||
if (count($grantedResourceIds) > 0) {
|
||||
$existingResourceIds = ServerResource::query()
|
||||
->whereIn('id', $grantedResourceIds)
|
||||
->whereNotNull('parent_id')
|
||||
->pluck('id')
|
||||
->map(fn (int $resourceId): int => (int) $resourceId)
|
||||
->all();
|
||||
|
||||
foreach ($existingResourceIds as $resourceId) {
|
||||
UserServerPermission::query()->updateOrCreate(
|
||||
[
|
||||
'user_id' => $user->id,
|
||||
'server_resource_id' => $resourceId,
|
||||
],
|
||||
[
|
||||
'can_ssh' => true,
|
||||
'can_sftp' => true,
|
||||
'can_rdp' => true,
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
$managedResourceIds = Permission::query()
|
||||
->where('guard_name', 'api')
|
||||
->where('name', 'like', 'resource.servers.use.%')
|
||||
->where('description', 'like', '服务器资源访问权限(资源ID:%')
|
||||
->pluck('description')
|
||||
->map(fn (string $description): ?int => $this->resourceIdFromPermissionDescription($description))
|
||||
->filter(fn (?int $resourceId): bool => $resourceId !== null)
|
||||
->map(fn (int $resourceId): int => $resourceId)
|
||||
->values()
|
||||
->all();
|
||||
|
||||
if (count($managedResourceIds) > 0) {
|
||||
UserServerPermission::query()
|
||||
->where('user_id', $user->id)
|
||||
->whereIn('server_resource_id', $managedResourceIds)
|
||||
->whereNotIn('server_resource_id', $grantedResourceIds)
|
||||
->update([
|
||||
'can_ssh' => false,
|
||||
'can_sftp' => false,
|
||||
'can_rdp' => false,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
private function resourceIdFromPermissionDescription(string $description): ?int
|
||||
{
|
||||
if (! preg_match('/资源ID:\s*(\d+)/u', $description, $matches)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return isset($matches[1]) ? (int) $matches[1] : null;
|
||||
}
|
||||
}
|
||||
33
app/Http/Controllers/Controller.php
Normal file
33
app/Http/Controllers/Controller.php
Normal file
@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\AccessLog;
|
||||
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
|
||||
use Illuminate\Foundation\Validation\ValidatesRequests;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Routing\Controller as BaseController;
|
||||
|
||||
abstract class Controller extends BaseController
|
||||
{
|
||||
use AuthorizesRequests;
|
||||
use ValidatesRequests;
|
||||
|
||||
protected function auditLog(Request $request, string $action, array $extra = []): void
|
||||
{
|
||||
AccessLog::query()->create([
|
||||
'user_id' => auth('api')->id(),
|
||||
'server_resource_id' => $extra['server_resource_id'] ?? null,
|
||||
'bastion_account_id' => $extra['bastion_account_id'] ?? null,
|
||||
'protocol' => $extra['protocol'] ?? null,
|
||||
'action' => $action,
|
||||
'requested_at' => now(),
|
||||
'metadata' => [
|
||||
'path' => $request->path(),
|
||||
'method' => $request->method(),
|
||||
'client_ip' => $request->ip(),
|
||||
...($extra['metadata'] ?? []),
|
||||
],
|
||||
]);
|
||||
}
|
||||
}
|
||||
34
app/Http/Requests/LogQueryRequest.php
Normal file
34
app/Http/Requests/LogQueryRequest.php
Normal file
@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class LogQueryRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'from' => ['nullable', 'date'],
|
||||
'to' => ['nullable', 'date'],
|
||||
'action' => ['nullable', 'string', 'max:255'],
|
||||
'actions' => ['nullable', 'array'],
|
||||
'actions.*' => ['string', 'max:255'],
|
||||
'user_id' => ['nullable', 'integer', 'exists:users,id'],
|
||||
'user_ids' => ['nullable', 'array'],
|
||||
'user_ids.*' => ['integer', 'exists:users,id'],
|
||||
'server_resource_id' => ['nullable', 'integer', 'exists:server_resources,id'],
|
||||
'server_resource_ids' => ['nullable', 'array'],
|
||||
'server_resource_ids.*' => ['integer', 'exists:server_resources,id'],
|
||||
'protocol' => ['nullable', 'string', 'in:SSH,SFTP,RDP'],
|
||||
'sort_by' => ['nullable', 'string', 'in:id,requested_at,action,protocol,user,resource,account'],
|
||||
'sort_order' => ['nullable', 'string', 'in:asc,desc'],
|
||||
'per_page' => ['nullable', 'integer', 'min:1', 'max:100'],
|
||||
];
|
||||
}
|
||||
}
|
||||
26
app/Http/Requests/StoreAccessLogRequest.php
Normal file
26
app/Http/Requests/StoreAccessLogRequest.php
Normal file
@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class StoreAccessLogRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'user_id' => ['required', 'integer', 'exists:users,id'],
|
||||
'server_resource_id' => ['required', 'integer', 'exists:server_resources,id'],
|
||||
'bastion_account_id' => ['nullable', 'integer', 'exists:bastion_accounts,id'],
|
||||
'protocol' => ['required', 'string', 'in:SSH,SFTP,RDP'],
|
||||
'action' => ['required', 'string', 'max:64'],
|
||||
'requested_at' => ['required', 'date'],
|
||||
'metadata' => ['nullable', 'array'],
|
||||
];
|
||||
}
|
||||
}
|
||||
23
app/Http/Requests/StoreBastionAccountRequest.php
Normal file
23
app/Http/Requests/StoreBastionAccountRequest.php
Normal file
@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class StoreBastionAccountRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'name' => ['required', 'string', 'max:255'],
|
||||
'username' => ['required', 'string', 'max:255', 'unique:bastion_accounts,username'],
|
||||
'password' => ['required', 'string', 'min:6'],
|
||||
'is_active' => ['sometimes', 'boolean'],
|
||||
];
|
||||
}
|
||||
}
|
||||
23
app/Http/Requests/StorePermissionRequest.php
Normal file
23
app/Http/Requests/StorePermissionRequest.php
Normal file
@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class StorePermissionRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'name' => ['required', 'string', 'max:255'],
|
||||
'category' => ['sometimes', 'string', 'max:100'],
|
||||
'description' => ['nullable', 'string', 'max:255'],
|
||||
'guard_name' => ['sometimes', 'string', 'max:255'],
|
||||
];
|
||||
}
|
||||
}
|
||||
23
app/Http/Requests/StoreRoleRequest.php
Normal file
23
app/Http/Requests/StoreRoleRequest.php
Normal file
@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class StoreRoleRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'name' => ['required', 'string', 'max:125'],
|
||||
'guard_name' => ['sometimes', 'string', 'max:125'],
|
||||
'permission_ids' => ['sometimes', 'array'],
|
||||
'permission_ids.*' => ['integer', 'exists:permissions,id'],
|
||||
];
|
||||
}
|
||||
}
|
||||
28
app/Http/Requests/StoreServerResourceRequest.php
Normal file
28
app/Http/Requests/StoreServerResourceRequest.php
Normal file
@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class StoreServerResourceRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'name' => ['required', 'string', 'max:255', 'regex:/^[A-Za-z][A-Za-z0-9._-]*$/'],
|
||||
'display_name' => ['nullable', 'string', 'max:255'],
|
||||
'parent_id' => ['nullable', 'integer', 'exists:server_resources,id'],
|
||||
'internal_ip' => ['nullable', 'ip'],
|
||||
'asset_id' => ['nullable', 'integer', 'min:1'],
|
||||
'account_id' => ['nullable', 'integer', 'min:1'],
|
||||
'protocol' => ['nullable', 'string', 'max:64'],
|
||||
'description' => ['nullable', 'string', 'max:255'],
|
||||
'is_active' => ['sometimes', 'boolean'],
|
||||
];
|
||||
}
|
||||
}
|
||||
25
app/Http/Requests/StoreUserRequest.php
Normal file
25
app/Http/Requests/StoreUserRequest.php
Normal file
@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class StoreUserRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'nickname' => ['required', 'string', 'max:255'],
|
||||
'email' => ['required', 'email', 'max:255', 'unique:users,email'],
|
||||
'phone' => ['nullable', 'string', 'max:32', 'unique:users,phone'],
|
||||
'password' => ['required', 'string', 'min:6'],
|
||||
'role_ids' => ['sometimes', 'array'],
|
||||
'role_ids.*' => ['integer', 'exists:roles,id'],
|
||||
];
|
||||
}
|
||||
}
|
||||
26
app/Http/Requests/UpdateBastionAccountRequest.php
Normal file
26
app/Http/Requests/UpdateBastionAccountRequest.php
Normal file
@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
||||
class UpdateBastionAccountRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
$id = (int) $this->route('id');
|
||||
|
||||
return [
|
||||
'name' => ['sometimes', 'required', 'string', 'max:255'],
|
||||
'username' => ['sometimes', 'required', 'string', 'max:255', Rule::unique('bastion_accounts', 'username')->ignore($id)],
|
||||
'password' => ['sometimes', 'required', 'string', 'min:6'],
|
||||
'is_active' => ['sometimes', 'boolean'],
|
||||
];
|
||||
}
|
||||
}
|
||||
23
app/Http/Requests/UpdatePermissionRequest.php
Normal file
23
app/Http/Requests/UpdatePermissionRequest.php
Normal file
@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class UpdatePermissionRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'name' => ['sometimes', 'required', 'string', 'max:255'],
|
||||
'category' => ['sometimes', 'required', 'string', 'max:100'],
|
||||
'description' => ['nullable', 'string', 'max:255'],
|
||||
'guard_name' => ['sometimes', 'string', 'max:255'],
|
||||
];
|
||||
}
|
||||
}
|
||||
23
app/Http/Requests/UpdateRoleRequest.php
Normal file
23
app/Http/Requests/UpdateRoleRequest.php
Normal file
@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class UpdateRoleRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'name' => ['sometimes', 'required', 'string', 'max:125'],
|
||||
'guard_name' => ['sometimes', 'string', 'max:125'],
|
||||
'permission_ids' => ['sometimes', 'array'],
|
||||
'permission_ids.*' => ['integer', 'exists:permissions,id'],
|
||||
];
|
||||
}
|
||||
}
|
||||
28
app/Http/Requests/UpdateServerResourceRequest.php
Normal file
28
app/Http/Requests/UpdateServerResourceRequest.php
Normal file
@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class UpdateServerResourceRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'name' => ['required', 'string', 'max:255', 'regex:/^[A-Za-z][A-Za-z0-9._-]*$/'],
|
||||
'display_name' => ['nullable', 'string', 'max:255'],
|
||||
'parent_id' => ['nullable', 'integer', 'exists:server_resources,id'],
|
||||
'internal_ip' => ['nullable', 'ip'],
|
||||
'asset_id' => ['nullable', 'integer', 'min:1'],
|
||||
'account_id' => ['nullable', 'integer', 'min:1'],
|
||||
'protocol' => ['nullable', 'string', 'max:64'],
|
||||
'description' => ['nullable', 'string', 'max:255'],
|
||||
'is_active' => ['sometimes', 'boolean'],
|
||||
];
|
||||
}
|
||||
}
|
||||
28
app/Http/Requests/UpdateUserRequest.php
Normal file
28
app/Http/Requests/UpdateUserRequest.php
Normal file
@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
||||
class UpdateUserRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
$userId = (int) $this->route('id');
|
||||
|
||||
return [
|
||||
'nickname' => ['sometimes', 'required', 'string', 'max:255'],
|
||||
'email' => ['sometimes', 'required', 'email', 'max:255', Rule::unique('users', 'email')->ignore($userId)],
|
||||
'phone' => ['nullable', 'string', 'max:32', Rule::unique('users', 'phone')->ignore($userId)],
|
||||
'password' => ['sometimes', 'required', 'string', 'min:6'],
|
||||
'role_ids' => ['sometimes', 'array'],
|
||||
'role_ids.*' => ['integer', 'exists:roles,id'],
|
||||
];
|
||||
}
|
||||
}
|
||||
45
app/Models/AccessLog.php
Normal file
45
app/Models/AccessLog.php
Normal file
@ -0,0 +1,45 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
class AccessLog extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $fillable = [
|
||||
'user_id',
|
||||
'server_resource_id',
|
||||
'bastion_account_id',
|
||||
'protocol',
|
||||
'action',
|
||||
'requested_at',
|
||||
'metadata',
|
||||
];
|
||||
|
||||
public function user(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class);
|
||||
}
|
||||
|
||||
public function serverResource(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(ServerResource::class);
|
||||
}
|
||||
|
||||
public function bastionAccount(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(BastionAccount::class);
|
||||
}
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'requested_at' => 'datetime',
|
||||
'metadata' => 'array',
|
||||
];
|
||||
}
|
||||
}
|
||||
36
app/Models/BastionAccount.php
Normal file
36
app/Models/BastionAccount.php
Normal file
@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
|
||||
class BastionAccount extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $fillable = [
|
||||
'name',
|
||||
'username',
|
||||
'password',
|
||||
'usm_authentication',
|
||||
'usm',
|
||||
'last_token_refreshed_at',
|
||||
'is_active',
|
||||
];
|
||||
|
||||
public function accessLogs(): HasMany
|
||||
{
|
||||
return $this->hasMany(AccessLog::class);
|
||||
}
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'password' => 'encrypted',
|
||||
'is_active' => 'boolean',
|
||||
'last_token_refreshed_at' => 'datetime',
|
||||
];
|
||||
}
|
||||
}
|
||||
32
app/Models/OpsProtocol.php
Normal file
32
app/Models/OpsProtocol.php
Normal file
@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
|
||||
class OpsProtocol extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $fillable = [
|
||||
'name',
|
||||
'bastion_protocol_id',
|
||||
'description',
|
||||
'sort',
|
||||
'is_active',
|
||||
];
|
||||
|
||||
public function softwares(): HasMany
|
||||
{
|
||||
return $this->hasMany(OpsSoftware::class)->orderBy('sort')->orderBy('id');
|
||||
}
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'is_active' => 'boolean',
|
||||
];
|
||||
}
|
||||
}
|
||||
34
app/Models/OpsSoftware.php
Normal file
34
app/Models/OpsSoftware.php
Normal file
@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
class OpsSoftware extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $table = 'ops_softwares';
|
||||
|
||||
protected $fillable = [
|
||||
'ops_protocol_id',
|
||||
'name',
|
||||
'client_path',
|
||||
'sort',
|
||||
'is_active',
|
||||
];
|
||||
|
||||
public function protocol(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(OpsProtocol::class, 'ops_protocol_id');
|
||||
}
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'is_active' => 'boolean',
|
||||
];
|
||||
}
|
||||
}
|
||||
56
app/Models/ServerResource.php
Normal file
56
app/Models/ServerResource.php
Normal file
@ -0,0 +1,56 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
|
||||
class ServerResource extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $fillable = [
|
||||
'name',
|
||||
'display_name',
|
||||
'parent_id',
|
||||
'internal_ip',
|
||||
'asset_id',
|
||||
'account_id',
|
||||
'protocols',
|
||||
'description',
|
||||
'is_active',
|
||||
];
|
||||
|
||||
public function parent(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(self::class, 'parent_id');
|
||||
}
|
||||
|
||||
public function children(): HasMany
|
||||
{
|
||||
return $this->hasMany(self::class, 'parent_id');
|
||||
}
|
||||
|
||||
public function users(): BelongsToMany
|
||||
{
|
||||
return $this->belongsToMany(User::class, 'user_server_permissions')
|
||||
->withPivot(['can_ssh', 'can_sftp', 'can_rdp'])
|
||||
->withTimestamps();
|
||||
}
|
||||
|
||||
public function accessLogs(): HasMany
|
||||
{
|
||||
return $this->hasMany(AccessLog::class);
|
||||
}
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'protocols' => 'array',
|
||||
'is_active' => 'boolean',
|
||||
];
|
||||
}
|
||||
}
|
||||
67
app/Models/User.php
Normal file
67
app/Models/User.php
Normal file
@ -0,0 +1,67 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
||||
use Illuminate\Foundation\Auth\User as Authenticatable;
|
||||
use Illuminate\Notifications\Notifiable;
|
||||
use Spatie\Permission\Traits\HasRoles;
|
||||
use Tymon\JWTAuth\Contracts\JWTSubject;
|
||||
|
||||
class User extends Authenticatable implements JWTSubject
|
||||
{
|
||||
use HasFactory;
|
||||
use HasRoles;
|
||||
use Notifiable;
|
||||
|
||||
protected string $guard_name = 'api';
|
||||
|
||||
protected $fillable = [
|
||||
'nickname',
|
||||
'email',
|
||||
'phone',
|
||||
'password',
|
||||
];
|
||||
|
||||
protected $hidden = [
|
||||
'password',
|
||||
'remember_token',
|
||||
];
|
||||
|
||||
public function getJWTIdentifier(): mixed
|
||||
{
|
||||
return $this->getKey();
|
||||
}
|
||||
|
||||
public function getJWTCustomClaims(): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
public function serverResources(): BelongsToMany
|
||||
{
|
||||
return $this->belongsToMany(ServerResource::class, 'user_server_permissions')
|
||||
->withPivot(['can_ssh', 'can_sftp', 'can_rdp'])
|
||||
->withTimestamps();
|
||||
}
|
||||
|
||||
public function opsSoftwarePreferences(): HasMany
|
||||
{
|
||||
return $this->hasMany(UserOpsSoftwarePreference::class);
|
||||
}
|
||||
|
||||
public function isAdmin(): bool
|
||||
{
|
||||
return $this->hasRole('admin', 'api');
|
||||
}
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'email_verified_at' => 'datetime',
|
||||
'password' => 'hashed',
|
||||
];
|
||||
}
|
||||
}
|
||||
34
app/Models/UserOpsSoftwarePreference.php
Normal file
34
app/Models/UserOpsSoftwarePreference.php
Normal file
@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
class UserOpsSoftwarePreference extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $fillable = [
|
||||
'user_id',
|
||||
'ops_protocol_id',
|
||||
'ops_software_id',
|
||||
];
|
||||
|
||||
public function user(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class);
|
||||
}
|
||||
|
||||
public function protocol(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(OpsProtocol::class, 'ops_protocol_id');
|
||||
}
|
||||
|
||||
public function software(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(OpsSoftware::class, 'ops_software_id');
|
||||
}
|
||||
}
|
||||
|
||||
28
app/Models/UserServerPermission.php
Normal file
28
app/Models/UserServerPermission.php
Normal file
@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class UserServerPermission extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $fillable = [
|
||||
'user_id',
|
||||
'server_resource_id',
|
||||
'can_ssh',
|
||||
'can_sftp',
|
||||
'can_rdp',
|
||||
];
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'can_ssh' => 'boolean',
|
||||
'can_sftp' => 'boolean',
|
||||
'can_rdp' => 'boolean',
|
||||
];
|
||||
}
|
||||
}
|
||||
35
app/Providers/AppServiceProvider.php
Normal file
35
app/Providers/AppServiceProvider.php
Normal file
@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
namespace App\Providers;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
class AppServiceProvider extends ServiceProvider
|
||||
{
|
||||
/**
|
||||
* Register any application services.
|
||||
*/
|
||||
public function register(): void
|
||||
{
|
||||
//
|
||||
}
|
||||
|
||||
/**
|
||||
* Bootstrap any application services.
|
||||
*/
|
||||
public function boot(): void
|
||||
{
|
||||
Schema::defaultStringLength(191);
|
||||
|
||||
Gate::before(function (User $user, string $ability): ?bool {
|
||||
if ($user->hasRole('admin', 'api')) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return null;
|
||||
});
|
||||
}
|
||||
}
|
||||
15
artisan
Normal file
15
artisan
Normal file
@ -0,0 +1,15 @@
|
||||
#!/usr/bin/env php
|
||||
<?php
|
||||
|
||||
use Symfony\Component\Console\Input\ArgvInput;
|
||||
|
||||
define('LARAVEL_START', microtime(true));
|
||||
|
||||
// Register the Composer autoloader...
|
||||
require __DIR__.'/vendor/autoload.php';
|
||||
|
||||
// Bootstrap Laravel and handle the command...
|
||||
$status = (require_once __DIR__.'/bootstrap/app.php')
|
||||
->handleCommand(new ArgvInput);
|
||||
|
||||
exit($status);
|
||||
144
bootstrap/app.php
Normal file
144
bootstrap/app.php
Normal file
@ -0,0 +1,144 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Auth\AuthenticationException;
|
||||
use Illuminate\Foundation\Application;
|
||||
use Illuminate\Foundation\Configuration\Exceptions;
|
||||
use Illuminate\Foundation\Configuration\Middleware;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
use Spatie\Permission\Exceptions\UnauthorizedException;
|
||||
use Spatie\Permission\Middleware\PermissionMiddleware;
|
||||
use Spatie\Permission\Middleware\RoleMiddleware;
|
||||
use Spatie\Permission\Middleware\RoleOrPermissionMiddleware;
|
||||
|
||||
return Application::configure(basePath: dirname(__DIR__))
|
||||
->withRouting(
|
||||
web: __DIR__.'/../routes/web.php',
|
||||
commands: __DIR__.'/../routes/console.php',
|
||||
health: '/up',
|
||||
)
|
||||
->withMiddleware(function (Middleware $middleware) {
|
||||
$middleware->alias([
|
||||
'role' => RoleMiddleware::class,
|
||||
'permission' => PermissionMiddleware::class,
|
||||
'role_or_permission' => RoleOrPermissionMiddleware::class,
|
||||
]);
|
||||
})
|
||||
->withExceptions(function (Exceptions $exceptions) {
|
||||
$exceptions->render(function (ValidationException $exception, Request $request) {
|
||||
if (! $request->expectsJson()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$attributeLabels = [
|
||||
'name' => '名称',
|
||||
'display_name' => '显示名称',
|
||||
'category' => '分类',
|
||||
'description' => '描述',
|
||||
'guard_name' => '守卫',
|
||||
'parent_id' => '所属服务器',
|
||||
'internal_ip' => '内网IP',
|
||||
'asset_id' => '资产ID',
|
||||
'account_id' => '账号ID',
|
||||
'protocol' => '协议',
|
||||
'protocols' => '协议',
|
||||
'is_active' => '启用状态',
|
||||
'nickname' => '昵称',
|
||||
'email' => '邮箱',
|
||||
'phone' => '手机号',
|
||||
'password' => '密码',
|
||||
'role_ids' => '角色',
|
||||
'permission_ids' => '权限',
|
||||
'users' => '用户',
|
||||
'from' => '开始日期',
|
||||
'to' => '结束日期',
|
||||
'action' => '动作',
|
||||
'actions' => '动作',
|
||||
'user_id' => '用户',
|
||||
'user_ids' => '用户',
|
||||
'server_resource_id' => '资源',
|
||||
'server_resource_ids' => '资源',
|
||||
'per_page' => '每页数量',
|
||||
'username' => '用户名',
|
||||
'token' => '令牌',
|
||||
];
|
||||
|
||||
$resolveAttribute = function (string $field) use ($attributeLabels): string {
|
||||
if (isset($attributeLabels[$field])) {
|
||||
return $attributeLabels[$field];
|
||||
}
|
||||
|
||||
foreach ($attributeLabels as $key => $label) {
|
||||
if (str_starts_with($field, $key.'.')) {
|
||||
return $label;
|
||||
}
|
||||
}
|
||||
|
||||
return $field;
|
||||
};
|
||||
|
||||
$ruleMessage = function (string $rule, string $attribute, array $params = []): string {
|
||||
return match (strtolower($rule)) {
|
||||
'required' => $attribute.'不能为空',
|
||||
'present' => $attribute.'必须传入',
|
||||
'array' => $attribute.'必须是数组',
|
||||
'string' => $attribute.'必须是字符串',
|
||||
'integer' => $attribute.'必须是整数',
|
||||
'boolean' => $attribute.'必须是布尔值',
|
||||
'email' => $attribute.'格式不正确',
|
||||
'ip' => $attribute.'格式不正确',
|
||||
'date' => $attribute.'必须是有效日期',
|
||||
'exists' => $attribute.'不存在或已失效',
|
||||
'unique' => $attribute.'已存在,请更换',
|
||||
'in' => $attribute.'不在允许范围内',
|
||||
'min' => $attribute.'不能小于'.($params[0] ?? '').'',
|
||||
'max' => $attribute.'不能大于'.($params[0] ?? '').'',
|
||||
'regex' => $attribute.'格式不正确',
|
||||
'nullable' => $attribute.'格式不正确',
|
||||
'sometimes' => $attribute.'格式不正确',
|
||||
default => $attribute.'参数不合法',
|
||||
};
|
||||
};
|
||||
|
||||
$failedRules = $exception->validator->failed();
|
||||
$translatedErrors = [];
|
||||
|
||||
foreach ($failedRules as $field => $rules) {
|
||||
$attribute = $resolveAttribute($field);
|
||||
foreach ($rules as $rule => $params) {
|
||||
$translatedErrors[$field][] = $ruleMessage($rule, $attribute, $params);
|
||||
}
|
||||
}
|
||||
|
||||
if (empty($translatedErrors)) {
|
||||
$translatedErrors = $exception->errors();
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
'code' => 422,
|
||||
'message' => '请求参数校验失败',
|
||||
'errors' => $translatedErrors,
|
||||
'data' => null,
|
||||
], 422);
|
||||
});
|
||||
|
||||
$exceptions->render(function (UnauthorizedException $exception, Request $request) {
|
||||
if ($request->expectsJson()) {
|
||||
return response()->json([
|
||||
'code' => 403,
|
||||
'message' => '无权限执行此操作',
|
||||
'data' => null,
|
||||
], 403);
|
||||
}
|
||||
});
|
||||
|
||||
$exceptions->render(function (AuthenticationException $exception, Request $request) {
|
||||
if ($request->expectsJson()) {
|
||||
return response()->json([
|
||||
'code' => 401,
|
||||
'message' => '未认证或登录已过期',
|
||||
'data' => null,
|
||||
], 401);
|
||||
}
|
||||
});
|
||||
})->create();
|
||||
2
bootstrap/cache/.gitignore
vendored
Normal file
2
bootstrap/cache/.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
*
|
||||
!.gitignore
|
||||
5
bootstrap/providers.php
Normal file
5
bootstrap/providers.php
Normal file
@ -0,0 +1,5 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
App\Providers\AppServiceProvider::class,
|
||||
];
|
||||
73
composer.json
Normal file
73
composer.json
Normal file
@ -0,0 +1,73 @@
|
||||
{
|
||||
"name": "laravel/laravel",
|
||||
"type": "project",
|
||||
"description": "The skeleton application for the Laravel framework.",
|
||||
"keywords": ["laravel", "framework"],
|
||||
"license": "MIT",
|
||||
"require": {
|
||||
"php": "^8.2",
|
||||
"hg/apidoc": "^5.3",
|
||||
"laravel/framework": "^11.0",
|
||||
"laravel/tinker": "^2.9",
|
||||
"spatie/laravel-permission": "6.9",
|
||||
"tymon/jwt-auth": "^2.3"
|
||||
},
|
||||
"require-dev": {
|
||||
"fakerphp/faker": "^1.23",
|
||||
"laravel/boost": "^2.4",
|
||||
"laravel/pint": "^1.13",
|
||||
"laravel/sail": "^1.26",
|
||||
"mockery/mockery": "^1.6",
|
||||
"nunomaduro/collision": "^8.0",
|
||||
"phpunit/phpunit": "^10.5",
|
||||
"spatie/laravel-ignition": "^2.4"
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"App\\": "app/",
|
||||
"Database\\Factories\\": "database/factories/",
|
||||
"Database\\Seeders\\": "database/seeders/"
|
||||
}
|
||||
},
|
||||
"autoload-dev": {
|
||||
"psr-4": {
|
||||
"Tests\\": "tests/"
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"post-autoload-dump": [
|
||||
"Illuminate\\Foundation\\ComposerScripts::postAutoloadDump",
|
||||
"@php artisan package:discover --ansi"
|
||||
],
|
||||
"post-update-cmd": [
|
||||
"@php artisan vendor:publish --tag=laravel-assets --ansi --force"
|
||||
],
|
||||
"post-root-package-install": [
|
||||
"@php -r \"file_exists('.env') || copy('.env.example', '.env');\""
|
||||
],
|
||||
"post-create-project-cmd": [
|
||||
"@php artisan key:generate --ansi",
|
||||
"@php -r \"file_exists('database/database.sqlite') || touch('database/database.sqlite');\"",
|
||||
"@php artisan migrate --ansi"
|
||||
]
|
||||
},
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-master": "11.x-dev"
|
||||
},
|
||||
"laravel": {
|
||||
"dont-discover": []
|
||||
}
|
||||
},
|
||||
"config": {
|
||||
"optimize-autoloader": true,
|
||||
"preferred-install": "dist",
|
||||
"sort-packages": true,
|
||||
"allow-plugins": {
|
||||
"pestphp/pest-plugin": true,
|
||||
"php-http/discovery": true
|
||||
}
|
||||
},
|
||||
"minimum-stability": "stable",
|
||||
"prefer-stable": true
|
||||
}
|
||||
9292
composer.lock
generated
Normal file
9292
composer.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
75
config/apidoc.php
Normal file
75
config/apidoc.php
Normal file
@ -0,0 +1,75 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
'title' => 'Bastion SSO ApiDoc',
|
||||
'desc' => '统一SSO登录系统 API 文档',
|
||||
'apps' => [
|
||||
[
|
||||
'title' => 'Api接口',
|
||||
'path' => 'app\Http\Controllers\Api',
|
||||
'key' => 'api',
|
||||
],
|
||||
],
|
||||
'definitions' => 'app\Http\Controllers\Api\Definitions',
|
||||
'auto_url' => [
|
||||
'letter_rule' => 'lcfirst',
|
||||
'prefix' => '',
|
||||
],
|
||||
'auto_register_routes' => true,
|
||||
'cache' => [
|
||||
'enable' => false,
|
||||
],
|
||||
'auth' => [
|
||||
'enable' => false,
|
||||
'password' => '123456',
|
||||
'secret_key' => 'apidoc#hg_code',
|
||||
'expire' => 24 * 60 * 60,
|
||||
],
|
||||
'params' => [
|
||||
'header' => [
|
||||
['name' => 'Authorization', 'type' => 'string', 'require' => true, 'desc' => 'Bearer Token'],
|
||||
],
|
||||
'query' => [],
|
||||
'body' => [],
|
||||
],
|
||||
'responses' => [
|
||||
'success' => [
|
||||
['name' => 'code', 'desc' => '业务代码', 'type' => 'int', 'require' => 1],
|
||||
['name' => 'message', 'desc' => '业务信息', 'type' => 'string', 'require' => 1],
|
||||
['name' => 'data', 'desc' => '业务数据', 'main' => true, 'type' => 'object', 'require' => 1],
|
||||
],
|
||||
'error' => [
|
||||
['name' => 'code', 'desc' => '业务代码', 'type' => 'int', 'require' => 1],
|
||||
['name' => 'message', 'desc' => '业务信息', 'type' => 'string', 'require' => 1],
|
||||
],
|
||||
],
|
||||
'responses_status' => [
|
||||
['name' => '200', 'desc' => '请求成功'],
|
||||
['name' => '401', 'desc' => '登录令牌无效', 'contentType' => ''],
|
||||
],
|
||||
'route_prefix' => '/apidoc',
|
||||
'default_author' => '',
|
||||
'default_method' => 'GET',
|
||||
'allowCrossDomain' => false,
|
||||
'ignored_annitation' => [],
|
||||
'ignored_methods' => [
|
||||
'__call',
|
||||
'middleware',
|
||||
'getMiddleware',
|
||||
'callAction',
|
||||
'authorize',
|
||||
'authorizeForUser',
|
||||
'authorizeResource',
|
||||
'validate',
|
||||
'validateWith',
|
||||
'validateWithBag',
|
||||
],
|
||||
'database' => [],
|
||||
'docs' => [],
|
||||
'generator' => [],
|
||||
'code_template' => [],
|
||||
'share' => [
|
||||
'enable' => false,
|
||||
'actions' => [],
|
||||
],
|
||||
];
|
||||
126
config/app.php
Normal file
126
config/app.php
Normal file
@ -0,0 +1,126 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Application Name
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This value is the name of your application, which will be used when the
|
||||
| framework needs to place the application's name in a notification or
|
||||
| other UI elements where an application name needs to be displayed.
|
||||
|
|
||||
*/
|
||||
|
||||
'name' => env('APP_NAME', 'Laravel'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Application Environment
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This value determines the "environment" your application is currently
|
||||
| running in. This may determine how you prefer to configure various
|
||||
| services the application utilizes. Set this in your ".env" file.
|
||||
|
|
||||
*/
|
||||
|
||||
'env' => env('APP_ENV', 'production'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Application Debug Mode
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| When your application is in debug mode, detailed error messages with
|
||||
| stack traces will be shown on every error that occurs within your
|
||||
| application. If disabled, a simple generic error page is shown.
|
||||
|
|
||||
*/
|
||||
|
||||
'debug' => (bool) env('APP_DEBUG', false),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Application URL
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This URL is used by the console to properly generate URLs when using
|
||||
| the Artisan command line tool. You should set this to the root of
|
||||
| the application so that it's available within Artisan commands.
|
||||
|
|
||||
*/
|
||||
|
||||
'url' => env('APP_URL', 'http://localhost'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Application Timezone
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Here you may specify the default timezone for your application, which
|
||||
| will be used by the PHP date and date-time functions. The timezone
|
||||
| is set to "UTC" by default as it is suitable for most use cases.
|
||||
|
|
||||
*/
|
||||
|
||||
'timezone' => env('APP_TIMEZONE', 'UTC'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Application Locale Configuration
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| The application locale determines the default locale that will be used
|
||||
| by Laravel's translation / localization methods. This option can be
|
||||
| set to any locale for which you plan to have translation strings.
|
||||
|
|
||||
*/
|
||||
|
||||
'locale' => env('APP_LOCALE', 'en'),
|
||||
|
||||
'fallback_locale' => env('APP_FALLBACK_LOCALE', 'en'),
|
||||
|
||||
'faker_locale' => env('APP_FAKER_LOCALE', 'en_US'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Encryption Key
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This key is utilized by Laravel's encryption services and should be set
|
||||
| to a random, 32 character string to ensure that all encrypted values
|
||||
| are secure. You should do this prior to deploying the application.
|
||||
|
|
||||
*/
|
||||
|
||||
'cipher' => 'AES-256-CBC',
|
||||
|
||||
'key' => env('APP_KEY'),
|
||||
|
||||
'previous_keys' => [
|
||||
...array_filter(
|
||||
explode(',', env('APP_PREVIOUS_KEYS', ''))
|
||||
),
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Maintenance Mode Driver
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| These configuration options determine the driver used to determine and
|
||||
| manage Laravel's "maintenance mode" status. The "cache" driver will
|
||||
| allow maintenance mode to be controlled across multiple machines.
|
||||
|
|
||||
| Supported drivers: "file", "cache"
|
||||
|
|
||||
*/
|
||||
|
||||
'maintenance' => [
|
||||
'driver' => env('APP_MAINTENANCE_DRIVER', 'file'),
|
||||
'store' => env('APP_MAINTENANCE_STORE', 'database'),
|
||||
],
|
||||
|
||||
];
|
||||
37
config/auth.php
Normal file
37
config/auth.php
Normal file
@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
'defaults' => [
|
||||
'guard' => env('AUTH_GUARD', 'web'),
|
||||
'passwords' => env('AUTH_PASSWORD_BROKER', 'users'),
|
||||
],
|
||||
|
||||
'guards' => [
|
||||
'web' => [
|
||||
'driver' => 'session',
|
||||
'provider' => 'users',
|
||||
],
|
||||
'api' => [
|
||||
'driver' => 'jwt',
|
||||
'provider' => 'users',
|
||||
],
|
||||
],
|
||||
|
||||
'providers' => [
|
||||
'users' => [
|
||||
'driver' => 'eloquent',
|
||||
'model' => env('AUTH_MODEL', App\Models\User::class),
|
||||
],
|
||||
],
|
||||
|
||||
'passwords' => [
|
||||
'users' => [
|
||||
'provider' => 'users',
|
||||
'table' => env('AUTH_PASSWORD_RESET_TOKEN_TABLE', 'password_reset_tokens'),
|
||||
'expire' => 60,
|
||||
'throttle' => 60,
|
||||
],
|
||||
],
|
||||
|
||||
'password_timeout' => env('AUTH_PASSWORD_TIMEOUT', 10800),
|
||||
];
|
||||
107
config/cache.php
Normal file
107
config/cache.php
Normal file
@ -0,0 +1,107 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
return [
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Default Cache Store
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This option controls the default cache store that will be used by the
|
||||
| framework. This connection is utilized if another isn't explicitly
|
||||
| specified when running a cache operation inside the application.
|
||||
|
|
||||
*/
|
||||
|
||||
'default' => env('CACHE_STORE', 'database'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Cache Stores
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Here you may define all of the cache "stores" for your application as
|
||||
| well as their drivers. You may even define multiple stores for the
|
||||
| same cache driver to group types of items stored in your caches.
|
||||
|
|
||||
| Supported drivers: "apc", "array", "database", "file", "memcached",
|
||||
| "redis", "dynamodb", "octane", "null"
|
||||
|
|
||||
*/
|
||||
|
||||
'stores' => [
|
||||
|
||||
'array' => [
|
||||
'driver' => 'array',
|
||||
'serialize' => false,
|
||||
],
|
||||
|
||||
'database' => [
|
||||
'driver' => 'database',
|
||||
'table' => env('DB_CACHE_TABLE', 'cache'),
|
||||
'connection' => env('DB_CACHE_CONNECTION', null),
|
||||
'lock_connection' => env('DB_CACHE_LOCK_CONNECTION', null),
|
||||
],
|
||||
|
||||
'file' => [
|
||||
'driver' => 'file',
|
||||
'path' => storage_path('framework/cache/data'),
|
||||
'lock_path' => storage_path('framework/cache/data'),
|
||||
],
|
||||
|
||||
'memcached' => [
|
||||
'driver' => 'memcached',
|
||||
'persistent_id' => env('MEMCACHED_PERSISTENT_ID'),
|
||||
'sasl' => [
|
||||
env('MEMCACHED_USERNAME'),
|
||||
env('MEMCACHED_PASSWORD'),
|
||||
],
|
||||
'options' => [
|
||||
// Memcached::OPT_CONNECT_TIMEOUT => 2000,
|
||||
],
|
||||
'servers' => [
|
||||
[
|
||||
'host' => env('MEMCACHED_HOST', '127.0.0.1'),
|
||||
'port' => env('MEMCACHED_PORT', 11211),
|
||||
'weight' => 100,
|
||||
],
|
||||
],
|
||||
],
|
||||
|
||||
'redis' => [
|
||||
'driver' => 'redis',
|
||||
'connection' => env('REDIS_CACHE_CONNECTION', 'cache'),
|
||||
'lock_connection' => env('REDIS_CACHE_LOCK_CONNECTION', 'default'),
|
||||
],
|
||||
|
||||
'dynamodb' => [
|
||||
'driver' => 'dynamodb',
|
||||
'key' => env('AWS_ACCESS_KEY_ID'),
|
||||
'secret' => env('AWS_SECRET_ACCESS_KEY'),
|
||||
'region' => env('AWS_DEFAULT_REGION', 'us-east-1'),
|
||||
'table' => env('DYNAMODB_CACHE_TABLE', 'cache'),
|
||||
'endpoint' => env('DYNAMODB_ENDPOINT'),
|
||||
],
|
||||
|
||||
'octane' => [
|
||||
'driver' => 'octane',
|
||||
],
|
||||
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Cache Key Prefix
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| When utilizing the APC, database, memcached, Redis, and DynamoDB cache
|
||||
| stores, there might be other applications using the same cache. For
|
||||
| that reason, you may prefix every cache key to avoid collisions.
|
||||
|
|
||||
*/
|
||||
|
||||
'prefix' => env('CACHE_PREFIX', Str::slug(env('APP_NAME', 'laravel'), '_').'_cache_'),
|
||||
|
||||
];
|
||||
170
config/database.php
Normal file
170
config/database.php
Normal file
@ -0,0 +1,170 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
return [
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Default Database Connection Name
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Here you may specify which of the database connections below you wish
|
||||
| to use as your default connection for database operations. This is
|
||||
| the connection which will be utilized unless another connection
|
||||
| is explicitly specified when you execute a query / statement.
|
||||
|
|
||||
*/
|
||||
|
||||
'default' => env('DB_CONNECTION', 'sqlite'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Database Connections
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Below are all of the database connections defined for your application.
|
||||
| An example configuration is provided for each database system which
|
||||
| is supported by Laravel. You're free to add / remove connections.
|
||||
|
|
||||
*/
|
||||
|
||||
'connections' => [
|
||||
|
||||
'sqlite' => [
|
||||
'driver' => 'sqlite',
|
||||
'url' => env('DB_URL'),
|
||||
'database' => env('DB_DATABASE', database_path('database.sqlite')),
|
||||
'prefix' => '',
|
||||
'foreign_key_constraints' => env('DB_FOREIGN_KEYS', true),
|
||||
],
|
||||
|
||||
'mysql' => [
|
||||
'driver' => 'mysql',
|
||||
'url' => env('DB_URL'),
|
||||
'host' => env('DB_HOST', '127.0.0.1'),
|
||||
'port' => env('DB_PORT', '3306'),
|
||||
'database' => env('DB_DATABASE', 'laravel'),
|
||||
'username' => env('DB_USERNAME', 'root'),
|
||||
'password' => env('DB_PASSWORD', ''),
|
||||
'unix_socket' => env('DB_SOCKET', ''),
|
||||
'charset' => env('DB_CHARSET', 'utf8mb4'),
|
||||
'collation' => env('DB_COLLATION', 'utf8mb4_0900_ai_ci'),
|
||||
'prefix' => '',
|
||||
'prefix_indexes' => true,
|
||||
'strict' => true,
|
||||
'engine' => null,
|
||||
'options' => extension_loaded('pdo_mysql') ? array_filter([
|
||||
PDO::MYSQL_ATTR_SSL_CA => env('MYSQL_ATTR_SSL_CA'),
|
||||
]) : [],
|
||||
],
|
||||
|
||||
'mariadb' => [
|
||||
'driver' => 'mariadb',
|
||||
'url' => env('DB_URL'),
|
||||
'host' => env('DB_HOST', '127.0.0.1'),
|
||||
'port' => env('DB_PORT', '3306'),
|
||||
'database' => env('DB_DATABASE', 'laravel'),
|
||||
'username' => env('DB_USERNAME', 'root'),
|
||||
'password' => env('DB_PASSWORD', ''),
|
||||
'unix_socket' => env('DB_SOCKET', ''),
|
||||
'charset' => env('DB_CHARSET', 'utf8mb4'),
|
||||
'collation' => env('DB_COLLATION', 'utf8mb4_uca1400_ai_ci'),
|
||||
'prefix' => '',
|
||||
'prefix_indexes' => true,
|
||||
'strict' => true,
|
||||
'engine' => null,
|
||||
'options' => extension_loaded('pdo_mysql') ? array_filter([
|
||||
PDO::MYSQL_ATTR_SSL_CA => env('MYSQL_ATTR_SSL_CA'),
|
||||
]) : [],
|
||||
],
|
||||
|
||||
'pgsql' => [
|
||||
'driver' => 'pgsql',
|
||||
'url' => env('DB_URL'),
|
||||
'host' => env('DB_HOST', '127.0.0.1'),
|
||||
'port' => env('DB_PORT', '5432'),
|
||||
'database' => env('DB_DATABASE', 'laravel'),
|
||||
'username' => env('DB_USERNAME', 'root'),
|
||||
'password' => env('DB_PASSWORD', ''),
|
||||
'charset' => env('DB_CHARSET', 'utf8'),
|
||||
'prefix' => '',
|
||||
'prefix_indexes' => true,
|
||||
'search_path' => 'public',
|
||||
'sslmode' => 'prefer',
|
||||
],
|
||||
|
||||
'sqlsrv' => [
|
||||
'driver' => 'sqlsrv',
|
||||
'url' => env('DB_URL'),
|
||||
'host' => env('DB_HOST', 'localhost'),
|
||||
'port' => env('DB_PORT', '1433'),
|
||||
'database' => env('DB_DATABASE', 'laravel'),
|
||||
'username' => env('DB_USERNAME', 'root'),
|
||||
'password' => env('DB_PASSWORD', ''),
|
||||
'charset' => env('DB_CHARSET', 'utf8'),
|
||||
'prefix' => '',
|
||||
'prefix_indexes' => true,
|
||||
// 'encrypt' => env('DB_ENCRYPT', 'yes'),
|
||||
// 'trust_server_certificate' => env('DB_TRUST_SERVER_CERTIFICATE', 'false'),
|
||||
],
|
||||
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Migration Repository Table
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This table keeps track of all the migrations that have already run for
|
||||
| your application. Using this information, we can determine which of
|
||||
| the migrations on disk haven't actually been run on the database.
|
||||
|
|
||||
*/
|
||||
|
||||
'migrations' => [
|
||||
'table' => 'migrations',
|
||||
'update_date_on_publish' => true,
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Redis Databases
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Redis is an open source, fast, and advanced key-value store that also
|
||||
| provides a richer body of commands than a typical key-value system
|
||||
| such as Memcached. You may define your connection settings here.
|
||||
|
|
||||
*/
|
||||
|
||||
'redis' => [
|
||||
|
||||
'client' => env('REDIS_CLIENT', 'phpredis'),
|
||||
|
||||
'options' => [
|
||||
'cluster' => env('REDIS_CLUSTER', 'redis'),
|
||||
'prefix' => env('REDIS_PREFIX', Str::slug(env('APP_NAME', 'laravel'), '_').'_database_'),
|
||||
],
|
||||
|
||||
'default' => [
|
||||
'url' => env('REDIS_URL'),
|
||||
'host' => env('REDIS_HOST', '127.0.0.1'),
|
||||
'username' => env('REDIS_USERNAME'),
|
||||
'password' => env('REDIS_PASSWORD'),
|
||||
'port' => env('REDIS_PORT', '6379'),
|
||||
'database' => env('REDIS_DB', '0'),
|
||||
],
|
||||
|
||||
'cache' => [
|
||||
'url' => env('REDIS_URL'),
|
||||
'host' => env('REDIS_HOST', '127.0.0.1'),
|
||||
'username' => env('REDIS_USERNAME'),
|
||||
'password' => env('REDIS_PASSWORD'),
|
||||
'port' => env('REDIS_PORT', '6379'),
|
||||
'database' => env('REDIS_CACHE_DB', '1'),
|
||||
],
|
||||
|
||||
],
|
||||
|
||||
];
|
||||
76
config/filesystems.php
Normal file
76
config/filesystems.php
Normal file
@ -0,0 +1,76 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Default Filesystem Disk
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Here you may specify the default filesystem disk that should be used
|
||||
| by the framework. The "local" disk, as well as a variety of cloud
|
||||
| based disks are available to your application for file storage.
|
||||
|
|
||||
*/
|
||||
|
||||
'default' => env('FILESYSTEM_DISK', 'local'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Filesystem Disks
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Below you may configure as many filesystem disks as necessary, and you
|
||||
| may even configure multiple disks for the same driver. Examples for
|
||||
| most supported storage drivers are configured here for reference.
|
||||
|
|
||||
| Supported Drivers: "local", "ftp", "sftp", "s3"
|
||||
|
|
||||
*/
|
||||
|
||||
'disks' => [
|
||||
|
||||
'local' => [
|
||||
'driver' => 'local',
|
||||
'root' => storage_path('app'),
|
||||
'throw' => false,
|
||||
],
|
||||
|
||||
'public' => [
|
||||
'driver' => 'local',
|
||||
'root' => storage_path('app/public'),
|
||||
'url' => env('APP_URL').'/storage',
|
||||
'visibility' => 'public',
|
||||
'throw' => false,
|
||||
],
|
||||
|
||||
's3' => [
|
||||
'driver' => 's3',
|
||||
'key' => env('AWS_ACCESS_KEY_ID'),
|
||||
'secret' => env('AWS_SECRET_ACCESS_KEY'),
|
||||
'region' => env('AWS_DEFAULT_REGION'),
|
||||
'bucket' => env('AWS_BUCKET'),
|
||||
'url' => env('AWS_URL'),
|
||||
'endpoint' => env('AWS_ENDPOINT'),
|
||||
'use_path_style_endpoint' => env('AWS_USE_PATH_STYLE_ENDPOINT', false),
|
||||
'throw' => false,
|
||||
],
|
||||
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Symbolic Links
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Here you may configure the symbolic links that will be created when the
|
||||
| `storage:link` Artisan command is executed. The array keys should be
|
||||
| the locations of the links and the values should be their targets.
|
||||
|
|
||||
*/
|
||||
|
||||
'links' => [
|
||||
public_path('storage') => storage_path('app/public'),
|
||||
],
|
||||
|
||||
];
|
||||
301
config/jwt.php
Normal file
301
config/jwt.php
Normal file
@ -0,0 +1,301 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of jwt-auth.
|
||||
*
|
||||
* (c) Sean Tymon <tymon148@gmail.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
return [
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| JWT Authentication Secret
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Don't forget to set this in your .env file, as it will be used to sign
|
||||
| your tokens. A helper command is provided for this:
|
||||
| `php artisan jwt:secret`
|
||||
|
|
||||
| Note: This will be used for Symmetric algorithms only (HMAC),
|
||||
| since RSA and ECDSA use a private/public key combo (See below).
|
||||
|
|
||||
*/
|
||||
|
||||
'secret' => env('JWT_SECRET'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| JWT Authentication Keys
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| The algorithm you are using, will determine whether your tokens are
|
||||
| signed with a random string (defined in `JWT_SECRET`) or using the
|
||||
| following public & private keys.
|
||||
|
|
||||
| Symmetric Algorithms:
|
||||
| HS256, HS384 & HS512 will use `JWT_SECRET`.
|
||||
|
|
||||
| Asymmetric Algorithms:
|
||||
| RS256, RS384 & RS512 / ES256, ES384 & ES512 will use the keys below.
|
||||
|
|
||||
*/
|
||||
|
||||
'keys' => [
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Public Key
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| A path or resource to your public key.
|
||||
|
|
||||
| E.g. 'file://path/to/public/key'
|
||||
|
|
||||
*/
|
||||
|
||||
'public' => env('JWT_PUBLIC_KEY'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Private Key
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| A path or resource to your private key.
|
||||
|
|
||||
| E.g. 'file://path/to/private/key'
|
||||
|
|
||||
*/
|
||||
|
||||
'private' => env('JWT_PRIVATE_KEY'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Passphrase
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| The passphrase for your private key. Can be null if none set.
|
||||
|
|
||||
*/
|
||||
|
||||
'passphrase' => env('JWT_PASSPHRASE'),
|
||||
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| JWT time to live
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Specify the length of time (in minutes) that the token will be valid for.
|
||||
| Defaults to 1 hour.
|
||||
|
|
||||
| You can also set this to null, to yield a never expiring token.
|
||||
| Some people may want this behaviour for e.g. a mobile app.
|
||||
| This is not particularly recommended, so make sure you have appropriate
|
||||
| systems in place to revoke the token if necessary.
|
||||
| Notice: If you set this to null you should remove 'exp' element from 'required_claims' list.
|
||||
|
|
||||
*/
|
||||
|
||||
'ttl' => env('JWT_TTL', 60),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Refresh time to live
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Specify the length of time (in minutes) that the token can be refreshed
|
||||
| within. I.E. The user can refresh their token within a 2 week window of
|
||||
| the original token being created until they must re-authenticate.
|
||||
| Defaults to 2 weeks.
|
||||
|
|
||||
| You can also set this to null, to yield an infinite refresh time.
|
||||
| Some may want this instead of never expiring tokens for e.g. a mobile app.
|
||||
| This is not particularly recommended, so make sure you have appropriate
|
||||
| systems in place to revoke the token if necessary.
|
||||
|
|
||||
*/
|
||||
|
||||
'refresh_ttl' => env('JWT_REFRESH_TTL', 20160),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| JWT hashing algorithm
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Specify the hashing algorithm that will be used to sign the token.
|
||||
|
|
||||
*/
|
||||
|
||||
'algo' => env('JWT_ALGO', Tymon\JWTAuth\Providers\JWT\Provider::ALGO_HS256),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Required Claims
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Specify the required claims that must exist in any token.
|
||||
| A TokenInvalidException will be thrown if any of these claims are not
|
||||
| present in the payload.
|
||||
|
|
||||
*/
|
||||
|
||||
'required_claims' => [
|
||||
'iss',
|
||||
'iat',
|
||||
'exp',
|
||||
'nbf',
|
||||
'sub',
|
||||
'jti',
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Persistent Claims
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Specify the claim keys to be persisted when refreshing a token.
|
||||
| `sub` and `iat` will automatically be persisted, in
|
||||
| addition to the these claims.
|
||||
|
|
||||
| Note: If a claim does not exist then it will be ignored.
|
||||
|
|
||||
*/
|
||||
|
||||
'persistent_claims' => [
|
||||
// 'foo',
|
||||
// 'bar',
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Lock Subject
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This will determine whether a `prv` claim is automatically added to
|
||||
| the token. The purpose of this is to ensure that if you have multiple
|
||||
| authentication models e.g. `App\User` & `App\OtherPerson`, then we
|
||||
| should prevent one authentication request from impersonating another,
|
||||
| if 2 tokens happen to have the same id across the 2 different models.
|
||||
|
|
||||
| Under specific circumstances, you may want to disable this behaviour
|
||||
| e.g. if you only have one authentication model, then you would save
|
||||
| a little on token size.
|
||||
|
|
||||
*/
|
||||
|
||||
'lock_subject' => true,
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Leeway
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This property gives the jwt timestamp claims some "leeway".
|
||||
| Meaning that if you have any unavoidable slight clock skew on
|
||||
| any of your servers then this will afford you some level of cushioning.
|
||||
|
|
||||
| This applies to the claims `iat`, `nbf` and `exp`.
|
||||
|
|
||||
| Specify in seconds - only if you know you need it.
|
||||
|
|
||||
*/
|
||||
|
||||
'leeway' => env('JWT_LEEWAY', 0),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Blacklist Enabled
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| In order to invalidate tokens, you must have the blacklist enabled.
|
||||
| If you do not want or need this functionality, then set this to false.
|
||||
|
|
||||
*/
|
||||
|
||||
'blacklist_enabled' => env('JWT_BLACKLIST_ENABLED', true),
|
||||
|
||||
/*
|
||||
| -------------------------------------------------------------------------
|
||||
| Blacklist Grace Period
|
||||
| -------------------------------------------------------------------------
|
||||
|
|
||||
| When multiple concurrent requests are made with the same JWT,
|
||||
| it is possible that some of them fail, due to token regeneration
|
||||
| on every request.
|
||||
|
|
||||
| Set grace period in seconds to prevent parallel request failure.
|
||||
|
|
||||
*/
|
||||
|
||||
'blacklist_grace_period' => env('JWT_BLACKLIST_GRACE_PERIOD', 0),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Cookies encryption
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| By default Laravel encrypt cookies for security reason.
|
||||
| If you decide to not decrypt cookies, you will have to configure Laravel
|
||||
| to not encrypt your cookie token by adding its name into the $except
|
||||
| array available in the middleware "EncryptCookies" provided by Laravel.
|
||||
| see https://laravel.com/docs/master/responses#cookies-and-encryption
|
||||
| for details.
|
||||
|
|
||||
| Set it to true if you want to decrypt cookies.
|
||||
|
|
||||
*/
|
||||
|
||||
'decrypt_cookies' => false,
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Providers
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Specify the various providers used throughout the package.
|
||||
|
|
||||
*/
|
||||
|
||||
'providers' => [
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| JWT Provider
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Specify the provider that is used to create and decode the tokens.
|
||||
|
|
||||
*/
|
||||
|
||||
'jwt' => Tymon\JWTAuth\Providers\JWT\Lcobucci::class,
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Authentication Provider
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Specify the provider that is used to authenticate users.
|
||||
|
|
||||
*/
|
||||
|
||||
'auth' => Tymon\JWTAuth\Providers\Auth\Illuminate::class,
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Storage Provider
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Specify the provider that is used to store tokens in the blacklist.
|
||||
|
|
||||
*/
|
||||
|
||||
'storage' => Tymon\JWTAuth\Providers\Storage\Illuminate::class,
|
||||
|
||||
],
|
||||
|
||||
];
|
||||
132
config/logging.php
Normal file
132
config/logging.php
Normal file
@ -0,0 +1,132 @@
|
||||
<?php
|
||||
|
||||
use Monolog\Handler\NullHandler;
|
||||
use Monolog\Handler\StreamHandler;
|
||||
use Monolog\Handler\SyslogUdpHandler;
|
||||
use Monolog\Processor\PsrLogMessageProcessor;
|
||||
|
||||
return [
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Default Log Channel
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This option defines the default log channel that is utilized to write
|
||||
| messages to your logs. The value provided here should match one of
|
||||
| the channels present in the list of "channels" configured below.
|
||||
|
|
||||
*/
|
||||
|
||||
'default' => env('LOG_CHANNEL', 'stack'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Deprecations Log Channel
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This option controls the log channel that should be used to log warnings
|
||||
| regarding deprecated PHP and library features. This allows you to get
|
||||
| your application ready for upcoming major versions of dependencies.
|
||||
|
|
||||
*/
|
||||
|
||||
'deprecations' => [
|
||||
'channel' => env('LOG_DEPRECATIONS_CHANNEL', 'null'),
|
||||
'trace' => env('LOG_DEPRECATIONS_TRACE', false),
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Log Channels
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Here you may configure the log channels for your application. Laravel
|
||||
| utilizes the Monolog PHP logging library, which includes a variety
|
||||
| of powerful log handlers and formatters that you're free to use.
|
||||
|
|
||||
| Available Drivers: "single", "daily", "slack", "syslog",
|
||||
| "errorlog", "monolog", "custom", "stack"
|
||||
|
|
||||
*/
|
||||
|
||||
'channels' => [
|
||||
|
||||
'stack' => [
|
||||
'driver' => 'stack',
|
||||
'channels' => explode(',', env('LOG_STACK', 'single')),
|
||||
'ignore_exceptions' => false,
|
||||
],
|
||||
|
||||
'single' => [
|
||||
'driver' => 'single',
|
||||
'path' => storage_path('logs/laravel.log'),
|
||||
'level' => env('LOG_LEVEL', 'debug'),
|
||||
'replace_placeholders' => true,
|
||||
],
|
||||
|
||||
'daily' => [
|
||||
'driver' => 'daily',
|
||||
'path' => storage_path('logs/laravel.log'),
|
||||
'level' => env('LOG_LEVEL', 'debug'),
|
||||
'days' => env('LOG_DAILY_DAYS', 14),
|
||||
'replace_placeholders' => true,
|
||||
],
|
||||
|
||||
'slack' => [
|
||||
'driver' => 'slack',
|
||||
'url' => env('LOG_SLACK_WEBHOOK_URL'),
|
||||
'username' => env('LOG_SLACK_USERNAME', 'Laravel Log'),
|
||||
'emoji' => env('LOG_SLACK_EMOJI', ':boom:'),
|
||||
'level' => env('LOG_LEVEL', 'critical'),
|
||||
'replace_placeholders' => true,
|
||||
],
|
||||
|
||||
'papertrail' => [
|
||||
'driver' => 'monolog',
|
||||
'level' => env('LOG_LEVEL', 'debug'),
|
||||
'handler' => env('LOG_PAPERTRAIL_HANDLER', SyslogUdpHandler::class),
|
||||
'handler_with' => [
|
||||
'host' => env('PAPERTRAIL_URL'),
|
||||
'port' => env('PAPERTRAIL_PORT'),
|
||||
'connectionString' => 'tls://'.env('PAPERTRAIL_URL').':'.env('PAPERTRAIL_PORT'),
|
||||
],
|
||||
'processors' => [PsrLogMessageProcessor::class],
|
||||
],
|
||||
|
||||
'stderr' => [
|
||||
'driver' => 'monolog',
|
||||
'level' => env('LOG_LEVEL', 'debug'),
|
||||
'handler' => StreamHandler::class,
|
||||
'formatter' => env('LOG_STDERR_FORMATTER'),
|
||||
'with' => [
|
||||
'stream' => 'php://stderr',
|
||||
],
|
||||
'processors' => [PsrLogMessageProcessor::class],
|
||||
],
|
||||
|
||||
'syslog' => [
|
||||
'driver' => 'syslog',
|
||||
'level' => env('LOG_LEVEL', 'debug'),
|
||||
'facility' => env('LOG_SYSLOG_FACILITY', LOG_USER),
|
||||
'replace_placeholders' => true,
|
||||
],
|
||||
|
||||
'errorlog' => [
|
||||
'driver' => 'errorlog',
|
||||
'level' => env('LOG_LEVEL', 'debug'),
|
||||
'replace_placeholders' => true,
|
||||
],
|
||||
|
||||
'null' => [
|
||||
'driver' => 'monolog',
|
||||
'handler' => NullHandler::class,
|
||||
],
|
||||
|
||||
'emergency' => [
|
||||
'path' => storage_path('logs/laravel.log'),
|
||||
],
|
||||
|
||||
],
|
||||
|
||||
];
|
||||
103
config/mail.php
Normal file
103
config/mail.php
Normal file
@ -0,0 +1,103 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Default Mailer
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This option controls the default mailer that is used to send all email
|
||||
| messages unless another mailer is explicitly specified when sending
|
||||
| the message. All additional mailers can be configured within the
|
||||
| "mailers" array. Examples of each type of mailer are provided.
|
||||
|
|
||||
*/
|
||||
|
||||
'default' => env('MAIL_MAILER', 'log'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Mailer Configurations
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Here you may configure all of the mailers used by your application plus
|
||||
| their respective settings. Several examples have been configured for
|
||||
| you and you are free to add your own as your application requires.
|
||||
|
|
||||
| Laravel supports a variety of mail "transport" drivers that can be used
|
||||
| when delivering an email. You may specify which one you're using for
|
||||
| your mailers below. You may also add additional mailers if needed.
|
||||
|
|
||||
| Supported: "smtp", "sendmail", "mailgun", "ses", "ses-v2",
|
||||
| "postmark", "log", "array", "failover", "roundrobin"
|
||||
|
|
||||
*/
|
||||
|
||||
'mailers' => [
|
||||
|
||||
'smtp' => [
|
||||
'transport' => 'smtp',
|
||||
'url' => env('MAIL_URL'),
|
||||
'host' => env('MAIL_HOST', '127.0.0.1'),
|
||||
'port' => env('MAIL_PORT', 2525),
|
||||
'encryption' => env('MAIL_ENCRYPTION', 'tls'),
|
||||
'username' => env('MAIL_USERNAME'),
|
||||
'password' => env('MAIL_PASSWORD'),
|
||||
'timeout' => null,
|
||||
'local_domain' => env('MAIL_EHLO_DOMAIN'),
|
||||
],
|
||||
|
||||
'ses' => [
|
||||
'transport' => 'ses',
|
||||
],
|
||||
|
||||
'postmark' => [
|
||||
'transport' => 'postmark',
|
||||
// 'message_stream_id' => env('POSTMARK_MESSAGE_STREAM_ID'),
|
||||
// 'client' => [
|
||||
// 'timeout' => 5,
|
||||
// ],
|
||||
],
|
||||
|
||||
'sendmail' => [
|
||||
'transport' => 'sendmail',
|
||||
'path' => env('MAIL_SENDMAIL_PATH', '/usr/sbin/sendmail -bs -i'),
|
||||
],
|
||||
|
||||
'log' => [
|
||||
'transport' => 'log',
|
||||
'channel' => env('MAIL_LOG_CHANNEL'),
|
||||
],
|
||||
|
||||
'array' => [
|
||||
'transport' => 'array',
|
||||
],
|
||||
|
||||
'failover' => [
|
||||
'transport' => 'failover',
|
||||
'mailers' => [
|
||||
'smtp',
|
||||
'log',
|
||||
],
|
||||
],
|
||||
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Global "From" Address
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| You may wish for all emails sent by your application to be sent from
|
||||
| the same address. Here you may specify a name and address that is
|
||||
| used globally for all emails that are sent by your application.
|
||||
|
|
||||
*/
|
||||
|
||||
'from' => [
|
||||
'address' => env('MAIL_FROM_ADDRESS', 'hello@example.com'),
|
||||
'name' => env('MAIL_FROM_NAME', 'Example'),
|
||||
],
|
||||
|
||||
];
|
||||
186
config/permission.php
Normal file
186
config/permission.php
Normal file
@ -0,0 +1,186 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
|
||||
'models' => [
|
||||
|
||||
/*
|
||||
* When using the "HasPermissions" trait from this package, we need to know which
|
||||
* Eloquent model should be used to retrieve your permissions. Of course, it
|
||||
* is often just the "Permission" model but you may use whatever you like.
|
||||
*
|
||||
* The model you want to use as a Permission model needs to implement the
|
||||
* `Spatie\Permission\Contracts\Permission` contract.
|
||||
*/
|
||||
|
||||
'permission' => Spatie\Permission\Models\Permission::class,
|
||||
|
||||
/*
|
||||
* When using the "HasRoles" trait from this package, we need to know which
|
||||
* Eloquent model should be used to retrieve your roles. Of course, it
|
||||
* is often just the "Role" model but you may use whatever you like.
|
||||
*
|
||||
* The model you want to use as a Role model needs to implement the
|
||||
* `Spatie\Permission\Contracts\Role` contract.
|
||||
*/
|
||||
|
||||
'role' => Spatie\Permission\Models\Role::class,
|
||||
|
||||
],
|
||||
|
||||
'table_names' => [
|
||||
|
||||
/*
|
||||
* When using the "HasRoles" trait from this package, we need to know which
|
||||
* table should be used to retrieve your roles. We have chosen a basic
|
||||
* default value but you may easily change it to any table you like.
|
||||
*/
|
||||
|
||||
'roles' => 'roles',
|
||||
|
||||
/*
|
||||
* When using the "HasPermissions" trait from this package, we need to know which
|
||||
* table should be used to retrieve your permissions. We have chosen a basic
|
||||
* default value but you may easily change it to any table you like.
|
||||
*/
|
||||
|
||||
'permissions' => 'permissions',
|
||||
|
||||
/*
|
||||
* When using the "HasPermissions" trait from this package, we need to know which
|
||||
* table should be used to retrieve your models permissions. We have chosen a
|
||||
* basic default value but you may easily change it to any table you like.
|
||||
*/
|
||||
|
||||
'model_has_permissions' => 'model_has_permissions',
|
||||
|
||||
/*
|
||||
* When using the "HasRoles" trait from this package, we need to know which
|
||||
* table should be used to retrieve your models roles. We have chosen a
|
||||
* basic default value but you may easily change it to any table you like.
|
||||
*/
|
||||
|
||||
'model_has_roles' => 'model_has_roles',
|
||||
|
||||
/*
|
||||
* When using the "HasRoles" trait from this package, we need to know which
|
||||
* table should be used to retrieve your roles permissions. We have chosen a
|
||||
* basic default value but you may easily change it to any table you like.
|
||||
*/
|
||||
|
||||
'role_has_permissions' => 'role_has_permissions',
|
||||
],
|
||||
|
||||
'column_names' => [
|
||||
/*
|
||||
* Change this if you want to name the related pivots other than defaults
|
||||
*/
|
||||
'role_pivot_key' => null, //default 'role_id',
|
||||
'permission_pivot_key' => null, //default 'permission_id',
|
||||
|
||||
/*
|
||||
* Change this if you want to name the related model primary key other than
|
||||
* `model_id`.
|
||||
*
|
||||
* For example, this would be nice if your primary keys are all UUIDs. In
|
||||
* that case, name this `model_uuid`.
|
||||
*/
|
||||
|
||||
'model_morph_key' => 'model_id',
|
||||
|
||||
/*
|
||||
* Change this if you want to use the teams feature and your related model's
|
||||
* foreign key is other than `team_id`.
|
||||
*/
|
||||
|
||||
'team_foreign_key' => 'team_id',
|
||||
],
|
||||
|
||||
/*
|
||||
* When set to true, the method for checking permissions will be registered on the gate.
|
||||
* Set this to false if you want to implement custom logic for checking permissions.
|
||||
*/
|
||||
|
||||
'register_permission_check_method' => true,
|
||||
|
||||
/*
|
||||
* When set to true, Laravel\Octane\Events\OperationTerminated event listener will be registered
|
||||
* this will refresh permissions on every TickTerminated, TaskTerminated and RequestTerminated
|
||||
* NOTE: This should not be needed in most cases, but an Octane/Vapor combination benefited from it.
|
||||
*/
|
||||
'register_octane_reset_listener' => false,
|
||||
|
||||
/*
|
||||
* Teams Feature.
|
||||
* When set to true the package implements teams using the 'team_foreign_key'.
|
||||
* If you want the migrations to register the 'team_foreign_key', you must
|
||||
* set this to true before doing the migration.
|
||||
* If you already did the migration then you must make a new migration to also
|
||||
* add 'team_foreign_key' to 'roles', 'model_has_roles', and 'model_has_permissions'
|
||||
* (view the latest version of this package's migration file)
|
||||
*/
|
||||
|
||||
'teams' => false,
|
||||
|
||||
/*
|
||||
* Passport Client Credentials Grant
|
||||
* When set to true the package will use Passports Client to check permissions
|
||||
*/
|
||||
|
||||
'use_passport_client_credentials' => false,
|
||||
|
||||
/*
|
||||
* When set to true, the required permission names are added to exception messages.
|
||||
* This could be considered an information leak in some contexts, so the default
|
||||
* setting is false here for optimum safety.
|
||||
*/
|
||||
|
||||
'display_permission_in_exception' => false,
|
||||
|
||||
/*
|
||||
* When set to true, the required role names are added to exception messages.
|
||||
* This could be considered an information leak in some contexts, so the default
|
||||
* setting is false here for optimum safety.
|
||||
*/
|
||||
|
||||
'display_role_in_exception' => false,
|
||||
|
||||
/*
|
||||
* By default wildcard permission lookups are disabled.
|
||||
* See documentation to understand supported syntax.
|
||||
*/
|
||||
|
||||
'enable_wildcard_permission' => false,
|
||||
|
||||
/*
|
||||
* The class to use for interpreting wildcard permissions.
|
||||
* If you need to modify delimiters, override the class and specify its name here.
|
||||
*/
|
||||
// 'permission.wildcard_permission' => Spatie\Permission\WildcardPermission::class,
|
||||
|
||||
/* Cache-specific settings */
|
||||
|
||||
'cache' => [
|
||||
|
||||
/*
|
||||
* By default all permissions are cached for 24 hours to speed up performance.
|
||||
* When permissions or roles are updated the cache is flushed automatically.
|
||||
*/
|
||||
|
||||
'expiration_time' => \DateInterval::createFromDateString('24 hours'),
|
||||
|
||||
/*
|
||||
* The cache key used to store all permissions.
|
||||
*/
|
||||
|
||||
'key' => 'spatie.permission.cache',
|
||||
|
||||
/*
|
||||
* You may optionally indicate a specific cache driver to use for permission and
|
||||
* role caching using any of the `store` drivers listed in the cache.php config
|
||||
* file. Using 'default' here means to use the `default` set in cache.php.
|
||||
*/
|
||||
|
||||
'store' => 'default',
|
||||
],
|
||||
];
|
||||
112
config/queue.php
Normal file
112
config/queue.php
Normal file
@ -0,0 +1,112 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Default Queue Connection Name
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Laravel's queue supports a variety of backends via a single, unified
|
||||
| API, giving you convenient access to each backend using identical
|
||||
| syntax for each. The default queue connection is defined below.
|
||||
|
|
||||
*/
|
||||
|
||||
'default' => env('QUEUE_CONNECTION', 'database'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Queue Connections
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Here you may configure the connection options for every queue backend
|
||||
| used by your application. An example configuration is provided for
|
||||
| each backend supported by Laravel. You're also free to add more.
|
||||
|
|
||||
| Drivers: "sync", "database", "beanstalkd", "sqs", "redis", "null"
|
||||
|
|
||||
*/
|
||||
|
||||
'connections' => [
|
||||
|
||||
'sync' => [
|
||||
'driver' => 'sync',
|
||||
],
|
||||
|
||||
'database' => [
|
||||
'driver' => 'database',
|
||||
'connection' => env('DB_QUEUE_CONNECTION', null),
|
||||
'table' => env('DB_QUEUE_TABLE', 'jobs'),
|
||||
'queue' => env('DB_QUEUE', 'default'),
|
||||
'retry_after' => env('DB_QUEUE_RETRY_AFTER', 90),
|
||||
'after_commit' => false,
|
||||
],
|
||||
|
||||
'beanstalkd' => [
|
||||
'driver' => 'beanstalkd',
|
||||
'host' => env('BEANSTALKD_QUEUE_HOST', 'localhost'),
|
||||
'queue' => env('BEANSTALKD_QUEUE', 'default'),
|
||||
'retry_after' => env('BEANSTALKD_QUEUE_RETRY_AFTER', 90),
|
||||
'block_for' => 0,
|
||||
'after_commit' => false,
|
||||
],
|
||||
|
||||
'sqs' => [
|
||||
'driver' => 'sqs',
|
||||
'key' => env('AWS_ACCESS_KEY_ID'),
|
||||
'secret' => env('AWS_SECRET_ACCESS_KEY'),
|
||||
'prefix' => env('SQS_PREFIX', 'https://sqs.us-east-1.amazonaws.com/your-account-id'),
|
||||
'queue' => env('SQS_QUEUE', 'default'),
|
||||
'suffix' => env('SQS_SUFFIX'),
|
||||
'region' => env('AWS_DEFAULT_REGION', 'us-east-1'),
|
||||
'after_commit' => false,
|
||||
],
|
||||
|
||||
'redis' => [
|
||||
'driver' => 'redis',
|
||||
'connection' => env('REDIS_QUEUE_CONNECTION', 'default'),
|
||||
'queue' => env('REDIS_QUEUE', 'default'),
|
||||
'retry_after' => env('REDIS_QUEUE_RETRY_AFTER', 90),
|
||||
'block_for' => null,
|
||||
'after_commit' => false,
|
||||
],
|
||||
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Job Batching
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| The following options configure the database and table that store job
|
||||
| batching information. These options can be updated to any database
|
||||
| connection and table which has been defined by your application.
|
||||
|
|
||||
*/
|
||||
|
||||
'batching' => [
|
||||
'database' => env('DB_CONNECTION', 'sqlite'),
|
||||
'table' => 'job_batches',
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Failed Queue Jobs
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| These options configure the behavior of failed queue job logging so you
|
||||
| can control how and where failed jobs are stored. Laravel ships with
|
||||
| support for storing failed jobs in a simple file or in a database.
|
||||
|
|
||||
| Supported drivers: "database-uuids", "dynamodb", "file", "null"
|
||||
|
|
||||
*/
|
||||
|
||||
'failed' => [
|
||||
'driver' => env('QUEUE_FAILED_DRIVER', 'database-uuids'),
|
||||
'database' => env('DB_CONNECTION', 'sqlite'),
|
||||
'table' => 'failed_jobs',
|
||||
],
|
||||
|
||||
];
|
||||
61
config/services.php
Normal file
61
config/services.php
Normal file
@ -0,0 +1,61 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Third Party Services
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This file is for storing the credentials for third party services such
|
||||
| as Mailgun, Postmark, AWS and more. This file provides the de facto
|
||||
| location for this type of information, allowing packages to have
|
||||
| a conventional file to locate the various service credentials.
|
||||
|
|
||||
*/
|
||||
|
||||
'postmark' => [
|
||||
'token' => env('POSTMARK_TOKEN'),
|
||||
],
|
||||
|
||||
'ses' => [
|
||||
'key' => env('AWS_ACCESS_KEY_ID'),
|
||||
'secret' => env('AWS_SECRET_ACCESS_KEY'),
|
||||
'region' => env('AWS_DEFAULT_REGION', 'us-east-1'),
|
||||
],
|
||||
|
||||
'slack' => [
|
||||
'notifications' => [
|
||||
'bot_user_oauth_token' => env('SLACK_BOT_USER_OAUTH_TOKEN'),
|
||||
'channel' => env('SLACK_BOT_USER_DEFAULT_CHANNEL'),
|
||||
],
|
||||
],
|
||||
|
||||
'bastion_token' => [
|
||||
'base_url' => env('BASTION_TOKEN_API_BASE_URL'),
|
||||
'submit_endpoint' => env('BASTION_TOKEN_SUBMIT_ENDPOINT', '/bastion_token'),
|
||||
'status_endpoint' => env('BASTION_TOKEN_STATUS_ENDPOINT', '/bastion_token/{task_id}'),
|
||||
'timeout' => (int) env('BASTION_TOKEN_TIMEOUT', 30),
|
||||
'poll_attempts' => (int) env('BASTION_TOKEN_POLL_ATTEMPTS', 60),
|
||||
'poll_interval_ms' => (int) env('BASTION_TOKEN_POLL_INTERVAL_MS', 500),
|
||||
'task_ttl_seconds' => (int) env('BASTION_TOKEN_TASK_TTL_SECONDS', 1800),
|
||||
],
|
||||
|
||||
'bastion_access' => [
|
||||
'base_url' => env('BASTION_ACCESS_BASE_URL', 'https://172.16.254.2'),
|
||||
'sso_endpoint' => env('BASTION_ACCESS_SSO_ENDPOINT', '/usmapi/v1/operation/custom/sso'),
|
||||
'timeout' => (int) env('BASTION_ACCESS_TIMEOUT', 30),
|
||||
'verify_ssl' => (bool) env('BASTION_ACCESS_VERIFY_SSL', false),
|
||||
'protocol_ids' => [
|
||||
'ssh' => (int) env('BASTION_PROTOCOL_ID_SSH', 2),
|
||||
'sftp' => (int) env('BASTION_PROTOCOL_ID_SFTP', 4),
|
||||
'rdp' => (int) env('BASTION_PROTOCOL_ID_RDP', 3),
|
||||
],
|
||||
],
|
||||
|
||||
'ops_client' => [
|
||||
'ipv4' => env('OPS_CLIENT_IPV4', '172.16.1.2'),
|
||||
'asset_ipv4' => env('OPS_CLIENT_ASSET_IPV4', '0.0.0.0'),
|
||||
],
|
||||
|
||||
];
|
||||
218
config/session.php
Normal file
218
config/session.php
Normal file
@ -0,0 +1,218 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
return [
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Default Session Driver
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This option determines the default session driver that is utilized for
|
||||
| incoming requests. Laravel supports a variety of storage options to
|
||||
| persist session data. Database storage is a great default choice.
|
||||
|
|
||||
| Supported: "file", "cookie", "database", "apc",
|
||||
| "memcached", "redis", "dynamodb", "array"
|
||||
|
|
||||
*/
|
||||
|
||||
'driver' => env('SESSION_DRIVER', 'database'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Session Lifetime
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Here you may specify the number of minutes that you wish the session
|
||||
| to be allowed to remain idle before it expires. If you want them
|
||||
| to expire immediately when the browser is closed then you may
|
||||
| indicate that via the expire_on_close configuration option.
|
||||
|
|
||||
*/
|
||||
|
||||
'lifetime' => env('SESSION_LIFETIME', 120),
|
||||
|
||||
'expire_on_close' => env('SESSION_EXPIRE_ON_CLOSE', false),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Session Encryption
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This option allows you to easily specify that all of your session data
|
||||
| should be encrypted before it's stored. All encryption is performed
|
||||
| automatically by Laravel and you may use the session like normal.
|
||||
|
|
||||
*/
|
||||
|
||||
'encrypt' => env('SESSION_ENCRYPT', false),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Session File Location
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| When utilizing the "file" session driver, the session files are placed
|
||||
| on disk. The default storage location is defined here; however, you
|
||||
| are free to provide another location where they should be stored.
|
||||
|
|
||||
*/
|
||||
|
||||
'files' => storage_path('framework/sessions'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Session Database Connection
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| When using the "database" or "redis" session drivers, you may specify a
|
||||
| connection that should be used to manage these sessions. This should
|
||||
| correspond to a connection in your database configuration options.
|
||||
|
|
||||
*/
|
||||
|
||||
'connection' => env('SESSION_CONNECTION'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Session Database Table
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| When using the "database" session driver, you may specify the table to
|
||||
| be used to store sessions. Of course, a sensible default is defined
|
||||
| for you; however, you're welcome to change this to another table.
|
||||
|
|
||||
*/
|
||||
|
||||
'table' => env('SESSION_TABLE', 'sessions'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Session Cache Store
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| When using one of the framework's cache driven session backends, you may
|
||||
| define the cache store which should be used to store the session data
|
||||
| between requests. This must match one of your defined cache stores.
|
||||
|
|
||||
| Affects: "apc", "dynamodb", "memcached", "redis"
|
||||
|
|
||||
*/
|
||||
|
||||
'store' => env('SESSION_STORE'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Session Sweeping Lottery
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Some session drivers must manually sweep their storage location to get
|
||||
| rid of old sessions from storage. Here are the chances that it will
|
||||
| happen on a given request. By default, the odds are 2 out of 100.
|
||||
|
|
||||
*/
|
||||
|
||||
'lottery' => [2, 100],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Session Cookie Name
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Here you may change the name of the session cookie that is created by
|
||||
| the framework. Typically, you should not need to change this value
|
||||
| since doing so does not grant a meaningful security improvement.
|
||||
|
|
||||
|
|
||||
*/
|
||||
|
||||
'cookie' => env(
|
||||
'SESSION_COOKIE',
|
||||
Str::slug(env('APP_NAME', 'laravel'), '_').'_session'
|
||||
),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Session Cookie Path
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| The session cookie path determines the path for which the cookie will
|
||||
| be regarded as available. Typically, this will be the root path of
|
||||
| your application, but you're free to change this when necessary.
|
||||
|
|
||||
*/
|
||||
|
||||
'path' => env('SESSION_PATH', '/'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Session Cookie Domain
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This value determines the domain and subdomains the session cookie is
|
||||
| available to. By default, the cookie will be available to the root
|
||||
| domain and all subdomains. Typically, this shouldn't be changed.
|
||||
|
|
||||
*/
|
||||
|
||||
'domain' => env('SESSION_DOMAIN'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| HTTPS Only Cookies
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| By setting this option to true, session cookies will only be sent back
|
||||
| to the server if the browser has a HTTPS connection. This will keep
|
||||
| the cookie from being sent to you when it can't be done securely.
|
||||
|
|
||||
*/
|
||||
|
||||
'secure' => env('SESSION_SECURE_COOKIE'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| HTTP Access Only
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Setting this value to true will prevent JavaScript from accessing the
|
||||
| value of the cookie and the cookie will only be accessible through
|
||||
| the HTTP protocol. It's unlikely you should disable this option.
|
||||
|
|
||||
*/
|
||||
|
||||
'http_only' => env('SESSION_HTTP_ONLY', true),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Same-Site Cookies
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This option determines how your cookies behave when cross-site requests
|
||||
| take place, and can be used to mitigate CSRF attacks. By default, we
|
||||
| will set this value to "lax" to permit secure cross-site requests.
|
||||
|
|
||||
| See: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie#samesitesamesite-value
|
||||
|
|
||||
| Supported: "lax", "strict", "none", null
|
||||
|
|
||||
*/
|
||||
|
||||
'same_site' => env('SESSION_SAME_SITE', 'lax'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Partitioned Cookies
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Setting this value to true will tie the cookie to the top-level site for
|
||||
| a cross-site context. Partitioned cookies are accepted by the browser
|
||||
| when flagged "secure" and the Same-Site attribute is set to "none".
|
||||
|
|
||||
*/
|
||||
|
||||
'partitioned' => env('SESSION_PARTITIONED_COOKIE', false),
|
||||
|
||||
];
|
||||
1
database/.gitignore
vendored
Normal file
1
database/.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
*.sqlite*
|
||||
31
database/factories/UserFactory.php
Normal file
31
database/factories/UserFactory.php
Normal file
@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
namespace Database\Factories;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class UserFactory extends Factory
|
||||
{
|
||||
protected static ?string $password;
|
||||
|
||||
public function definition(): array
|
||||
{
|
||||
return [
|
||||
'nickname' => fake()->name(),
|
||||
'email' => fake()->unique()->safeEmail(),
|
||||
'phone' => fake()->optional()->phoneNumber(),
|
||||
'email_verified_at' => now(),
|
||||
'password' => static::$password ??= Hash::make('password'),
|
||||
'remember_token' => Str::random(10),
|
||||
];
|
||||
}
|
||||
|
||||
public function unverified(): static
|
||||
{
|
||||
return $this->state(fn (array $attributes) => [
|
||||
'email_verified_at' => null,
|
||||
]);
|
||||
}
|
||||
}
|
||||
46
database/migrations/0001_01_01_000000_create_users_table.php
Normal file
46
database/migrations/0001_01_01_000000_create_users_table.php
Normal file
@ -0,0 +1,46 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('users', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('nickname');
|
||||
$table->string('email')->unique();
|
||||
$table->string('phone')->nullable()->unique();
|
||||
$table->timestamp('email_verified_at')->nullable();
|
||||
$table->string('password');
|
||||
$table->rememberToken();
|
||||
$table->timestamps();
|
||||
|
||||
$table->index('nickname');
|
||||
});
|
||||
|
||||
Schema::create('password_reset_tokens', function (Blueprint $table) {
|
||||
$table->string('email')->primary();
|
||||
$table->string('token');
|
||||
$table->timestamp('created_at')->nullable();
|
||||
});
|
||||
|
||||
Schema::create('sessions', function (Blueprint $table) {
|
||||
$table->string('id')->primary();
|
||||
$table->foreignId('user_id')->nullable()->index();
|
||||
$table->string('ip_address', 45)->nullable();
|
||||
$table->text('user_agent')->nullable();
|
||||
$table->longText('payload');
|
||||
$table->integer('last_activity')->index();
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('users');
|
||||
Schema::dropIfExists('password_reset_tokens');
|
||||
Schema::dropIfExists('sessions');
|
||||
}
|
||||
};
|
||||
35
database/migrations/0001_01_01_000001_create_cache_table.php
Normal file
35
database/migrations/0001_01_01_000001_create_cache_table.php
Normal file
@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('cache', function (Blueprint $table) {
|
||||
$table->string('key')->primary();
|
||||
$table->mediumText('value');
|
||||
$table->integer('expiration');
|
||||
});
|
||||
|
||||
Schema::create('cache_locks', function (Blueprint $table) {
|
||||
$table->string('key')->primary();
|
||||
$table->string('owner');
|
||||
$table->integer('expiration');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('cache');
|
||||
Schema::dropIfExists('cache_locks');
|
||||
}
|
||||
};
|
||||
57
database/migrations/0001_01_01_000002_create_jobs_table.php
Normal file
57
database/migrations/0001_01_01_000002_create_jobs_table.php
Normal file
@ -0,0 +1,57 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('jobs', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('queue')->index();
|
||||
$table->longText('payload');
|
||||
$table->unsignedTinyInteger('attempts');
|
||||
$table->unsignedInteger('reserved_at')->nullable();
|
||||
$table->unsignedInteger('available_at');
|
||||
$table->unsignedInteger('created_at');
|
||||
});
|
||||
|
||||
Schema::create('job_batches', function (Blueprint $table) {
|
||||
$table->string('id')->primary();
|
||||
$table->string('name');
|
||||
$table->integer('total_jobs');
|
||||
$table->integer('pending_jobs');
|
||||
$table->integer('failed_jobs');
|
||||
$table->longText('failed_job_ids');
|
||||
$table->mediumText('options')->nullable();
|
||||
$table->integer('cancelled_at')->nullable();
|
||||
$table->integer('created_at');
|
||||
$table->integer('finished_at')->nullable();
|
||||
});
|
||||
|
||||
Schema::create('failed_jobs', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('uuid')->unique();
|
||||
$table->text('connection');
|
||||
$table->text('queue');
|
||||
$table->longText('payload');
|
||||
$table->longText('exception');
|
||||
$table->timestamp('failed_at')->useCurrent();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('jobs');
|
||||
Schema::dropIfExists('job_batches');
|
||||
Schema::dropIfExists('failed_jobs');
|
||||
}
|
||||
};
|
||||
@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('permissions', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('name', 125);
|
||||
$table->string('guard_name', 125);
|
||||
$table->timestamps();
|
||||
|
||||
$table->unique(['name', 'guard_name']);
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('permissions');
|
||||
}
|
||||
};
|
||||
25
database/migrations/2026_04_18_183839_create_roles_table.php
Normal file
25
database/migrations/2026_04_18_183839_create_roles_table.php
Normal file
@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('roles', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('name', 125);
|
||||
$table->string('guard_name', 125);
|
||||
$table->timestamps();
|
||||
|
||||
$table->unique(['name', 'guard_name']);
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('roles');
|
||||
}
|
||||
};
|
||||
@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('bastion_accounts', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('name');
|
||||
$table->string('username')->unique();
|
||||
$table->text('password');
|
||||
$table->string('usm_authentication')->nullable();
|
||||
$table->string('usm')->nullable();
|
||||
$table->timestamp('last_token_refreshed_at')->nullable();
|
||||
$table->boolean('is_active')->default(true);
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('bastion_accounts');
|
||||
}
|
||||
};
|
||||
@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('server_resources', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('name');
|
||||
$table->string('internal_ip');
|
||||
$table->unsignedBigInteger('asset_id')->index();
|
||||
$table->unsignedBigInteger('account_id')->index();
|
||||
$table->json('protocols');
|
||||
$table->string('description')->nullable();
|
||||
$table->boolean('is_active')->default(true);
|
||||
$table->timestamps();
|
||||
|
||||
$table->index(['internal_ip', 'is_active']);
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('server_resources');
|
||||
}
|
||||
};
|
||||
@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('access_logs', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('user_id')->constrained()->cascadeOnDelete();
|
||||
$table->foreignId('server_resource_id')->constrained()->cascadeOnDelete();
|
||||
$table->foreignId('bastion_account_id')->nullable()->constrained()->nullOnDelete();
|
||||
$table->string('protocol', 16);
|
||||
$table->string('action', 64);
|
||||
$table->timestamp('requested_at');
|
||||
$table->json('metadata')->nullable();
|
||||
$table->timestamps();
|
||||
|
||||
$table->index(['user_id', 'requested_at']);
|
||||
$table->index(['server_resource_id', 'requested_at']);
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('access_logs');
|
||||
}
|
||||
};
|
||||
@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('user_server_permissions', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('user_id')->constrained()->cascadeOnDelete();
|
||||
$table->foreignId('server_resource_id')->constrained()->cascadeOnDelete();
|
||||
$table->boolean('can_ssh')->default(false);
|
||||
$table->boolean('can_sftp')->default(false);
|
||||
$table->boolean('can_rdp')->default(false);
|
||||
$table->timestamps();
|
||||
|
||||
$table->unique(['user_id', 'server_resource_id']);
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('user_server_permissions');
|
||||
}
|
||||
};
|
||||
@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('role_has_permissions', function (Blueprint $table) {
|
||||
$table->unsignedBigInteger('permission_id');
|
||||
$table->unsignedBigInteger('role_id');
|
||||
|
||||
$table->foreign('permission_id')
|
||||
->references('id')
|
||||
->on('permissions')
|
||||
->onDelete('cascade');
|
||||
$table->foreign('role_id')
|
||||
->references('id')
|
||||
->on('roles')
|
||||
->onDelete('cascade');
|
||||
|
||||
$table->primary(['permission_id', 'role_id']);
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('role_has_permissions');
|
||||
}
|
||||
};
|
||||
@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('model_has_roles', function (Blueprint $table) {
|
||||
$table->unsignedBigInteger('role_id');
|
||||
$table->string('model_type');
|
||||
$table->unsignedBigInteger('model_id');
|
||||
|
||||
$table->index(['model_id', 'model_type'], 'model_has_roles_model_id_model_type_index');
|
||||
$table->foreign('role_id')
|
||||
->references('id')
|
||||
->on('roles')
|
||||
->onDelete('cascade');
|
||||
|
||||
$table->primary(['role_id', 'model_id', 'model_type'], 'model_has_roles_role_model_type_primary');
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('model_has_roles');
|
||||
}
|
||||
};
|
||||
@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('model_has_permissions', function (Blueprint $table) {
|
||||
$table->unsignedBigInteger('permission_id');
|
||||
$table->string('model_type');
|
||||
$table->unsignedBigInteger('model_id');
|
||||
|
||||
$table->index(['model_id', 'model_type'], 'model_has_permissions_model_id_model_type_index');
|
||||
$table->foreign('permission_id')
|
||||
->references('id')
|
||||
->on('permissions')
|
||||
->onDelete('cascade');
|
||||
|
||||
$table->primary(['permission_id', 'model_id', 'model_type'], 'model_has_permissions_permission_model_type_primary');
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('model_has_permissions');
|
||||
}
|
||||
};
|
||||
@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('permissions', function (Blueprint $table) {
|
||||
$table->string('category', 100)->default('general')->after('name');
|
||||
$table->string('description', 255)->nullable()->after('category');
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('permissions', function (Blueprint $table) {
|
||||
$table->dropColumn(['category', 'description']);
|
||||
});
|
||||
}
|
||||
};
|
||||
@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('server_resources', function (Blueprint $table) {
|
||||
$table->foreignId('parent_id')->nullable()->after('id')->constrained('server_resources')->nullOnDelete();
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('server_resources', function (Blueprint $table) {
|
||||
$table->dropConstrainedForeignId('parent_id');
|
||||
});
|
||||
}
|
||||
};
|
||||
@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('access_logs', function (Blueprint $table) {
|
||||
$table->foreignId('user_id')->nullable()->change();
|
||||
$table->foreignId('server_resource_id')->nullable()->change();
|
||||
$table->string('protocol', 16)->nullable()->change();
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('access_logs', function (Blueprint $table) {
|
||||
$table->foreignId('user_id')->nullable(false)->change();
|
||||
$table->foreignId('server_resource_id')->nullable(false)->change();
|
||||
$table->string('protocol', 16)->nullable(false)->change();
|
||||
});
|
||||
}
|
||||
};
|
||||
@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('server_resources', function (Blueprint $table) {
|
||||
$table->string('display_name')->nullable()->after('name');
|
||||
$table->unsignedBigInteger('asset_id')->nullable()->change();
|
||||
$table->unsignedBigInteger('account_id')->nullable()->change();
|
||||
});
|
||||
|
||||
DB::table('server_resources')->update([
|
||||
'display_name' => DB::raw('name'),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
DB::table('server_resources')
|
||||
->whereNull('asset_id')
|
||||
->orWhereNull('account_id')
|
||||
->delete();
|
||||
|
||||
Schema::table('server_resources', function (Blueprint $table) {
|
||||
$table->dropColumn('display_name');
|
||||
$table->unsignedBigInteger('asset_id')->nullable(false)->change();
|
||||
$table->unsignedBigInteger('account_id')->nullable(false)->change();
|
||||
});
|
||||
}
|
||||
};
|
||||
@ -0,0 +1,50 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('ops_protocols', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('name')->unique();
|
||||
$table->string('description')->nullable();
|
||||
$table->unsignedInteger('sort')->default(0);
|
||||
$table->boolean('is_active')->default(true);
|
||||
$table->timestamps();
|
||||
});
|
||||
|
||||
Schema::create('ops_softwares', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('ops_protocol_id')->constrained('ops_protocols')->cascadeOnDelete();
|
||||
$table->string('name');
|
||||
$table->string('client_path')->nullable();
|
||||
$table->unsignedInteger('sort')->default(0);
|
||||
$table->boolean('is_active')->default(true);
|
||||
$table->timestamps();
|
||||
|
||||
$table->unique(['ops_protocol_id', 'name']);
|
||||
});
|
||||
|
||||
Schema::create('user_ops_software_preferences', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('user_id')->constrained('users')->cascadeOnDelete();
|
||||
$table->foreignId('ops_protocol_id')->constrained('ops_protocols')->cascadeOnDelete();
|
||||
$table->foreignId('ops_software_id')->constrained('ops_softwares')->cascadeOnDelete();
|
||||
$table->timestamps();
|
||||
|
||||
$table->unique(['user_id', 'ops_protocol_id'], 'uq_user_ops_protocol');
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('user_ops_software_preferences');
|
||||
Schema::dropIfExists('ops_softwares');
|
||||
Schema::dropIfExists('ops_protocols');
|
||||
}
|
||||
};
|
||||
|
||||
@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('ops_protocols', function (Blueprint $table) {
|
||||
$table->unsignedInteger('bastion_protocol_id')->default(2)->after('name');
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('ops_protocols', function (Blueprint $table) {
|
||||
$table->dropColumn('bastion_protocol_id');
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
23
database/seeders/DatabaseSeeder.php
Normal file
23
database/seeders/DatabaseSeeder.php
Normal file
@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
namespace Database\Seeders;
|
||||
|
||||
use App\Models\User;
|
||||
// use Illuminate\Database\Console\Seeds\WithoutModelEvents;
|
||||
use Illuminate\Database\Seeder;
|
||||
|
||||
class DatabaseSeeder extends Seeder
|
||||
{
|
||||
/**
|
||||
* Seed the application's database.
|
||||
*/
|
||||
public function run(): void
|
||||
{
|
||||
// User::factory(10)->create();
|
||||
|
||||
User::factory()->create([
|
||||
'name' => 'Test User',
|
||||
'email' => 'test@example.com',
|
||||
]);
|
||||
}
|
||||
}
|
||||
13
package.json
Normal file
13
package.json
Normal file
@ -0,0 +1,13 @@
|
||||
{
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build"
|
||||
},
|
||||
"devDependencies": {
|
||||
"axios": "^1.6.4",
|
||||
"laravel-vite-plugin": "^1.0",
|
||||
"vite": "^5.0"
|
||||
}
|
||||
}
|
||||
33
phpunit.xml
Normal file
33
phpunit.xml
Normal file
@ -0,0 +1,33 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:noNamespaceSchemaLocation="vendor/phpunit/phpunit/phpunit.xsd"
|
||||
bootstrap="vendor/autoload.php"
|
||||
colors="true"
|
||||
>
|
||||
<testsuites>
|
||||
<testsuite name="Unit">
|
||||
<directory>tests/Unit</directory>
|
||||
</testsuite>
|
||||
<testsuite name="Feature">
|
||||
<directory>tests/Feature</directory>
|
||||
</testsuite>
|
||||
</testsuites>
|
||||
<source>
|
||||
<include>
|
||||
<directory>app</directory>
|
||||
</include>
|
||||
</source>
|
||||
<php>
|
||||
<env name="APP_ENV" value="testing"/>
|
||||
<env name="APP_MAINTENANCE_DRIVER" value="file"/>
|
||||
<env name="BCRYPT_ROUNDS" value="4"/>
|
||||
<env name="CACHE_STORE" value="array"/>
|
||||
<!-- <env name="DB_CONNECTION" value="sqlite"/> -->
|
||||
<!-- <env name="DB_DATABASE" value=":memory:"/> -->
|
||||
<env name="MAIL_MAILER" value="array"/>
|
||||
<env name="PULSE_ENABLED" value="false"/>
|
||||
<env name="QUEUE_CONNECTION" value="sync"/>
|
||||
<env name="SESSION_DRIVER" value="array"/>
|
||||
<env name="TELESCOPE_ENABLED" value="false"/>
|
||||
</php>
|
||||
</phpunit>
|
||||
0
public/.htaccess
Normal file
0
public/.htaccess
Normal file
BIN
public/apidoc/assets/Blimone-Light.0af1a4d6.woff
Normal file
BIN
public/apidoc/assets/Blimone-Light.0af1a4d6.woff
Normal file
Binary file not shown.
1
public/apidoc/assets/Skeleton.be316f96.js
Normal file
1
public/apidoc/assets/Skeleton.be316f96.js
Normal file
File diff suppressed because one or more lines are too long
10
public/apidoc/assets/Skeleton.d3f8d668.css
Normal file
10
public/apidoc/assets/Skeleton.d3f8d668.css
Normal file
File diff suppressed because one or more lines are too long
7
public/apidoc/assets/abap.6ba285e6.js
Normal file
7
public/apidoc/assets/abap.6ba285e6.js
Normal file
File diff suppressed because one or more lines are too long
7
public/apidoc/assets/apex.4a5ec4a4.js
Normal file
7
public/apidoc/assets/apex.4a5ec4a4.js
Normal file
@ -0,0 +1,7 @@
|
||||
/*!-----------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Version: 0.33.0(4b1abad427e58dbedc1215d99a0902ffc885fcd4)
|
||||
* Released under the MIT license
|
||||
* https://github.com/microsoft/monaco-editor/blob/main/LICENSE.txt
|
||||
*-----------------------------------------------------------------------------*/
|
||||
var e={wordPattern:/(-?\d*\.\d\w*)|([^\`\~\!\#\%\^\&\*\(\)\-\=\+\[\{\]\}\\\|\;\:\'\"\,\.\<\>\/\?\s]+)/g,comments:{lineComment:"//",blockComment:["/*","*/"]},brackets:[["{","}"],["[","]"],["(",")"]],autoClosingPairs:[{open:"{",close:"}"},{open:"[",close:"]"},{open:"(",close:")"},{open:'"',close:'"'},{open:"'",close:"'"}],surroundingPairs:[{open:"{",close:"}"},{open:"[",close:"]"},{open:"(",close:")"},{open:'"',close:'"'},{open:"'",close:"'"},{open:"<",close:">"}],folding:{markers:{start:new RegExp("^\\s*//\\s*(?:(?:#?region\\b)|(?:<editor-fold\\b))"),end:new RegExp("^\\s*//\\s*(?:(?:#?endregion\\b)|(?:</editor-fold>))")}}},t=[];["abstract","activate","and","any","array","as","asc","assert","autonomous","begin","bigdecimal","blob","boolean","break","bulk","by","case","cast","catch","char","class","collect","commit","const","continue","convertcurrency","decimal","default","delete","desc","do","double","else","end","enum","exception","exit","export","extends","false","final","finally","float","for","from","future","get","global","goto","group","having","hint","if","implements","import","in","inner","insert","instanceof","int","interface","into","join","last_90_days","last_month","last_n_days","last_week","like","limit","list","long","loop","map","merge","native","new","next_90_days","next_month","next_n_days","next_week","not","null","nulls","number","object","of","on","or","outer","override","package","parallel","pragma","private","protected","public","retrieve","return","returning","rollback","savepoint","search","select","set","short","sort","stat","static","strictfp","super","switch","synchronized","system","testmethod","then","this","this_month","this_week","throw","throws","today","tolabel","tomorrow","transaction","transient","trigger","true","try","type","undelete","update","upsert","using","virtual","void","volatile","webservice","when","where","while","yesterday"].forEach((e=>{t.push(e),t.push(e.toUpperCase()),t.push((e=>e.charAt(0).toUpperCase()+e.substr(1))(e))}));var s={defaultToken:"",tokenPostfix:".apex",keywords:t,operators:["=",">","<","!","~","?",":","==","<=",">=","!=","&&","||","++","--","+","-","*","/","&","|","^","%","<<",">>",">>>","+=","-=","*=","/=","&=","|=","^=","%=","<<=",">>=",">>>="],symbols:/[=><!~?:&|+\-*\/\^%]+/,escapes:/\\(?:[abfnrtv\\"']|x[0-9A-Fa-f]{1,4}|u[0-9A-Fa-f]{4}|U[0-9A-Fa-f]{8})/,digits:/\d+(_+\d+)*/,octaldigits:/[0-7]+(_+[0-7]+)*/,binarydigits:/[0-1]+(_+[0-1]+)*/,hexdigits:/[[0-9a-fA-F]+(_+[0-9a-fA-F]+)*/,tokenizer:{root:[[/[a-z_$][\w$]*/,{cases:{"@keywords":{token:"keyword.$0"},"@default":"identifier"}}],[/[A-Z][\w\$]*/,{cases:{"@keywords":{token:"keyword.$0"},"@default":"type.identifier"}}],{include:"@whitespace"},[/[{}()\[\]]/,"@brackets"],[/[<>](?!@symbols)/,"@brackets"],[/@symbols/,{cases:{"@operators":"delimiter","@default":""}}],[/@\s*[a-zA-Z_\$][\w\$]*/,"annotation"],[/(@digits)[eE]([\-+]?(@digits))?[fFdD]?/,"number.float"],[/(@digits)\.(@digits)([eE][\-+]?(@digits))?[fFdD]?/,"number.float"],[/(@digits)[fFdD]/,"number.float"],[/(@digits)[lL]?/,"number"],[/[;,.]/,"delimiter"],[/"([^"\\]|\\.)*$/,"string.invalid"],[/'([^'\\]|\\.)*$/,"string.invalid"],[/"/,"string",'@string."'],[/'/,"string","@string.'"],[/'[^\\']'/,"string"],[/(')(@escapes)(')/,["string","string.escape","string"]],[/'/,"string.invalid"]],whitespace:[[/[ \t\r\n]+/,""],[/\/\*\*(?!\/)/,"comment.doc","@apexdoc"],[/\/\*/,"comment","@comment"],[/\/\/.*$/,"comment"]],comment:[[/[^\/*]+/,"comment"],[/\*\//,"comment","@pop"],[/[\/*]/,"comment"]],apexdoc:[[/[^\/*]+/,"comment.doc"],[/\*\//,"comment.doc","@pop"],[/[\/*]/,"comment.doc"]],string:[[/[^\\"']+/,"string"],[/@escapes/,"string.escape"],[/\\./,"string.escape.invalid"],[/["']/,{cases:{"$#==$S2":{token:"string",next:"@pop"},"@default":"string"}}]]}};export{e as conf,s as language};
|
||||
7
public/apidoc/assets/azcli.4c9b6b47.js
Normal file
7
public/apidoc/assets/azcli.4c9b6b47.js
Normal file
@ -0,0 +1,7 @@
|
||||
/*!-----------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Version: 0.33.0(4b1abad427e58dbedc1215d99a0902ffc885fcd4)
|
||||
* Released under the MIT license
|
||||
* https://github.com/microsoft/monaco-editor/blob/main/LICENSE.txt
|
||||
*-----------------------------------------------------------------------------*/
|
||||
var e={comments:{lineComment:"#"}},t={defaultToken:"keyword",ignoreCase:!0,tokenPostfix:".azcli",str:/[^#\s]/,tokenizer:{root:[{include:"@comment"},[/\s-+@str*\s*/,{cases:{"@eos":{token:"key.identifier",next:"@popall"},"@default":{token:"key.identifier",next:"@type"}}}],[/^-+@str*\s*/,{cases:{"@eos":{token:"key.identifier",next:"@popall"},"@default":{token:"key.identifier",next:"@type"}}}]],type:[{include:"@comment"},[/-+@str*\s*/,{cases:{"@eos":{token:"key.identifier",next:"@popall"},"@default":"key.identifier"}}],[/@str+\s*/,{cases:{"@eos":{token:"string",next:"@popall"},"@default":"string"}}]],comment:[[/#.*$/,{cases:{"@eos":{token:"comment",next:"@popall"}}}]]}};export{e as conf,t as language};
|
||||
7
public/apidoc/assets/bat.8a420ace.js
Normal file
7
public/apidoc/assets/bat.8a420ace.js
Normal file
@ -0,0 +1,7 @@
|
||||
/*!-----------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Version: 0.33.0(4b1abad427e58dbedc1215d99a0902ffc885fcd4)
|
||||
* Released under the MIT license
|
||||
* https://github.com/microsoft/monaco-editor/blob/main/LICENSE.txt
|
||||
*-----------------------------------------------------------------------------*/
|
||||
var e={comments:{lineComment:"REM"},brackets:[["{","}"],["[","]"],["(",")"]],autoClosingPairs:[{open:"{",close:"}"},{open:"[",close:"]"},{open:"(",close:")"},{open:'"',close:'"'}],surroundingPairs:[{open:"[",close:"]"},{open:"(",close:")"},{open:'"',close:'"'}],folding:{markers:{start:new RegExp("^\\s*(::\\s*|REM\\s+)#region"),end:new RegExp("^\\s*(::\\s*|REM\\s+)#endregion")}}},s={defaultToken:"",ignoreCase:!0,tokenPostfix:".bat",brackets:[{token:"delimiter.bracket",open:"{",close:"}"},{token:"delimiter.parenthesis",open:"(",close:")"},{token:"delimiter.square",open:"[",close:"]"}],keywords:/call|defined|echo|errorlevel|exist|for|goto|if|pause|set|shift|start|title|not|pushd|popd/,symbols:/[=><!~?&|+\-*\/\^;\.,]+/,escapes:/\\(?:[abfnrtv\\"']|x[0-9A-Fa-f]{1,4}|u[0-9A-Fa-f]{4}|U[0-9A-Fa-f]{8})/,tokenizer:{root:[[/^(\s*)(rem(?:\s.*|))$/,["","comment"]],[/(\@?)(@keywords)(?!\w)/,[{token:"keyword"},{token:"keyword.$2"}]],[/[ \t\r\n]+/,""],[/setlocal(?!\w)/,"keyword.tag-setlocal"],[/endlocal(?!\w)/,"keyword.tag-setlocal"],[/[a-zA-Z_]\w*/,""],[/:\w*/,"metatag"],[/%[^%]+%/,"variable"],[/%%[\w]+(?!\w)/,"variable"],[/[{}()\[\]]/,"@brackets"],[/@symbols/,"delimiter"],[/\d*\.\d+([eE][\-+]?\d+)?/,"number.float"],[/0[xX][0-9a-fA-F_]*[0-9a-fA-F]/,"number.hex"],[/\d+/,"number"],[/[;,.]/,"delimiter"],[/"/,"string",'@string."'],[/'/,"string","@string.'"]],string:[[/[^\\"'%]+/,{cases:{"@eos":{token:"string",next:"@popall"},"@default":"string"}}],[/@escapes/,"string.escape"],[/\\./,"string.escape.invalid"],[/%[\w ]+%/,"variable"],[/%%[\w]+(?!\w)/,"variable"],[/["']/,{cases:{"$#==$S2":{token:"string",next:"@pop"},"@default":"string"}}],[/$/,"string","@popall"]]}};export{e as conf,s as language};
|
||||
7
public/apidoc/assets/bicep.5032e09b.js
Normal file
7
public/apidoc/assets/bicep.5032e09b.js
Normal file
@ -0,0 +1,7 @@
|
||||
/*!-----------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Version: 0.33.0(4b1abad427e58dbedc1215d99a0902ffc885fcd4)
|
||||
* Released under the MIT license
|
||||
* https://github.com/microsoft/monaco-editor/blob/main/LICENSE.txt
|
||||
*-----------------------------------------------------------------------------*/
|
||||
var e=`\\b${"[_a-zA-Z][_a-zA-Z0-9]*"}\\b`,n={comments:{lineComment:"//",blockComment:["/*","*/"]},brackets:[["{","}"],["[","]"],["(",")"]],surroundingPairs:[{open:"{",close:"}"},{open:"[",close:"]"},{open:"(",close:")"},{open:"'",close:"'"},{open:"'''",close:"'''"}],autoClosingPairs:[{open:"{",close:"}"},{open:"[",close:"]"},{open:"(",close:")"},{open:"'",close:"'",notIn:["string","comment"]},{open:"'''",close:"'''",notIn:["string","comment"]}],autoCloseBefore:":.,=}])' \n\t",indentationRules:{increaseIndentPattern:new RegExp("^((?!\\/\\/).)*(\\{[^}\"'`]*|\\([^)\"'`]*|\\[[^\\]\"'`]*)$"),decreaseIndentPattern:new RegExp("^((?!.*?\\/\\*).*\\*/)?\\s*[\\}\\]].*$")}},t={defaultToken:"",tokenPostfix:".bicep",brackets:[{open:"{",close:"}",token:"delimiter.curly"},{open:"[",close:"]",token:"delimiter.square"},{open:"(",close:")",token:"delimiter.parenthesis"}],symbols:/[=><!~?:&|+\-*/^%]+/,keywords:["targetScope","resource","module","param","var","output","for","in","if","existing"],namedLiterals:["true","false","null"],escapes:"\\\\(u{[0-9A-Fa-f]+}|n|r|t|\\\\|'|\\${)",tokenizer:{root:[{include:"@expression"},{include:"@whitespace"}],stringVerbatim:[{regex:"(|'|'')[^']",action:{token:"string"}},{regex:"'''",action:{token:"string.quote",next:"@pop"}}],stringLiteral:[{regex:"\\${",action:{token:"delimiter.bracket",next:"@bracketCounting"}},{regex:"[^\\\\'$]+",action:{token:"string"}},{regex:"@escapes",action:{token:"string.escape"}},{regex:"\\\\.",action:{token:"string.escape.invalid"}},{regex:"'",action:{token:"string",next:"@pop"}}],bracketCounting:[{regex:"{",action:{token:"delimiter.bracket",next:"@bracketCounting"}},{regex:"}",action:{token:"delimiter.bracket",next:"@pop"}},{include:"expression"}],comment:[{regex:"[^\\*]+",action:{token:"comment"}},{regex:"\\*\\/",action:{token:"comment",next:"@pop"}},{regex:"[\\/*]",action:{token:"comment"}}],whitespace:[{regex:"[ \\t\\r\\n]"},{regex:"\\/\\*",action:{token:"comment",next:"@comment"}},{regex:"\\/\\/.*$",action:{token:"comment"}}],expression:[{regex:"'''",action:{token:"string.quote",next:"@stringVerbatim"}},{regex:"'",action:{token:"string.quote",next:"@stringLiteral"}},{regex:"[0-9]+",action:{token:"number"}},{regex:e,action:{cases:{"@keywords":{token:"keyword"},"@namedLiterals":{token:"keyword"},"@default":{token:"identifier"}}}}]}};export{n as conf,t as language};
|
||||
7
public/apidoc/assets/cameligo.00bc63f8.js
Normal file
7
public/apidoc/assets/cameligo.00bc63f8.js
Normal file
@ -0,0 +1,7 @@
|
||||
/*!-----------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Version: 0.33.0(4b1abad427e58dbedc1215d99a0902ffc885fcd4)
|
||||
* Released under the MIT license
|
||||
* https://github.com/microsoft/monaco-editor/blob/main/LICENSE.txt
|
||||
*-----------------------------------------------------------------------------*/
|
||||
var e={comments:{lineComment:"//",blockComment:["(*","*)"]},brackets:[["{","}"],["[","]"],["(",")"],["<",">"]],autoClosingPairs:[{open:"{",close:"}"},{open:"[",close:"]"},{open:"(",close:")"},{open:"<",close:">"},{open:"'",close:"'"},{open:'"',close:'"'},{open:"(*",close:"*)"}],surroundingPairs:[{open:"{",close:"}"},{open:"[",close:"]"},{open:"(",close:")"},{open:"<",close:">"},{open:"'",close:"'"},{open:'"',close:'"'},{open:"(*",close:"*)"}]},o={defaultToken:"",tokenPostfix:".cameligo",ignoreCase:!0,brackets:[{open:"{",close:"}",token:"delimiter.curly"},{open:"[",close:"]",token:"delimiter.square"},{open:"(",close:")",token:"delimiter.parenthesis"},{open:"<",close:">",token:"delimiter.angle"}],keywords:["abs","assert","block","Bytes","case","Crypto","Current","else","failwith","false","for","fun","if","in","let","let%entry","let%init","List","list","Map","map","match","match%nat","mod","not","operation","Operation","of","record","Set","set","sender","skip","source","String","then","to","true","type","with"],typeKeywords:["int","unit","string","tz","nat","bool"],operators:["=",">","<","<=",">=","<>",":",":=","and","mod","or","+","-","*","/","@","&","^","%","->","<-","&&","||"],symbols:/[=><:@\^&|+\-*\/\^%]+/,tokenizer:{root:[[/[a-zA-Z_][\w]*/,{cases:{"@keywords":{token:"keyword.$0"},"@default":"identifier"}}],{include:"@whitespace"},[/[{}()\[\]]/,"@brackets"],[/[<>](?!@symbols)/,"@brackets"],[/@symbols/,{cases:{"@operators":"delimiter","@default":""}}],[/\d*\.\d+([eE][\-+]?\d+)?/,"number.float"],[/\$[0-9a-fA-F]{1,16}/,"number.hex"],[/\d+/,"number"],[/[;,.]/,"delimiter"],[/'([^'\\]|\\.)*$/,"string.invalid"],[/'/,"string","@string"],[/'[^\\']'/,"string"],[/'/,"string.invalid"],[/\#\d+/,"string"]],comment:[[/[^\(\*]+/,"comment"],[/\*\)/,"comment","@pop"],[/\(\*/,"comment"]],string:[[/[^\\']+/,"string"],[/\\./,"string.escape.invalid"],[/'/,{token:"string.quote",bracket:"@close",next:"@pop"}]],whitespace:[[/[ \t\r\n]+/,"white"],[/\(\*/,"comment","@comment"],[/\/\/.*$/,"comment"]]}};export{e as conf,o as language};
|
||||
7
public/apidoc/assets/clojure.bc79377e.js
Normal file
7
public/apidoc/assets/clojure.bc79377e.js
Normal file
File diff suppressed because one or more lines are too long
BIN
public/apidoc/assets/codicon.c99115f8.ttf
Normal file
BIN
public/apidoc/assets/codicon.c99115f8.ttf
Normal file
Binary file not shown.
7
public/apidoc/assets/coffee.54897858.js
Normal file
7
public/apidoc/assets/coffee.54897858.js
Normal file
@ -0,0 +1,7 @@
|
||||
/*!-----------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Version: 0.33.0(4b1abad427e58dbedc1215d99a0902ffc885fcd4)
|
||||
* Released under the MIT license
|
||||
* https://github.com/microsoft/monaco-editor/blob/main/LICENSE.txt
|
||||
*-----------------------------------------------------------------------------*/
|
||||
var e={wordPattern:/(-?\d*\.\d\w*)|([^\`\~\!\@\#%\^\&\*\(\)\=\$\-\+\[\{\]\}\\\|\;\:\'\"\,\.\<\>\/\?\s]+)/g,comments:{blockComment:["###","###"],lineComment:"#"},brackets:[["{","}"],["[","]"],["(",")"]],autoClosingPairs:[{open:"{",close:"}"},{open:"[",close:"]"},{open:"(",close:")"},{open:'"',close:'"'},{open:"'",close:"'"}],surroundingPairs:[{open:"{",close:"}"},{open:"[",close:"]"},{open:"(",close:")"},{open:'"',close:'"'},{open:"'",close:"'"}],folding:{markers:{start:new RegExp("^\\s*#region\\b"),end:new RegExp("^\\s*#endregion\\b")}}},r={defaultToken:"",ignoreCase:!0,tokenPostfix:".coffee",brackets:[{open:"{",close:"}",token:"delimiter.curly"},{open:"[",close:"]",token:"delimiter.square"},{open:"(",close:")",token:"delimiter.parenthesis"}],regEx:/\/(?!\/\/)(?:[^\/\\]|\\.)*\/[igm]*/,keywords:["and","or","is","isnt","not","on","yes","@","no","off","true","false","null","this","new","delete","typeof","in","instanceof","return","throw","break","continue","debugger","if","else","switch","for","while","do","try","catch","finally","class","extends","super","undefined","then","unless","until","loop","of","by","when"],symbols:/[=><!~?&%|+\-*\/\^\.,\:]+/,escapes:/\\(?:[abfnrtv\\"'$]|x[0-9A-Fa-f]{1,4}|u[0-9A-Fa-f]{4}|U[0-9A-Fa-f]{8})/,tokenizer:{root:[[/\@[a-zA-Z_]\w*/,"variable.predefined"],[/[a-zA-Z_]\w*/,{cases:{this:"variable.predefined","@keywords":{token:"keyword.$0"},"@default":""}}],[/[ \t\r\n]+/,""],[/###/,"comment","@comment"],[/#.*$/,"comment"],["///",{token:"regexp",next:"@hereregexp"}],[/^(\s*)(@regEx)/,["","regexp"]],[/(\()(\s*)(@regEx)/,["@brackets","","regexp"]],[/(\,)(\s*)(@regEx)/,["delimiter","","regexp"]],[/(\=)(\s*)(@regEx)/,["delimiter","","regexp"]],[/(\:)(\s*)(@regEx)/,["delimiter","","regexp"]],[/(\[)(\s*)(@regEx)/,["@brackets","","regexp"]],[/(\!)(\s*)(@regEx)/,["delimiter","","regexp"]],[/(\&)(\s*)(@regEx)/,["delimiter","","regexp"]],[/(\|)(\s*)(@regEx)/,["delimiter","","regexp"]],[/(\?)(\s*)(@regEx)/,["delimiter","","regexp"]],[/(\{)(\s*)(@regEx)/,["@brackets","","regexp"]],[/(\;)(\s*)(@regEx)/,["","","regexp"]],[/}/,{cases:{"$S2==interpolatedstring":{token:"string",next:"@pop"},"@default":"@brackets"}}],[/[{}()\[\]]/,"@brackets"],[/@symbols/,"delimiter"],[/\d+[eE]([\-+]?\d+)?/,"number.float"],[/\d+\.\d+([eE][\-+]?\d+)?/,"number.float"],[/0[xX][0-9a-fA-F]+/,"number.hex"],[/0[0-7]+(?!\d)/,"number.octal"],[/\d+/,"number"],[/[,.]/,"delimiter"],[/"""/,"string",'@herestring."""'],[/'''/,"string","@herestring.'''"],[/"/,{cases:{"@eos":"string","@default":{token:"string",next:'@string."'}}}],[/'/,{cases:{"@eos":"string","@default":{token:"string",next:"@string.'"}}}]],string:[[/[^"'\#\\]+/,"string"],[/@escapes/,"string.escape"],[/\./,"string.escape.invalid"],[/\./,"string.escape.invalid"],[/#{/,{cases:{'$S2=="':{token:"string",next:"root.interpolatedstring"},"@default":"string"}}],[/["']/,{cases:{"$#==$S2":{token:"string",next:"@pop"},"@default":"string"}}],[/#/,"string"]],herestring:[[/("""|''')/,{cases:{"$1==$S2":{token:"string",next:"@pop"},"@default":"string"}}],[/[^#\\'"]+/,"string"],[/['"]+/,"string"],[/@escapes/,"string.escape"],[/\./,"string.escape.invalid"],[/#{/,{token:"string.quote",next:"root.interpolatedstring"}],[/#/,"string"]],comment:[[/[^#]+/,"comment"],[/###/,"comment","@pop"],[/#/,"comment"]],hereregexp:[[/[^\\\/#]+/,"regexp"],[/\\./,"regexp"],[/#.*$/,"comment"],["///[igm]*",{token:"regexp",next:"@pop"}],[/\//,"regexp"]]}};export{e as conf,r as language};
|
||||
7
public/apidoc/assets/cpp.337468ce.js
Normal file
7
public/apidoc/assets/cpp.337468ce.js
Normal file
File diff suppressed because one or more lines are too long
7
public/apidoc/assets/csharp.01a8eaa8.js
Normal file
7
public/apidoc/assets/csharp.01a8eaa8.js
Normal file
@ -0,0 +1,7 @@
|
||||
/*!-----------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Version: 0.33.0(4b1abad427e58dbedc1215d99a0902ffc885fcd4)
|
||||
* Released under the MIT license
|
||||
* https://github.com/microsoft/monaco-editor/blob/main/LICENSE.txt
|
||||
*-----------------------------------------------------------------------------*/
|
||||
var e={wordPattern:/(-?\d*\.\d\w*)|([^\`\~\!\#\$\%\^\&\*\(\)\-\=\+\[\{\]\}\\\|\;\:\'\"\,\.\<\>\/\?\s]+)/g,comments:{lineComment:"//",blockComment:["/*","*/"]},brackets:[["{","}"],["[","]"],["(",")"]],autoClosingPairs:[{open:"{",close:"}"},{open:"[",close:"]"},{open:"(",close:")"},{open:"'",close:"'",notIn:["string","comment"]},{open:'"',close:'"',notIn:["string","comment"]}],surroundingPairs:[{open:"{",close:"}"},{open:"[",close:"]"},{open:"(",close:")"},{open:"<",close:">"},{open:"'",close:"'"},{open:'"',close:'"'}],folding:{markers:{start:new RegExp("^\\s*#region\\b"),end:new RegExp("^\\s*#endregion\\b")}}},t={defaultToken:"",tokenPostfix:".cs",brackets:[{open:"{",close:"}",token:"delimiter.curly"},{open:"[",close:"]",token:"delimiter.square"},{open:"(",close:")",token:"delimiter.parenthesis"},{open:"<",close:">",token:"delimiter.angle"}],keywords:["extern","alias","using","bool","decimal","sbyte","byte","short","ushort","int","uint","long","ulong","char","float","double","object","dynamic","string","assembly","is","as","ref","out","this","base","new","typeof","void","checked","unchecked","default","delegate","var","const","if","else","switch","case","while","do","for","foreach","in","break","continue","goto","return","throw","try","catch","finally","lock","yield","from","let","where","join","on","equals","into","orderby","ascending","descending","select","group","by","namespace","partial","class","field","event","method","param","public","protected","internal","private","abstract","sealed","static","struct","readonly","volatile","virtual","override","params","get","set","add","remove","operator","true","false","implicit","explicit","interface","enum","null","async","await","fixed","sizeof","stackalloc","unsafe","nameof","when"],namespaceFollows:["namespace","using"],parenFollows:["if","for","while","switch","foreach","using","catch","when"],operators:["=","??","||","&&","|","^","&","==","!=","<=",">=","<<","+","-","*","/","%","!","~","++","--","+=","-=","*=","/=","%=","&=","|=","^=","<<=",">>=",">>","=>"],symbols:/[=><!~?:&|+\-*\/\^%]+/,escapes:/\\(?:[abfnrtv\\"']|x[0-9A-Fa-f]{1,4}|u[0-9A-Fa-f]{4}|U[0-9A-Fa-f]{8})/,tokenizer:{root:[[/\@?[a-zA-Z_]\w*/,{cases:{"@namespaceFollows":{token:"keyword.$0",next:"@namespace"},"@keywords":{token:"keyword.$0",next:"@qualified"},"@default":{token:"identifier",next:"@qualified"}}}],{include:"@whitespace"},[/}/,{cases:{"$S2==interpolatedstring":{token:"string.quote",next:"@pop"},"$S2==litinterpstring":{token:"string.quote",next:"@pop"},"@default":"@brackets"}}],[/[{}()\[\]]/,"@brackets"],[/[<>](?!@symbols)/,"@brackets"],[/@symbols/,{cases:{"@operators":"delimiter","@default":""}}],[/[0-9_]*\.[0-9_]+([eE][\-+]?\d+)?[fFdD]?/,"number.float"],[/0[xX][0-9a-fA-F_]+/,"number.hex"],[/0[bB][01_]+/,"number.hex"],[/[0-9_]+/,"number"],[/[;,.]/,"delimiter"],[/"([^"\\]|\\.)*$/,"string.invalid"],[/"/,{token:"string.quote",next:"@string"}],[/\$\@"/,{token:"string.quote",next:"@litinterpstring"}],[/\@"/,{token:"string.quote",next:"@litstring"}],[/\$"/,{token:"string.quote",next:"@interpolatedstring"}],[/'[^\\']'/,"string"],[/(')(@escapes)(')/,["string","string.escape","string"]],[/'/,"string.invalid"]],qualified:[[/[a-zA-Z_][\w]*/,{cases:{"@keywords":{token:"keyword.$0"},"@default":"identifier"}}],[/\./,"delimiter"],["","","@pop"]],namespace:[{include:"@whitespace"},[/[A-Z]\w*/,"namespace"],[/[\.=]/,"delimiter"],["","","@pop"]],comment:[[/[^\/*]+/,"comment"],["\\*/","comment","@pop"],[/[\/*]/,"comment"]],string:[[/[^\\"]+/,"string"],[/@escapes/,"string.escape"],[/\\./,"string.escape.invalid"],[/"/,{token:"string.quote",next:"@pop"}]],litstring:[[/[^"]+/,"string"],[/""/,"string.escape"],[/"/,{token:"string.quote",next:"@pop"}]],litinterpstring:[[/[^"{]+/,"string"],[/""/,"string.escape"],[/{{/,"string.escape"],[/}}/,"string.escape"],[/{/,{token:"string.quote",next:"root.litinterpstring"}],[/"/,{token:"string.quote",next:"@pop"}]],interpolatedstring:[[/[^\\"{]+/,"string"],[/@escapes/,"string.escape"],[/\\./,"string.escape.invalid"],[/{{/,"string.escape"],[/}}/,"string.escape"],[/{/,{token:"string.quote",next:"root.interpolatedstring"}],[/"/,{token:"string.quote",next:"@pop"}]],whitespace:[[/^[ \t\v\f]*#((r)|(load))(?=\s)/,"directive.csx"],[/^[ \t\v\f]*#\w.*$/,"namespace.cpp"],[/[ \t\v\f\r\n]+/,""],[/\/\*/,"comment","@comment"],[/\/\/.*$/,"comment"]]}};export{e as conf,t as language};
|
||||
7
public/apidoc/assets/csp.aec2811b.js
Normal file
7
public/apidoc/assets/csp.aec2811b.js
Normal file
@ -0,0 +1,7 @@
|
||||
/*!-----------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Version: 0.33.0(4b1abad427e58dbedc1215d99a0902ffc885fcd4)
|
||||
* Released under the MIT license
|
||||
* https://github.com/microsoft/monaco-editor/blob/main/LICENSE.txt
|
||||
*-----------------------------------------------------------------------------*/
|
||||
var t={brackets:[],autoClosingPairs:[],surroundingPairs:[]},r={keywords:[],typeKeywords:[],tokenPostfix:".csp",operators:[],symbols:/[=><!~?:&|+\-*\/\^%]+/,escapes:/\\(?:[abfnrtv\\"']|x[0-9A-Fa-f]{1,4}|u[0-9A-Fa-f]{4}|U[0-9A-Fa-f]{8})/,tokenizer:{root:[[/child-src/,"string.quote"],[/connect-src/,"string.quote"],[/default-src/,"string.quote"],[/font-src/,"string.quote"],[/frame-src/,"string.quote"],[/img-src/,"string.quote"],[/manifest-src/,"string.quote"],[/media-src/,"string.quote"],[/object-src/,"string.quote"],[/script-src/,"string.quote"],[/style-src/,"string.quote"],[/worker-src/,"string.quote"],[/base-uri/,"string.quote"],[/plugin-types/,"string.quote"],[/sandbox/,"string.quote"],[/disown-opener/,"string.quote"],[/form-action/,"string.quote"],[/frame-ancestors/,"string.quote"],[/report-uri/,"string.quote"],[/report-to/,"string.quote"],[/upgrade-insecure-requests/,"string.quote"],[/block-all-mixed-content/,"string.quote"],[/require-sri-for/,"string.quote"],[/reflected-xss/,"string.quote"],[/referrer/,"string.quote"],[/policy-uri/,"string.quote"],[/'self'/,"string.quote"],[/'unsafe-inline'/,"string.quote"],[/'unsafe-eval'/,"string.quote"],[/'strict-dynamic'/,"string.quote"],[/'unsafe-hashed-attributes'/,"string.quote"]]}};export{t as conf,r as language};
|
||||
7
public/apidoc/assets/css.4c22ed20.js
Normal file
7
public/apidoc/assets/css.4c22ed20.js
Normal file
@ -0,0 +1,7 @@
|
||||
/*!-----------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Version: 0.33.0(4b1abad427e58dbedc1215d99a0902ffc885fcd4)
|
||||
* Released under the MIT license
|
||||
* https://github.com/microsoft/monaco-editor/blob/main/LICENSE.txt
|
||||
*-----------------------------------------------------------------------------*/
|
||||
var e={wordPattern:/(#?-?\d*\.\d\w*%?)|((::|[@#.!:])?[\w-?]+%?)|::|[@#.!:]/g,comments:{blockComment:["/*","*/"]},brackets:[["{","}"],["[","]"],["(",")"]],autoClosingPairs:[{open:"{",close:"}",notIn:["string","comment"]},{open:"[",close:"]",notIn:["string","comment"]},{open:"(",close:")",notIn:["string","comment"]},{open:'"',close:'"',notIn:["string","comment"]},{open:"'",close:"'",notIn:["string","comment"]}],surroundingPairs:[{open:"{",close:"}"},{open:"[",close:"]"},{open:"(",close:")"},{open:'"',close:'"'},{open:"'",close:"'"}],folding:{markers:{start:new RegExp("^\\s*\\/\\*\\s*#region\\b\\s*(.*?)\\s*\\*\\/"),end:new RegExp("^\\s*\\/\\*\\s*#endregion\\b.*\\*\\/")}}},t={defaultToken:"",tokenPostfix:".css",ws:"[ \t\n\r\f]*",identifier:"-?-?([a-zA-Z]|(\\\\(([0-9a-fA-F]{1,6}\\s?)|[^[0-9a-fA-F])))([\\w\\-]|(\\\\(([0-9a-fA-F]{1,6}\\s?)|[^[0-9a-fA-F])))*",brackets:[{open:"{",close:"}",token:"delimiter.bracket"},{open:"[",close:"]",token:"delimiter.bracket"},{open:"(",close:")",token:"delimiter.parenthesis"},{open:"<",close:">",token:"delimiter.angle"}],tokenizer:{root:[{include:"@selector"}],selector:[{include:"@comments"},{include:"@import"},{include:"@strings"},["[@](keyframes|-webkit-keyframes|-moz-keyframes|-o-keyframes)",{token:"keyword",next:"@keyframedeclaration"}],["[@](page|content|font-face|-moz-document)",{token:"keyword"}],["[@](charset|namespace)",{token:"keyword",next:"@declarationbody"}],["(url-prefix)(\\()",["attribute.value",{token:"delimiter.parenthesis",next:"@urldeclaration"}]],["(url)(\\()",["attribute.value",{token:"delimiter.parenthesis",next:"@urldeclaration"}]],{include:"@selectorname"},["[\\*]","tag"],["[>\\+,]","delimiter"],["\\[",{token:"delimiter.bracket",next:"@selectorattribute"}],["{",{token:"delimiter.bracket",next:"@selectorbody"}]],selectorbody:[{include:"@comments"},["[*_]?@identifier@ws:(?=(\\s|\\d|[^{;}]*[;}]))","attribute.name","@rulevalue"],["}",{token:"delimiter.bracket",next:"@pop"}]],selectorname:[["(\\.|#(?=[^{])|%|(@identifier)|:)+","tag"]],selectorattribute:[{include:"@term"},["]",{token:"delimiter.bracket",next:"@pop"}]],term:[{include:"@comments"},["(url-prefix)(\\()",["attribute.value",{token:"delimiter.parenthesis",next:"@urldeclaration"}]],["(url)(\\()",["attribute.value",{token:"delimiter.parenthesis",next:"@urldeclaration"}]],{include:"@functioninvocation"},{include:"@numbers"},{include:"@name"},{include:"@strings"},["([<>=\\+\\-\\*\\/\\^\\|\\~,])","delimiter"],[",","delimiter"]],rulevalue:[{include:"@comments"},{include:"@strings"},{include:"@term"},["!important","keyword"],[";","delimiter","@pop"],["(?=})",{token:"",next:"@pop"}]],warndebug:[["[@](warn|debug)",{token:"keyword",next:"@declarationbody"}]],import:[["[@](import)",{token:"keyword",next:"@declarationbody"}]],urldeclaration:[{include:"@strings"},["[^)\r\n]+","string"],["\\)",{token:"delimiter.parenthesis",next:"@pop"}]],parenthizedterm:[{include:"@term"},["\\)",{token:"delimiter.parenthesis",next:"@pop"}]],declarationbody:[{include:"@term"},[";","delimiter","@pop"],["(?=})",{token:"",next:"@pop"}]],comments:[["\\/\\*","comment","@comment"],["\\/\\/+.*","comment"]],comment:[["\\*\\/","comment","@pop"],[/[^*/]+/,"comment"],[/./,"comment"]],name:[["@identifier","attribute.value"]],numbers:[["-?(\\d*\\.)?\\d+([eE][\\-+]?\\d+)?",{token:"attribute.value.number",next:"@units"}],["#[0-9a-fA-F_]+(?!\\w)","attribute.value.hex"]],units:[["(em|ex|ch|rem|vmin|vmax|vw|vh|vm|cm|mm|in|px|pt|pc|deg|grad|rad|turn|s|ms|Hz|kHz|%)?","attribute.value.unit","@pop"]],keyframedeclaration:[["@identifier","attribute.value"],["{",{token:"delimiter.bracket",switchTo:"@keyframebody"}]],keyframebody:[{include:"@term"},["{",{token:"delimiter.bracket",next:"@selectorbody"}],["}",{token:"delimiter.bracket",next:"@pop"}]],functioninvocation:[["@identifier\\(",{token:"attribute.value",next:"@functionarguments"}]],functionarguments:[["\\$@identifier@ws:","attribute.name"],["[,]","delimiter"],{include:"@term"},["\\)",{token:"attribute.value",next:"@pop"}]],strings:[['~?"',{token:"string",next:"@stringenddoublequote"}],["~?'",{token:"string",next:"@stringendquote"}]],stringenddoublequote:[["\\\\.","string"],['"',{token:"string",next:"@pop"}],[/[^\\"]+/,"string"],[".","string"]],stringendquote:[["\\\\.","string"],["'",{token:"string",next:"@pop"}],[/[^\\']+/,"string"],[".","string"]]}};export{e as conf,t as language};
|
||||
7
public/apidoc/assets/dart.50deccbd.js
Normal file
7
public/apidoc/assets/dart.50deccbd.js
Normal file
@ -0,0 +1,7 @@
|
||||
/*!-----------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Version: 0.33.0(4b1abad427e58dbedc1215d99a0902ffc885fcd4)
|
||||
* Released under the MIT license
|
||||
* https://github.com/microsoft/monaco-editor/blob/main/LICENSE.txt
|
||||
*-----------------------------------------------------------------------------*/
|
||||
var e={comments:{lineComment:"//",blockComment:["/*","*/"]},brackets:[["{","}"],["[","]"],["(",")"]],autoClosingPairs:[{open:"{",close:"}"},{open:"[",close:"]"},{open:"(",close:")"},{open:"'",close:"'",notIn:["string","comment"]},{open:'"',close:'"',notIn:["string"]},{open:"`",close:"`",notIn:["string","comment"]},{open:"/**",close:" */",notIn:["string"]}],surroundingPairs:[{open:"{",close:"}"},{open:"[",close:"]"},{open:"(",close:")"},{open:"<",close:">"},{open:"'",close:"'"},{open:"(",close:")"},{open:'"',close:'"'},{open:"`",close:"`"}],folding:{markers:{start:/^\s*\s*#?region\b/,end:/^\s*\s*#?endregion\b/}}},t={defaultToken:"invalid",tokenPostfix:".dart",keywords:["abstract","dynamic","implements","show","as","else","import","static","assert","enum","in","super","async","export","interface","switch","await","extends","is","sync","break","external","library","this","case","factory","mixin","throw","catch","false","new","true","class","final","null","try","const","finally","on","typedef","continue","for","operator","var","covariant","Function","part","void","default","get","rethrow","while","deferred","hide","return","with","do","if","set","yield"],typeKeywords:["int","double","String","bool"],operators:["+","-","*","/","~/","%","++","--","==","!=",">","<",">=","<=","=","-=","/=","%=",">>=","^=","+=","*=","~/=","<<=","&=","!=","||","&&","&","|","^","~","<<",">>","!",">>>","??","?",":","|="],symbols:/[=><!~?:&|+\-*\/\^%]+/,escapes:/\\(?:[abfnrtv\\"']|x[0-9A-Fa-f]{1,4}|u[0-9A-Fa-f]{4}|U[0-9A-Fa-f]{8})/,digits:/\d+(_+\d+)*/,octaldigits:/[0-7]+(_+[0-7]+)*/,binarydigits:/[0-1]+(_+[0-1]+)*/,hexdigits:/[[0-9a-fA-F]+(_+[0-9a-fA-F]+)*/,regexpctl:/[(){}\[\]\$\^|\-*+?\.]/,regexpesc:/\\(?:[bBdDfnrstvwWn0\\\/]|@regexpctl|c[A-Z]|x[0-9a-fA-F]{2}|u[0-9a-fA-F]{4})/,tokenizer:{root:[[/[{}]/,"delimiter.bracket"],{include:"common"}],common:[[/[a-z_$][\w$]*/,{cases:{"@typeKeywords":"type.identifier","@keywords":"keyword","@default":"identifier"}}],[/[A-Z_$][\w\$]*/,"type.identifier"],{include:"@whitespace"},[/\/(?=([^\\\/]|\\.)+\/([gimsuy]*)(\s*)(\.|;|,|\)|\]|\}|$))/,{token:"regexp",bracket:"@open",next:"@regexp"}],[/@[a-zA-Z]+/,"annotation"],[/[()\[\]]/,"@brackets"],[/[<>](?!@symbols)/,"@brackets"],[/!(?=([^=]|$))/,"delimiter"],[/@symbols/,{cases:{"@operators":"delimiter","@default":""}}],[/(@digits)[eE]([\-+]?(@digits))?/,"number.float"],[/(@digits)\.(@digits)([eE][\-+]?(@digits))?/,"number.float"],[/0[xX](@hexdigits)n?/,"number.hex"],[/0[oO]?(@octaldigits)n?/,"number.octal"],[/0[bB](@binarydigits)n?/,"number.binary"],[/(@digits)n?/,"number"],[/[;,.]/,"delimiter"],[/"([^"\\]|\\.)*$/,"string.invalid"],[/'([^'\\]|\\.)*$/,"string.invalid"],[/"/,"string","@string_double"],[/'/,"string","@string_single"]],whitespace:[[/[ \t\r\n]+/,""],[/\/\*\*(?!\/)/,"comment.doc","@jsdoc"],[/\/\*/,"comment","@comment"],[/\/\/\/.*$/,"comment.doc"],[/\/\/.*$/,"comment"]],comment:[[/[^\/*]+/,"comment"],[/\*\//,"comment","@pop"],[/[\/*]/,"comment"]],jsdoc:[[/[^\/*]+/,"comment.doc"],[/\*\//,"comment.doc","@pop"],[/[\/*]/,"comment.doc"]],regexp:[[/(\{)(\d+(?:,\d*)?)(\})/,["regexp.escape.control","regexp.escape.control","regexp.escape.control"]],[/(\[)(\^?)(?=(?:[^\]\\\/]|\\.)+)/,["regexp.escape.control",{token:"regexp.escape.control",next:"@regexrange"}]],[/(\()(\?:|\?=|\?!)/,["regexp.escape.control","regexp.escape.control"]],[/[()]/,"regexp.escape.control"],[/@regexpctl/,"regexp.escape.control"],[/[^\\\/]/,"regexp"],[/@regexpesc/,"regexp.escape"],[/\\\./,"regexp.invalid"],[/(\/)([gimsuy]*)/,[{token:"regexp",bracket:"@close",next:"@pop"},"keyword.other"]]],regexrange:[[/-/,"regexp.escape.control"],[/\^/,"regexp.invalid"],[/@regexpesc/,"regexp.escape"],[/[^\]]/,"regexp"],[/\]/,{token:"regexp.escape.control",next:"@pop",bracket:"@close"}]],string_double:[[/[^\\"\$]+/,"string"],[/[^\\"]+/,"string"],[/@escapes/,"string.escape"],[/\\./,"string.escape.invalid"],[/"/,"string","@pop"],[/\$\w+/,"identifier"]],string_single:[[/[^\\'\$]+/,"string"],[/@escapes/,"string.escape"],[/\\./,"string.escape.invalid"],[/'/,"string","@pop"],[/\$\w+/,"identifier"]]}};export{e as conf,t as language};
|
||||
7
public/apidoc/assets/dockerfile.de6a0f2c.js
Normal file
7
public/apidoc/assets/dockerfile.de6a0f2c.js
Normal file
@ -0,0 +1,7 @@
|
||||
/*!-----------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Version: 0.33.0(4b1abad427e58dbedc1215d99a0902ffc885fcd4)
|
||||
* Released under the MIT license
|
||||
* https://github.com/microsoft/monaco-editor/blob/main/LICENSE.txt
|
||||
*-----------------------------------------------------------------------------*/
|
||||
var e={brackets:[["{","}"],["[","]"],["(",")"]],autoClosingPairs:[{open:"{",close:"}"},{open:"[",close:"]"},{open:"(",close:")"},{open:'"',close:'"'},{open:"'",close:"'"}],surroundingPairs:[{open:"{",close:"}"},{open:"[",close:"]"},{open:"(",close:")"},{open:'"',close:'"'},{open:"'",close:"'"}]},o={defaultToken:"",tokenPostfix:".dockerfile",variable:/\${?[\w]+}?/,tokenizer:{root:[{include:"@whitespace"},{include:"@comment"},[/(ONBUILD)(\s+)/,["keyword",""]],[/(ENV)(\s+)([\w]+)/,["keyword","",{token:"variable",next:"@arguments"}]],[/(FROM|MAINTAINER|RUN|EXPOSE|ENV|ADD|ARG|VOLUME|LABEL|USER|WORKDIR|COPY|CMD|STOPSIGNAL|SHELL|HEALTHCHECK|ENTRYPOINT)/,{token:"keyword",next:"@arguments"}]],arguments:[{include:"@whitespace"},{include:"@strings"},[/(@variable)/,{cases:{"@eos":{token:"variable",next:"@popall"},"@default":"variable"}}],[/\\/,{cases:{"@eos":"","@default":""}}],[/./,{cases:{"@eos":{token:"",next:"@popall"},"@default":""}}]],whitespace:[[/\s+/,{cases:{"@eos":{token:"",next:"@popall"},"@default":""}}]],comment:[[/(^#.*$)/,"comment","@popall"]],strings:[[/\\'$/,"","@popall"],[/\\'/,""],[/'$/,"string","@popall"],[/'/,"string","@stringBody"],[/"$/,"string","@popall"],[/"/,"string","@dblStringBody"]],stringBody:[[/[^\\\$']/,{cases:{"@eos":{token:"string",next:"@popall"},"@default":"string"}}],[/\\./,"string.escape"],[/'$/,"string","@popall"],[/'/,"string","@pop"],[/(@variable)/,"variable"],[/\\$/,"string"],[/$/,"string","@popall"]],dblStringBody:[[/[^\\\$"]/,{cases:{"@eos":{token:"string",next:"@popall"},"@default":"string"}}],[/\\./,"string.escape"],[/"$/,"string","@popall"],[/"/,"string","@pop"],[/(@variable)/,"variable"],[/\\$/,"string"],[/$/,"string","@popall"]]}};export{e as conf,o as language};
|
||||
7
public/apidoc/assets/ecl.19ccc34b.js
Normal file
7
public/apidoc/assets/ecl.19ccc34b.js
Normal file
File diff suppressed because one or more lines are too long
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user