Compare commits
541 Commits
4fc7bd47f9
...
refactor/c
| Author | SHA1 | Date | |
|---|---|---|---|
|
24c756a9a8
|
|||
|
7ecb64742f
|
|||
|
3a7140f0a6
|
|||
|
42082fbefa
|
|||
|
bc3d030a1e
|
|||
|
8642737a07
|
|||
|
8181938aaf
|
|||
|
922afc2239
|
|||
|
a071bd2738
|
|||
|
43945fc524
|
|||
|
e477429a35
|
|||
|
fe3a057185
|
|||
|
ad3c104c5c
|
|||
|
2020d625aa
|
|||
|
f471c5635d
|
|||
|
eaeaa28c60
|
|||
|
ee5c7cb7ce
|
|||
|
33abf12e41
|
|||
|
4a71f92ef0
|
|||
|
4faa1a4b64
|
|||
|
e49a1ec49a
|
|||
|
a88f42b26a
|
|||
|
c45be62331
|
|||
|
c8228e0c8e
|
|||
|
c642c6d646
|
|||
|
270c211cb8
|
|||
|
74c8f3490d
|
|||
|
b364edc74b
|
|||
|
9addf38677
|
|||
|
a02ed10434
|
|||
|
aca28f9318
|
|||
|
c2f72993b7
|
|||
|
158cc75c5b
|
|||
|
fa2f53ff7a
|
|||
|
2cce5ebf80
|
|||
|
13b2e46ecc
|
|||
|
cbd68c9ae6
|
|||
|
b99b61e0f9
|
|||
|
94f4e68120
|
|||
|
d5510f7e4d
|
|||
|
c038ab9e3c
|
|||
|
e97719ec84
|
|||
|
40b8ea8eb8
|
|||
|
f9b4dd45d7
|
|||
|
a46de4662c
|
|||
|
fdd14b860e
|
|||
|
cb62df81e2
|
|||
|
46717e39a7
|
|||
|
344ed6e348
|
|||
|
a8b62fb0eb
|
|||
|
00b3087d6a
|
|||
|
78f3873a0c
|
|||
|
a7f4173df7
|
|||
|
f51c3c1724
|
|||
|
a92dc7e140
|
|||
|
c42befed6b
|
|||
|
2b95d58611
|
|||
|
726a752fbb
|
|||
|
2024972832
|
|||
|
d553ca2ca7
|
|||
|
aeef16495f
|
|||
|
9b26a2a7eb
|
|||
|
2317033dae
|
|||
|
fd6e9c9780
|
|||
|
af0a2ff493
|
|||
|
b142a71c32
|
|||
|
27e3cc853a
|
|||
|
590519c28f
|
|||
|
8ccf8100d4
|
|||
|
ec21a94921
|
|||
|
7b7a6c9218
|
|||
|
0e44d9c514
|
|||
|
e449e16d33
|
|||
|
3ce2b36c15
|
|||
|
f7388822e0
|
|||
|
3800dae8b7
|
|||
|
c62ed191f3
|
|||
|
8b77f0e0ad
|
|||
|
2b56c6f1e5
|
|||
|
ef02265ccd
|
|||
|
f4505d2ecc
|
|||
|
9d2242d331
|
|||
|
c806365a81
|
|||
|
bd1715c9a3
|
|||
|
0b0598712e
|
|||
|
92a4899e7c
|
|||
|
bdc8db3091
|
|||
|
a16da37221
|
|||
|
70a18b07ff
|
|||
|
98b8d5f33b
|
|||
|
2a35786204
|
|||
|
7016a0a943
|
|||
|
cad72502d9
|
|||
|
226a64df41
|
|||
|
75b8567a28
|
|||
|
3aa5561a07
|
|||
|
c0ebb496fe
|
|||
|
afccb27bd4
|
|||
|
6ed96780ab
|
|||
|
8e5cdfbc62
|
|||
|
1b774c1de6
|
|||
|
9b4cbade5c
|
|||
|
a52e54f672
|
|||
|
aa48d5e25d
|
|||
|
ce18b194a5
|
|||
|
382579a20e
|
|||
|
18d50346a9
|
|||
|
ac51bbde6c
|
|||
|
4ab0dcf1c2
|
|||
|
587066d847
|
|||
|
faa375042a
|
|||
|
65b6f3a606
|
|||
|
fa1a40c637
|
|||
|
d43ce7cb11
|
|||
|
92b28d830d
|
|||
|
1fa6c893a5
|
|||
|
ba57becba8
|
|||
|
4280168002
|
|||
|
a172128d84
|
|||
|
34e78294a1
|
|||
|
82afdb3922
|
|||
|
260b3e7bc6
|
|||
|
713777cd8a
|
|||
|
5cd09bc2d0
|
|||
|
861fc7cafa
|
|||
|
6313f15375
|
|||
|
337cc1be97
|
|||
|
9b4f61fcda
|
|||
|
6252988390
|
|||
|
aace3b48b1
|
|||
|
5a097c7518
|
|||
|
ba3be1e3bb
|
|||
|
6fd90c424d
|
|||
|
a0ac3b5820
|
|||
|
076bf347c8
|
|||
|
788326381f
|
|||
|
a035b23242
|
|||
|
b29f4fce4d
|
|||
|
5418489f77
|
|||
|
310f2c1497
|
|||
|
0ae8a2cfd4
|
|||
|
c69256bda6
|
|||
|
80ea44f2cc
|
|||
|
b5f9faa724
|
|||
|
05985e0852
|
|||
|
6814b5690e
|
|||
|
78447de1b6
|
|||
|
e54dcccad9
|
|||
|
429a08930f
|
|||
|
b94b288755
|
|||
|
1c50c2f822
|
|||
|
73700e7cfd
|
|||
|
bd2943345a
|
|||
|
1647aa2f1e
|
|||
|
b137021b1f
|
|||
|
ffca94f789
|
|||
|
e2b2bdd262
|
|||
|
ce715cd6b0
|
|||
|
f7b3926338
|
|||
|
68cd23d64f
|
|||
|
db7d994039
|
|||
|
741ed18ce5
|
|||
|
2bfb50cc71
|
|||
|
db98fa240e
|
|||
|
d96937aabc
|
|||
|
dc0be3467f
|
|||
|
6101de741f
|
|||
|
6c8ad05872
|
|||
|
f5b37e9419
|
|||
|
ce5f3434eb
|
|||
|
c08503d2f3
|
|||
|
c8fec66e07
|
|||
|
61b49377a7
|
|||
|
0123c74ab8
|
|||
|
637cc0cfa4
|
|||
|
94a0ec71da
|
|||
|
1351db5482
|
|||
|
3e98ac29b7
|
|||
|
09625335f0
|
|||
|
ee9ad6d87f
|
|||
|
67fc82a8fb
|
|||
|
58e79655e8
|
|||
|
f271681b5d
|
|||
|
3e838cfdb5
|
|||
|
e0e00d023f
|
|||
|
433230b495
|
|||
|
b8fa5f5f24
|
|||
|
091fbd857e
|
|||
|
bfa9bedeea
|
|||
|
74f8221be4
|
|||
|
6817ab6b56
|
|||
|
c74ab20236
|
|||
|
b9edf51f05
|
|||
|
74a9ca98ad
|
|||
|
4bd59f107b
|
|||
|
08f924f647
|
|||
|
5445df3b61
|
|||
|
a377ca2072
|
|||
|
623e7a5771
|
|||
|
0351a2b4fa
|
|||
|
322dee4453
|
|||
|
5e5f4528b9
|
|||
|
70fdc247e7
|
|||
|
8f5f1efa24
|
|||
|
0f15510ac6
|
|||
|
3ce457e9f9
|
|||
|
a9168dcdc5
|
|||
|
4ad63577ba
|
|||
|
47722cfd57
|
|||
|
b46a010e73
|
|||
|
ccd9dbcdbf
|
|||
|
0b65bf8dd7
|
|||
|
ab23f87a66
|
|||
|
8f1047ff5d
|
|||
|
43e50a00ce
|
|||
|
50133684c7
|
|||
|
befde25266
|
|||
|
437f49fb20
|
|||
|
c3b6358f33
|
|||
|
4347281fcd
|
|||
|
92cd6b5f7e
|
|||
|
cf6e534d02
|
|||
|
29c5971554
|
|||
|
cdfc3f6571
|
|||
|
f65a7360e2
|
|||
|
85e706335a
|
|||
|
fe74060df9
|
|||
|
e8d5f22395
|
|||
|
83fa2568aa
|
|||
|
bf1c8e0a85
|
|||
|
323fa8ee15
|
|||
|
e7a46e96ed
|
|||
|
3a0dee11a6
|
|||
|
43be47d526
|
|||
|
48067af034
|
|||
|
7e7e90ad24
|
|||
|
3af4069581
|
|||
|
609b130b4e
|
|||
|
93f7dfd379
|
|||
|
40325c6df5
|
|||
|
bbcaa27ac5
|
|||
|
19d833a522
|
|||
|
a94102e136
|
|||
|
fc693793fe
|
|||
|
8cfdabbae4
|
|||
|
985ff41c72
|
|||
|
a79ea4ac49
|
|||
|
7385caff9a
|
|||
|
15954dbfe2
|
|||
|
4ba6206c9d
|
|||
|
266b9e36e2
|
|||
|
e6aa61b03b
|
|||
|
0c09ef25ec
|
|||
|
dd5929c691
|
|||
|
cf87fdfb49
|
|||
|
ff03584518
|
|||
|
d6c37784e1
|
|||
|
46ebd92dc1
|
|||
|
7f8521bb40
|
|||
|
f01226d91a
|
|||
|
6cb6dee6be
|
|||
|
0e9caf67ff
|
|||
|
ca70bb5487
|
|||
|
59ed135f20
|
|||
|
6077f91529
|
|||
|
5c485bb1c3
|
|||
|
27d979d77b
|
|||
|
15687a0c32
|
|||
|
37ea882ef7
|
|||
|
e624c2bb3e
|
|||
|
9631cd3edd
|
|||
|
f4a659fce5
|
|||
|
1ded811b36
|
|||
|
32977d9580
|
|||
|
aaf29e7228
|
|||
|
658ef3bddf
|
|||
|
fc0bc936ce
|
|||
|
3850ae6a8e
|
|||
|
21c99567b4
|
|||
|
1315c7f4d4
|
|||
|
630a532d98
|
|||
|
b9bb180113
|
|||
|
04d74d0d70
|
|||
|
6a8a0ed491
|
|||
|
0f835845bf
|
|||
|
c5d8a8d07f
|
|||
|
95e2ba1136
|
|||
|
1176fde8b4
|
|||
|
e634968e00
|
|||
|
282a1dbddc
|
|||
|
c64adace24
|
|||
|
8ac0b28c66
|
|||
|
8f71d7f9e5
|
|||
|
c435e63917
|
|||
|
243159e4cc
|
|||
|
42dad7095a
|
|||
|
d1efcdede8
|
|||
|
47680475b3
|
|||
|
6632d43f32
|
|||
|
29c4dcd71c
|
|||
|
e7aa887715
|
|||
|
0f05633996
|
|||
|
966af08a33
|
|||
|
b25b90a074
|
|||
|
dcbefeaaab
|
|||
|
eb83a0392a
|
|||
|
85fefcf724
|
|||
|
d17c26a228
|
|||
|
2e5ef8ff94
|
|||
|
7a5f410e36
|
|||
|
0b4e8a9777
|
|||
|
30fd912281
|
|||
|
5bf58f0194
|
|||
|
8e3e3f09df
|
|||
|
fa24f14c05
|
|||
|
a93b633e84
|
|||
|
97a7b876db
|
|||
|
909fe173c2
|
|||
|
58a44e8af4
|
|||
|
1075177511
|
|||
|
78f8a9e638
|
|||
|
9ce31c4dd8
|
|||
|
e70d8371f8
|
|||
|
51b6f7309e
|
|||
|
d75876a772
|
|||
|
4910c3296b
|
|||
|
7b924fa075
|
|||
|
d69c9f9623
|
|||
|
a88d828e21
|
|||
|
14c93d372e
|
|||
|
adf371a72e
|
|||
|
c03f2472fa
|
|||
|
50efe62bac
|
|||
|
7bc94a9646
|
|||
|
d9fe1273b5
|
|||
|
ff9d490869
|
|||
|
266312e97e
|
|||
|
7087736e31
|
|||
|
82bf1608fd
|
|||
|
3b3287db0b
|
|||
|
4573d9395f
|
|||
|
a8c99b3128
|
|||
|
fdd7bd3c9d
|
|||
|
b785d0098b
|
|||
|
5b31357fe9
|
|||
|
d5a5721402
|
|||
|
204640a759
|
|||
|
e3657386cd
|
|||
|
f81e3dc9f4
|
|||
|
b2a0d25ffa
|
|||
|
e1459951c4
|
|||
|
a88843a4c2
|
|||
|
4d83c2de31
|
|||
|
f63c934cee
|
|||
|
001da9ae40
|
|||
|
4efbfa948a
|
|||
|
3458e85a8b
|
|||
|
3710169f8c
|
|||
|
9e4a58a8a0
|
|||
|
dc93991de2
|
|||
|
b0154e1a63
|
|||
|
66e14ffedb
|
|||
|
b152edb848
|
|||
|
2ace444dbb
|
|||
|
634958ffc5
|
|||
|
1e374a73c7
|
|||
|
cc59e046bd
|
|||
|
f3dcff2e4a
|
|||
|
1a5723c880
|
|||
|
96559a2c26
|
|||
| 366edfc14f | |||
|
f6f0703cb3
|
|||
|
3d47b4e44e
|
|||
|
71fe2a30e7
|
|||
|
d8f57161ae
|
|||
|
3caa79b9a7
|
|||
|
49beb17925
|
|||
|
bd8e13f25d
|
|||
|
1128c9a0ba
|
|||
|
8dfe201afe
|
|||
|
c1016e496a
|
|||
|
091097a858
|
|||
|
5c97733b3e
|
|||
|
4ee387ab76
|
|||
|
19bf17200d
|
|||
|
be6d97ec85
|
|||
|
9d282b26f3
|
|||
|
dbc2c54ab0
|
|||
|
aa062932cf
|
|||
|
812dd03e85
|
|||
|
06d639a114
|
|||
|
74f51036b1
|
|||
|
8308325b73
|
|||
|
fa7010db3d
|
|||
|
89320fc540
|
|||
|
5ec8d89563
|
|||
|
0eeafb5352
|
|||
|
ab2bdcc7ca
|
|||
|
c2b49e6642
|
|||
|
1a89c48790
|
|||
|
8dddfe77cd
|
|||
|
8e8b011fdd
|
|||
|
abd346bb97
|
|||
|
6386ec8caa
|
|||
|
ad062828ff
|
|||
|
92e4988114
|
|||
|
f9269d7558
|
|||
|
fa01b7027a
|
|||
|
eaa3a9c297
|
|||
|
6cedda9307
|
|||
|
942ca73f8d
|
|||
|
da3f58f2ec
|
|||
|
4a8521d59d
|
|||
|
d7ad84e199
|
|||
|
52430c19a5
|
|||
|
9492b6cac6
|
|||
|
5f324a2348
|
|||
|
7452b14817
|
|||
|
4a27794ccc
|
|||
|
d2f5ba36ab
|
|||
| 0117fdf084 | |||
| 02680d224a | |||
| 68bfdebcbd | |||
| 54907eede1 | |||
| a21d19c3ef | |||
| df732616d5 | |||
| 79a31ae060 | |||
| 6eacfcd8f2 | |||
| 5e328509bd | |||
| 9c078db564 | |||
| ddd109c77c | |||
| 3ee04d0b24 | |||
| 7f110313e9 | |||
| bc2e87c56f | |||
| d7271a2d11 | |||
| c57d65db67 | |||
| edf3aab173 | |||
| 352746a141 | |||
| 216c72ea36 | |||
| d0723b366b | |||
| fb6721cb1b | |||
| 9fcb169c94 | |||
| 572874431d | |||
| f595ac8001 | |||
| 18674e0e1d | |||
| da4c4d3a84 | |||
| aec01b117d | |||
| d299c32e35 | |||
| 344007af66 | |||
| d4de5aeac2 | |||
| 8ce5ba50f4 | |||
| 5a44952b27 | |||
| c30946daf6 | |||
| 0221d7b294 | |||
| c44b0b64c3 | |||
| 442ee3bcfd | |||
| 081815c512 | |||
| eab2a388ae | |||
| 5f7ab49abb | |||
| 4ff89173b2 | |||
| f2052410c7 | |||
| 83a49be725 | |||
| 9b205a73fd | |||
| d5157eb7e3 | |||
| 75c92c51db | |||
| 915054fce0 | |||
| 63653680ba | |||
| 84c4df6620 | |||
| 8c748fd57a | |||
| 4684550ebf | |||
| 51db08f374 | |||
| 9f38a288b9 | |||
| 75a975049c | |||
| f8c35c0350 | |||
| d9a5fed77f | |||
| 7cb14940d9 | |||
| 953bf5d4de | |||
| d9620fd6a4 | |||
| 541e2dd14c | |||
| c7925d98c8 | |||
| f759b19bcb | |||
| 5d7429a416 | |||
| fb7e52d6f3 | |||
| 50e888b075 | |||
| 76c8bbf307 | |||
| 8f3825e92c | |||
| d1c3610ec8 | |||
| 4b958a3c31 | |||
| 1f9021d459 | |||
| 7ad9deaf70 | |||
| c1c17b5f4e | |||
| d92220b4bc | |||
| 4d1972bc99 | |||
| 83c052ec4e | |||
| 57a75fe9e6 | |||
| 379bc37aff | |||
| 0217fbb13b | |||
| 4e9943e6a2 | |||
| b3cc623168 | |||
| 3ee5e5367d | |||
| 85fef30c7f | |||
| e8d8dcbb2d | |||
| 3b679d6134 | |||
| ec44b51ab6 | |||
| 2e52a13c30 | |||
| 1e8e2e9ea7 | |||
| 9e8363c004 | |||
| 56c40ee001 | |||
| e3dfccfee3 | |||
| d555fcaf17 | |||
| 2fdefae718 | |||
| e78858b7b4 | |||
| 636b674229 | |||
| fc6cee17d7 | |||
| 7f7b47fb1c | |||
| bf181b88ec | |||
| c056938b6e | |||
| 66eadf96b0 | |||
| 665595b8b4 | |||
| 29550401fd | |||
| 1bb0012c40 | |||
| 2cea391ebf | |||
| 32e91da0b2 | |||
| 69b56b9658 | |||
| 83e3d77f79 | |||
| 38a8eecd50 | |||
| bd77137714 | |||
| 201126e5d0 | |||
| d4a2e5ef5b | |||
| 2761abf405 | |||
| add16ffdad | |||
| b49cd1c382 | |||
| aa9ae5c11e | |||
| 8e8965eb3d | |||
| a0fe8fd0f0 | |||
| 855031a4fe | |||
| adc2b20aeb | |||
| c860f10cf9 | |||
| d441eff2d2 | |||
| d31f36d3dc | |||
| 9e1178b7a1 |
4
.aspire/settings.json
Normal file
4
.aspire/settings.json
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"appHostPath": "../DysonNetwork.Control/DysonNetwork.Control.csproj"
|
||||
}
|
||||
|
||||
5
.editorconfig
Normal file
5
.editorconfig
Normal file
@@ -0,0 +1,5 @@
|
||||
root = true
|
||||
|
||||
[*]
|
||||
indent_style = space
|
||||
indent_size = 4
|
||||
38
.env
Normal file
38
.env
Normal file
@@ -0,0 +1,38 @@
|
||||
# Default container port for ring
|
||||
RING_PORT=8080
|
||||
|
||||
# Default container port for pass
|
||||
PASS_PORT=8080
|
||||
|
||||
# Default container port for drive
|
||||
DRIVE_PORT=8080
|
||||
|
||||
# Default container port for sphere
|
||||
SPHERE_PORT=8080
|
||||
|
||||
# Default container port for develop
|
||||
DEVELOP_PORT=8080
|
||||
|
||||
# Parameter cache-password
|
||||
CACHE_PASSWORD=KS3jSPaU9e
|
||||
|
||||
# Parameter queue-password
|
||||
QUEUE_PASSWORD=8xEECa4ckz
|
||||
|
||||
# Container image name for ring
|
||||
RING_IMAGE=ring:latest
|
||||
|
||||
# Container image name for pass
|
||||
PASS_IMAGE=pass:latest
|
||||
|
||||
# Container image name for drive
|
||||
DRIVE_IMAGE=drive:latest
|
||||
|
||||
# Container image name for sphere
|
||||
SPHERE_IMAGE=sphere:latest
|
||||
|
||||
# Container image name for develop
|
||||
DEVELOP_IMAGE=develop:latest
|
||||
|
||||
# Container image name for gateway
|
||||
GATEWAY_IMAGE=gateway:latest
|
||||
292
.github/workflows/docker-build.yml
vendored
292
.github/workflows/docker-build.yml
vendored
@@ -1,189 +1,103 @@
|
||||
name: Build and Push Microservices
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
build-sphere:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Setup NBGV
|
||||
uses: dotnet/nbgv@master
|
||||
id: nbgv
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
- name: Log in to GitHub Container Registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Build and push DysonNetwork.Sphere Docker image
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
file: DysonNetwork.Sphere/Dockerfile
|
||||
context: .
|
||||
push: true
|
||||
tags: ghcr.io/${{ vars.PACKAGE_OWNER }}/dyson-sphere:latest
|
||||
platforms: linux/amd64
|
||||
|
||||
build-pass:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Setup NBGV
|
||||
uses: dotnet/nbgv@master
|
||||
id: nbgv
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
- name: Log in to GitHub Container Registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Build and push DysonNetwork.Pass Docker image
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
file: DysonNetwork.Pass/Dockerfile
|
||||
context: .
|
||||
push: true
|
||||
tags: ghcr.io/${{ vars.PACKAGE_OWNER }}/dyson-pass:latest
|
||||
platforms: linux/amd64
|
||||
|
||||
build-pusher:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Setup NBGV
|
||||
uses: dotnet/nbgv@master
|
||||
id: nbgv
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
- name: Log in to GitHub Container Registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Build and push DysonNetwork.Pusher Docker image
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
file: DysonNetwork.Pusher/Dockerfile
|
||||
context: .
|
||||
push: true
|
||||
tags: ghcr.io/${{ vars.PACKAGE_OWNER }}/dyson-pusher:latest
|
||||
platforms: linux/amd64
|
||||
|
||||
build-drive:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Setup NBGV
|
||||
uses: dotnet/nbgv@master
|
||||
id: nbgv
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
- name: Log in to GitHub Container Registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Build and push DysonNetwork.Drive Docker image
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
file: DysonNetwork.Drive/Dockerfile
|
||||
context: .
|
||||
push: true
|
||||
tags: ghcr.io/${{ vars.PACKAGE_OWNER }}/dyson-drive:latest
|
||||
platforms: linux/amd64
|
||||
|
||||
build-gateway:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Setup NBGV
|
||||
uses: dotnet/nbgv@master
|
||||
id: nbgv
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
- name: Log in to GitHub Container Registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Build and push DysonNetwork.Gateway Docker image
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
file: DysonNetwork.Gateway/Dockerfile
|
||||
context: .
|
||||
push: true
|
||||
tags: ghcr.io/${{ vars.PACKAGE_OWNER }}/dyson-gateway:latest
|
||||
platforms: linux/amd64
|
||||
|
||||
build-develop:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Setup NBGV
|
||||
uses: dotnet/nbgv@master
|
||||
id: nbgv
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
- name: Log in to GitHub Container Registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Build and push DysonNetwork.Develop Docker image
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
file: DysonNetwork.Develop/Dockerfile
|
||||
context: .
|
||||
push: true
|
||||
tags: ghcr.io/${{ vars.PACKAGE_OWNER }}/dyson-develop:latest
|
||||
platforms: linux/amd64
|
||||
|
||||
name: Build and Push Microservices
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
determine-changes:
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
matrix: ${{ steps.changes.outputs.matrix }}
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Get changed files
|
||||
id: changed-files
|
||||
run: |
|
||||
echo "files=$(git diff --name-only ${{ github.event.before }} ${{ github.sha }} | xargs)" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Determine changed services
|
||||
id: changes
|
||||
run: |
|
||||
files="${{ steps.changed-files.outputs.files }}"
|
||||
matrix="{\"include\":[]}"
|
||||
services=("Sphere" "Pass" "Ring" "Drive" "Develop" "Gateway" "Insight" "Zone")
|
||||
images=("sphere" "pass" "ring" "drive" "develop" "gateway" "insight" "zone")
|
||||
changed_services=()
|
||||
|
||||
for file in $files; do
|
||||
if [[ "$file" == DysonNetwork.Shared/* ]]; then
|
||||
changed_services=("${services[@]}")
|
||||
break
|
||||
fi
|
||||
for i in "${!services[@]}"; do
|
||||
if [[ "$file" == DysonNetwork.${services[$i]}/* ]]; then
|
||||
# check if service is already in changed_services
|
||||
if [[ ! " ${changed_services[@]} " =~ " ${services[$i]} " ]]; then
|
||||
changed_services+=("${services[$i]}")
|
||||
fi
|
||||
fi
|
||||
done
|
||||
done
|
||||
|
||||
if [ ${#changed_services[@]} -gt 0 ]; then
|
||||
json_objects=""
|
||||
for service in "${changed_services[@]}"; do
|
||||
for i in "${!services[@]}"; do
|
||||
if [[ "${services[$i]}" == "$service" ]]; then
|
||||
image="${images[$i]}"
|
||||
break
|
||||
fi
|
||||
done
|
||||
json_objects+="{\"service\":\"$service\",\"image\":\"$image\"},"
|
||||
done
|
||||
matrix="{\"include\":[${json_objects%,}]}"
|
||||
fi
|
||||
echo "matrix=$matrix" >> $GITHUB_OUTPUT
|
||||
|
||||
build-and-push:
|
||||
needs: determine-changes
|
||||
if: ${{ needs.determine-changes.outputs.matrix != '{"include":[]}' }}
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
strategy:
|
||||
matrix: ${{ fromJson(needs.determine-changes.outputs.matrix) }}
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Setup NBGV
|
||||
uses: dotnet/nbgv@master
|
||||
id: nbgv
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Log in to GitHub Container Registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Build and push Docker image for ${{ matrix.service }}
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
file: DysonNetwork.${{ matrix.service }}/Dockerfile
|
||||
push: true
|
||||
tags: |
|
||||
ghcr.io/${{ vars.PACKAGE_OWNER }}/dyson-${{ matrix.image }}:${{ steps.nbgv.outputs.SimpleVersion }}
|
||||
ghcr.io/${{ vars.PACKAGE_OWNER }}/dyson-${{ matrix.image }}:latest
|
||||
platforms: linux/amd64
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -6,3 +6,4 @@ riderModule.iml
|
||||
/_ReSharper.Caches/
|
||||
.idea
|
||||
.DS_Store
|
||||
/Keys/
|
||||
|
||||
613
API_WALLET_FUNDS.md
Normal file
613
API_WALLET_FUNDS.md
Normal file
@@ -0,0 +1,613 @@
|
||||
# Wallet Funds API Documentation
|
||||
|
||||
## Overview
|
||||
|
||||
The Wallet Funds API provides red packet functionality for the DysonNetwork platform, allowing users to create and distribute funds among multiple recipients with expiration and claiming mechanisms.
|
||||
|
||||
## Authentication
|
||||
|
||||
All endpoints require Bearer token authentication:
|
||||
|
||||
```
|
||||
Authorization: Bearer {jwt_token}
|
||||
```
|
||||
|
||||
## Data Types
|
||||
|
||||
### Enums
|
||||
|
||||
#### FundSplitType
|
||||
```typescript
|
||||
enum FundSplitType {
|
||||
Even = 0, // Equal distribution
|
||||
Random = 1 // Lucky draw distribution
|
||||
}
|
||||
```
|
||||
|
||||
#### FundStatus
|
||||
```typescript
|
||||
enum FundStatus {
|
||||
Created = 0, // Fund created, waiting for claims
|
||||
PartiallyReceived = 1, // Some recipients claimed
|
||||
FullyReceived = 2, // All recipients claimed
|
||||
Expired = 3, // Fund expired, unclaimed amounts refunded
|
||||
Refunded = 4 // Legacy status
|
||||
}
|
||||
```
|
||||
|
||||
### Request/Response Models
|
||||
|
||||
#### CreateFundRequest
|
||||
```typescript
|
||||
interface CreateFundRequest {
|
||||
recipientAccountIds: string[]; // UUIDs of recipients
|
||||
currency: string; // e.g., "points", "golds"
|
||||
totalAmount: number; // Total amount to distribute
|
||||
splitType: FundSplitType; // Even or Random
|
||||
message?: string; // Optional message
|
||||
expirationHours?: number; // Optional: hours until expiration (default: 24)
|
||||
pinCode: string; // Required: 6-digit PIN code for security
|
||||
}
|
||||
```
|
||||
|
||||
#### SnWalletFund
|
||||
```typescript
|
||||
interface SnWalletFund {
|
||||
id: string; // UUID
|
||||
currency: string;
|
||||
totalAmount: number;
|
||||
splitType: FundSplitType;
|
||||
status: FundStatus;
|
||||
message?: string;
|
||||
creatorAccountId: string; // UUID
|
||||
creatorAccount: SnAccount; // Creator account details (includes profile)
|
||||
recipients: SnWalletFundRecipient[];
|
||||
expiredAt: string; // ISO 8601 timestamp
|
||||
createdAt: string; // ISO 8601 timestamp
|
||||
updatedAt: string; // ISO 8601 timestamp
|
||||
}
|
||||
```
|
||||
|
||||
#### SnWalletFundRecipient
|
||||
```typescript
|
||||
interface SnWalletFundRecipient {
|
||||
id: string; // UUID
|
||||
fundId: string; // UUID
|
||||
recipientAccountId: string; // UUID
|
||||
recipientAccount: SnAccount; // Recipient account details (includes profile)
|
||||
amount: number; // Allocated amount
|
||||
isReceived: boolean;
|
||||
receivedAt?: string; // ISO 8601 timestamp (if claimed)
|
||||
createdAt: string; // ISO 8601 timestamp
|
||||
updatedAt: string; // ISO 8601 timestamp
|
||||
}
|
||||
```
|
||||
|
||||
#### SnWalletTransaction
|
||||
```typescript
|
||||
interface SnWalletTransaction {
|
||||
id: string; // UUID
|
||||
payerWalletId?: string; // UUID (null for system transfers)
|
||||
payeeWalletId?: string; // UUID (null for system transfers)
|
||||
currency: string;
|
||||
amount: number;
|
||||
remarks?: string;
|
||||
type: TransactionType;
|
||||
createdAt: string; // ISO 8601 timestamp
|
||||
updatedAt: string; // ISO 8601 timestamp
|
||||
}
|
||||
```
|
||||
|
||||
#### Error Response
|
||||
```typescript
|
||||
interface ErrorResponse {
|
||||
type: string; // Error type
|
||||
title: string; // Error title
|
||||
status: number; // HTTP status code
|
||||
detail: string; // Error details
|
||||
instance?: string; // Request instance
|
||||
}
|
||||
```
|
||||
|
||||
## API Endpoints
|
||||
|
||||
### 1. Create Fund
|
||||
|
||||
Creates a new fund (red packet) for distribution among recipients.
|
||||
|
||||
**Endpoint:** `POST /api/wallets/funds`
|
||||
|
||||
**Request Body:** `CreateFundRequest`
|
||||
|
||||
**Response:** `SnWalletFund` (201 Created)
|
||||
|
||||
**Example Request:**
|
||||
```bash
|
||||
curl -X POST "/api/wallets/funds" \
|
||||
-H "Authorization: Bearer {token}" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"recipientAccountIds": [
|
||||
"550e8400-e29b-41d4-a716-446655440000",
|
||||
"550e8400-e29b-41d4-a716-446655440001",
|
||||
"550e8400-e29b-41d4-a716-446655440002"
|
||||
],
|
||||
"currency": "points",
|
||||
"totalAmount": 100.00,
|
||||
"splitType": "Even",
|
||||
"message": "Happy New Year! 🎉",
|
||||
"expirationHours": 48,
|
||||
"pinCode": "123456"
|
||||
}'
|
||||
```
|
||||
|
||||
**Example Response:**
|
||||
```json
|
||||
{
|
||||
"id": "550e8400-e29b-41d4-a716-446655440003",
|
||||
"currency": "points",
|
||||
"totalAmount": 100.00,
|
||||
"splitType": 0,
|
||||
"status": 0,
|
||||
"message": "Happy New Year! 🎉",
|
||||
"creatorAccountId": "550e8400-e29b-41d4-a716-446655440004",
|
||||
"creatorAccount": {
|
||||
"id": "550e8400-e29b-41d4-a716-446655440004",
|
||||
"username": "creator_user"
|
||||
},
|
||||
"recipients": [
|
||||
{
|
||||
"id": "550e8400-e29b-41d4-a716-446655440005",
|
||||
"fundId": "550e8400-e29b-41d4-a716-446655440003",
|
||||
"recipientAccountId": "550e8400-e29b-41d4-a716-446655440000",
|
||||
"amount": 33.34,
|
||||
"isReceived": false,
|
||||
"createdAt": "2025-10-03T22:00:00Z",
|
||||
"updatedAt": "2025-10-03T22:00:00Z"
|
||||
},
|
||||
{
|
||||
"id": "550e8400-e29b-41d4-a716-446655440006",
|
||||
"fundId": "550e8400-e29b-41d4-a716-446655440003",
|
||||
"recipientAccountId": "550e8400-e29b-41d4-a716-446655440001",
|
||||
"amount": 33.33,
|
||||
"isReceived": false,
|
||||
"createdAt": "2025-10-03T22:00:00Z",
|
||||
"updatedAt": "2025-10-03T22:00:00Z"
|
||||
},
|
||||
{
|
||||
"id": "550e8400-e29b-41d4-a716-446655440007",
|
||||
"fundId": "550e8400-e29b-41d4-a716-446655440003",
|
||||
"recipientAccountId": "550e8400-e29b-41d4-a716-446655440002",
|
||||
"amount": 33.33,
|
||||
"isReceived": false,
|
||||
"createdAt": "2025-10-03T22:00:00Z",
|
||||
"updatedAt": "2025-10-03T22:00:00Z"
|
||||
}
|
||||
],
|
||||
"expiredAt": "2025-10-05T22:00:00Z",
|
||||
"createdAt": "2025-10-03T22:00:00Z",
|
||||
"updatedAt": "2025-10-03T22:00:00Z"
|
||||
}
|
||||
```
|
||||
|
||||
**Error Responses:**
|
||||
- `400 Bad Request`: Invalid parameters, insufficient funds, invalid recipients
|
||||
- `401 Unauthorized`: Missing or invalid authentication
|
||||
- `403 Forbidden`: Invalid PIN code
|
||||
- `422 Unprocessable Entity`: Business logic violations
|
||||
|
||||
---
|
||||
|
||||
### 2. Get Funds
|
||||
|
||||
Retrieves funds that the authenticated user is involved in (as creator or recipient).
|
||||
|
||||
**Endpoint:** `GET /api/wallets/funds`
|
||||
|
||||
**Query Parameters:**
|
||||
- `offset` (number, optional): Pagination offset (default: 0)
|
||||
- `take` (number, optional): Number of items to return (default: 20, max: 100)
|
||||
- `status` (FundStatus, optional): Filter by fund status
|
||||
|
||||
**Response:** `SnWalletFund[]` (200 OK)
|
||||
|
||||
**Headers:**
|
||||
- `X-Total`: Total number of funds matching the criteria
|
||||
|
||||
**Example Request:**
|
||||
```bash
|
||||
curl -X GET "/api/wallets/funds?offset=0&take=10&status=0" \
|
||||
-H "Authorization: Bearer {token}"
|
||||
```
|
||||
|
||||
**Example Response:**
|
||||
```json
|
||||
[
|
||||
{
|
||||
"id": "550e8400-e29b-41d4-a716-446655440003",
|
||||
"currency": "points",
|
||||
"totalAmount": 100.00,
|
||||
"splitType": 0,
|
||||
"status": 0,
|
||||
"message": "Happy New Year! 🎉",
|
||||
"creatorAccountId": "550e8400-e29b-41d4-a716-446655440004",
|
||||
"creatorAccount": {
|
||||
"id": "550e8400-e29b-41d4-a716-446655440004",
|
||||
"username": "creator_user"
|
||||
},
|
||||
"recipients": [
|
||||
{
|
||||
"id": "550e8400-e29b-41d4-a716-446655440005",
|
||||
"fundId": "550e8400-e29b-41d4-a716-446655440003",
|
||||
"recipientAccountId": "550e8400-e29b-41d4-a716-446655440000",
|
||||
"amount": 33.34,
|
||||
"isReceived": false
|
||||
}
|
||||
],
|
||||
"expiredAt": "2025-10-05T22:00:00Z",
|
||||
"createdAt": "2025-10-03T22:00:00Z",
|
||||
"updatedAt": "2025-10-03T22:00:00Z"
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
**Error Responses:**
|
||||
- `401 Unauthorized`: Missing or invalid authentication
|
||||
|
||||
---
|
||||
|
||||
### 3. Get Fund
|
||||
|
||||
Retrieves details of a specific fund.
|
||||
|
||||
**Endpoint:** `GET /api/wallets/funds/{id}`
|
||||
|
||||
**Path Parameters:**
|
||||
- `id` (string): Fund UUID
|
||||
|
||||
**Response:** `SnWalletFund` (200 OK)
|
||||
|
||||
**Example Request:**
|
||||
```bash
|
||||
curl -X GET "/api/wallets/funds/550e8400-e29b-41d4-a716-446655440003" \
|
||||
-H "Authorization: Bearer {token}"
|
||||
```
|
||||
|
||||
**Example Response:** (Same as create fund response)
|
||||
|
||||
**Error Responses:**
|
||||
- `401 Unauthorized`: Missing or invalid authentication
|
||||
- `403 Forbidden`: User doesn't have permission to view this fund
|
||||
- `404 Not Found`: Fund not found
|
||||
|
||||
---
|
||||
|
||||
### 4. Receive Fund
|
||||
|
||||
Claims the authenticated user's portion of a fund.
|
||||
|
||||
**Endpoint:** `POST /api/wallets/funds/{id}/receive`
|
||||
|
||||
**Path Parameters:**
|
||||
- `id` (string): Fund UUID
|
||||
|
||||
**Response:** `SnWalletTransaction` (200 OK)
|
||||
|
||||
**Example Request:**
|
||||
```bash
|
||||
curl -X POST "/api/wallets/funds/550e8400-e29b-41d4-a716-446655440003/receive" \
|
||||
-H "Authorization: Bearer {token}"
|
||||
```
|
||||
|
||||
**Example Response:**
|
||||
```json
|
||||
{
|
||||
"id": "550e8400-e29b-41d4-a716-446655440008",
|
||||
"payerWalletId": null,
|
||||
"payeeWalletId": "550e8400-e29b-41d4-a716-446655440009",
|
||||
"currency": "points",
|
||||
"amount": 33.34,
|
||||
"remarks": "Received fund portion from 550e8400-e29b-41d4-a716-446655440004",
|
||||
"type": 1,
|
||||
"createdAt": "2025-10-03T22:05:00Z",
|
||||
"updatedAt": "2025-10-03T22:05:00Z"
|
||||
}
|
||||
```
|
||||
|
||||
**Error Responses:**
|
||||
- `400 Bad Request`: Fund expired, already claimed, not a recipient
|
||||
- `401 Unauthorized`: Missing or invalid authentication
|
||||
- `404 Not Found`: Fund not found
|
||||
|
||||
---
|
||||
|
||||
### 5. Get Wallet Overview
|
||||
|
||||
Retrieves a summarized overview of wallet transactions grouped by type for graphing/charting purposes.
|
||||
|
||||
**Endpoint:** `GET /api/wallets/overview`
|
||||
|
||||
**Query Parameters:**
|
||||
- `startDate` (string, optional): Start date in ISO 8601 format (e.g., "2025-01-01T00:00:00Z")
|
||||
- `endDate` (string, optional): End date in ISO 8601 format (e.g., "2025-12-31T23:59:59Z")
|
||||
|
||||
**Response:** `WalletOverview` (200 OK)
|
||||
|
||||
**Example Request:**
|
||||
```bash
|
||||
curl -X GET "/api/wallets/overview?startDate=2025-01-01T00:00:00Z&endDate=2025-12-31T23:59:59Z" \
|
||||
-H "Authorization: Bearer {token}"
|
||||
```
|
||||
|
||||
**Example Response:**
|
||||
```json
|
||||
{
|
||||
"accountId": "550e8400-e29b-41d4-a716-446655440000",
|
||||
"startDate": "2025-01-01T00:00:00.0000000Z",
|
||||
"endDate": "2025-12-31T23:59:59.0000000Z",
|
||||
"summary": {
|
||||
"System": {
|
||||
"type": "System",
|
||||
"currencies": {
|
||||
"points": {
|
||||
"currency": "points",
|
||||
"income": 150.00,
|
||||
"spending": 0.00,
|
||||
"net": 150.00
|
||||
}
|
||||
}
|
||||
},
|
||||
"Transfer": {
|
||||
"type": "Transfer",
|
||||
"currencies": {
|
||||
"points": {
|
||||
"currency": "points",
|
||||
"income": 25.00,
|
||||
"spending": 75.00,
|
||||
"net": -50.00
|
||||
},
|
||||
"golds": {
|
||||
"currency": "golds",
|
||||
"income": 0.00,
|
||||
"spending": 10.00,
|
||||
"net": -10.00
|
||||
}
|
||||
}
|
||||
},
|
||||
"Order": {
|
||||
"type": "Order",
|
||||
"currencies": {
|
||||
"points": {
|
||||
"currency": "points",
|
||||
"income": 0.00,
|
||||
"spending": 200.00,
|
||||
"net": -200.00
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"totalIncome": 175.00,
|
||||
"totalSpending": 285.00,
|
||||
"netTotal": -110.00
|
||||
}
|
||||
```
|
||||
|
||||
**Response Fields:**
|
||||
- `accountId`: User's account UUID
|
||||
- `startDate`/`endDate`: Date range applied (ISO 8601 format)
|
||||
- `summary`: Object keyed by transaction type
|
||||
- `type`: Transaction type name
|
||||
- `currencies`: Object keyed by currency code
|
||||
- `currency`: Currency name
|
||||
- `income`: Total money received
|
||||
- `spending`: Total money spent
|
||||
- `net`: Income minus spending
|
||||
- `totalIncome`: Sum of all income across all types/currencies
|
||||
- `totalSpending`: Sum of all spending across all types/currencies
|
||||
- `netTotal`: Overall net (totalIncome - totalSpending)
|
||||
|
||||
**Error Responses:**
|
||||
- `401 Unauthorized`: Missing or invalid authentication
|
||||
|
||||
## Error Codes
|
||||
|
||||
### Common Error Types
|
||||
|
||||
#### Validation Errors
|
||||
```json
|
||||
{
|
||||
"type": "https://tools.ietf.org/html/rfc9110#section-15.5.1",
|
||||
"title": "Bad Request",
|
||||
"status": 400,
|
||||
"detail": "At least one recipient is required",
|
||||
"instance": "/api/wallets/funds"
|
||||
}
|
||||
```
|
||||
|
||||
#### Insufficient Funds
|
||||
```json
|
||||
{
|
||||
"type": "https://tools.ietf.org/html/rfc9110#section-15.5.1",
|
||||
"title": "Bad Request",
|
||||
"status": 400,
|
||||
"detail": "Insufficient funds",
|
||||
"instance": "/api/wallets/funds"
|
||||
}
|
||||
```
|
||||
|
||||
#### Fund Not Available
|
||||
```json
|
||||
{
|
||||
"type": "https://tools.ietf.org/html/rfc9110#section-15.5.1",
|
||||
"title": "Bad Request",
|
||||
"status": 400,
|
||||
"detail": "Fund is no longer available",
|
||||
"instance": "/api/wallets/funds/550e8400-e29b-41d4-a716-446655440003/receive"
|
||||
}
|
||||
```
|
||||
|
||||
#### Already Claimed
|
||||
```json
|
||||
{
|
||||
"type": "https://tools.ietf.org/html/rfc9110#section-15.5.1",
|
||||
"title": "Bad Request",
|
||||
"status": 400,
|
||||
"detail": "You have already received this fund",
|
||||
"instance": "/api/wallets/funds/550e8400-e29b-41d4-a716-446655440003/receive"
|
||||
}
|
||||
```
|
||||
|
||||
## Rate Limiting
|
||||
|
||||
- **Create Fund**: 10 requests per minute per user
|
||||
- **Get Funds**: 60 requests per minute per user
|
||||
- **Get Fund**: 60 requests per minute per user
|
||||
- **Receive Fund**: 30 requests per minute per user
|
||||
|
||||
## Webhooks/Notifications
|
||||
|
||||
The system integrates with the platform's notification system:
|
||||
|
||||
- **Fund Created**: Creator receives confirmation
|
||||
- **Fund Claimed**: Creator receives notification when someone claims
|
||||
- **Fund Expired**: Creator receives refund notification
|
||||
|
||||
## SDK Examples
|
||||
|
||||
### JavaScript/TypeScript
|
||||
|
||||
```typescript
|
||||
// Create a fund
|
||||
const createFund = async (fundData: CreateFundRequest): Promise<SnWalletFund> => {
|
||||
const response = await fetch('/api/wallets/funds', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(fundData)
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
|
||||
return response.json();
|
||||
};
|
||||
|
||||
// Get user's funds
|
||||
const getFunds = async (params?: {
|
||||
offset?: number;
|
||||
take?: number;
|
||||
status?: FundStatus;
|
||||
}): Promise<SnWalletFund[]> => {
|
||||
const queryParams = new URLSearchParams();
|
||||
if (params?.offset) queryParams.set('offset', params.offset.toString());
|
||||
if (params?.take) queryParams.set('take', params.take.toString());
|
||||
if (params?.status !== undefined) queryParams.set('status', params.status.toString());
|
||||
|
||||
const response = await fetch(`/api/wallets/funds?${queryParams}`, {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`
|
||||
}
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
|
||||
return response.json();
|
||||
};
|
||||
|
||||
// Claim a fund
|
||||
const receiveFund = async (fundId: string): Promise<SnWalletTransaction> => {
|
||||
const response = await fetch(`/api/wallets/funds/${fundId}/receive`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`
|
||||
}
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
|
||||
return response.json();
|
||||
};
|
||||
```
|
||||
|
||||
### Python
|
||||
|
||||
```python
|
||||
import requests
|
||||
from typing import List, Optional
|
||||
from enum import Enum
|
||||
|
||||
class FundSplitType(Enum):
|
||||
EVEN = 0
|
||||
RANDOM = 1
|
||||
|
||||
class FundStatus(Enum):
|
||||
CREATED = 0
|
||||
PARTIALLY_RECEIVED = 1
|
||||
FULLY_RECEIVED = 2
|
||||
EXPIRED = 3
|
||||
REFUNDED = 4
|
||||
|
||||
def create_fund(token: str, fund_data: dict) -> dict:
|
||||
"""Create a new fund"""
|
||||
response = requests.post(
|
||||
'/api/wallets/funds',
|
||||
json=fund_data,
|
||||
headers={
|
||||
'Authorization': f'Bearer {token}',
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
)
|
||||
response.raise_for_status()
|
||||
return response.json()
|
||||
|
||||
def get_funds(
|
||||
token: str,
|
||||
offset: int = 0,
|
||||
take: int = 20,
|
||||
status: Optional[FundStatus] = None
|
||||
) -> List[dict]:
|
||||
"""Get user's funds"""
|
||||
params = {'offset': offset, 'take': take}
|
||||
if status is not None:
|
||||
params['status'] = status.value
|
||||
|
||||
response = requests.get(
|
||||
'/api/wallets/funds',
|
||||
params=params,
|
||||
headers={'Authorization': f'Bearer {token}'}
|
||||
)
|
||||
response.raise_for_status()
|
||||
return response.json()
|
||||
|
||||
def receive_fund(token: str, fund_id: str) -> dict:
|
||||
"""Claim a fund portion"""
|
||||
response = requests.post(
|
||||
f'/api/wallets/funds/{fund_id}/receive',
|
||||
headers={'Authorization': f'Bearer {token}'}
|
||||
)
|
||||
response.raise_for_status()
|
||||
return response.json()
|
||||
```
|
||||
|
||||
## Changelog
|
||||
|
||||
### Version 1.0.0
|
||||
- Initial release with basic red packet functionality
|
||||
- Support for even and random split types
|
||||
- 24-hour expiration with automatic refunds
|
||||
- RESTful API endpoints
|
||||
- Comprehensive error handling
|
||||
|
||||
## Support
|
||||
|
||||
For API support or questions:
|
||||
- Check the main documentation at `README_WALLET_FUNDS.md`
|
||||
- Review error messages for specific guidance
|
||||
- Contact the development team for technical issues
|
||||
@@ -1,6 +1,8 @@
|
||||
using DysonNetwork.Develop.Identity;
|
||||
using DysonNetwork.Shared.Data;
|
||||
using DysonNetwork.Shared.Models;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Design;
|
||||
using NodaTime;
|
||||
|
||||
namespace DysonNetwork.Develop;
|
||||
|
||||
@@ -9,10 +11,13 @@ public class AppDatabase(
|
||||
IConfiguration configuration
|
||||
) : DbContext(options)
|
||||
{
|
||||
public DbSet<Developer> Developers { get; set; } = null!;
|
||||
public DbSet<SnDeveloper> Developers { get; set; } = null!;
|
||||
|
||||
public DbSet<CustomApp> CustomApps { get; set; } = null!;
|
||||
public DbSet<CustomAppSecret> CustomAppSecrets { get; set; } = null!;
|
||||
public DbSet<SnDevProject> DevProjects { get; set; } = null!;
|
||||
|
||||
public DbSet<SnCustomApp> CustomApps { get; set; } = null!;
|
||||
public DbSet<SnCustomAppSecret> CustomAppSecrets { get; set; } = null!;
|
||||
public DbSet<SnBotAccount> BotAccounts { get; set; } = null!;
|
||||
|
||||
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
|
||||
{
|
||||
@@ -26,10 +31,18 @@ public class AppDatabase(
|
||||
|
||||
base.OnConfiguring(optionsBuilder);
|
||||
}
|
||||
|
||||
public override async Task<int> SaveChangesAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
this.ApplyAuditableAndSoftDelete();
|
||||
return await base.SaveChangesAsync(cancellationToken);
|
||||
}
|
||||
|
||||
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||
{
|
||||
base.OnModelCreating(modelBuilder);
|
||||
|
||||
modelBuilder.ApplySoftDeleteFilters();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
FROM mcr.microsoft.com/dotnet/aspnet:9.0 AS base
|
||||
FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS base
|
||||
USER $APP_UID
|
||||
WORKDIR /app
|
||||
EXPOSE 8080
|
||||
EXPOSE 8081
|
||||
|
||||
FROM mcr.microsoft.com/dotnet/sdk:9.0 AS build
|
||||
FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
|
||||
ARG BUILD_CONFIGURATION=Release
|
||||
WORKDIR /src
|
||||
COPY ["DysonNetwork.Develop/DysonNetwork.Develop.csproj", "DysonNetwork.Develop/"]
|
||||
|
||||
@@ -1,24 +1,19 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="EFCore.NamingConventions" Version="9.0.0" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="9.0.7"/>
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.7">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="10.0.0" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.11">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="NodaTime.Serialization.Protobuf" Version="2.0.2" />
|
||||
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="9.0.4"/>
|
||||
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL.NodaTime" Version="9.0.4" />
|
||||
<PackageReference Include="prometheus-net.AspNetCore" Version="8.2.1"/>
|
||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="9.0.3"/>
|
||||
<PackageReference Include="NodaTime" Version="3.2.2"/>
|
||||
<PackageReference Include="NodaTime.Serialization.SystemTextJson" Version="1.3.0"/>
|
||||
<PackageReference Include="Grpc.AspNetCore.Server" Version="2.71.0"/>
|
||||
|
||||
460
DysonNetwork.Develop/Identity/BotAccountController.cs
Normal file
460
DysonNetwork.Develop/Identity/BotAccountController.cs
Normal file
@@ -0,0 +1,460 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using DysonNetwork.Develop.Project;
|
||||
using DysonNetwork.Shared.Models;
|
||||
using DysonNetwork.Shared.Proto;
|
||||
using DysonNetwork.Shared.Registry;
|
||||
using Grpc.Core;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using NodaTime;
|
||||
using NodaTime.Serialization.Protobuf;
|
||||
|
||||
namespace DysonNetwork.Develop.Identity;
|
||||
|
||||
[ApiController]
|
||||
[Route("/api/developers/{pubName}/projects/{projectId:guid}/bots")]
|
||||
[Authorize]
|
||||
public class BotAccountController(
|
||||
BotAccountService botService,
|
||||
DeveloperService ds,
|
||||
DevProjectService projectService,
|
||||
ILogger<BotAccountController> logger,
|
||||
RemoteAccountService remoteAccounts,
|
||||
BotAccountReceiverService.BotAccountReceiverServiceClient accountsReceiver
|
||||
)
|
||||
: ControllerBase
|
||||
{
|
||||
public class CommonBotRequest
|
||||
{
|
||||
[MaxLength(256)] public string? FirstName { get; set; }
|
||||
[MaxLength(256)] public string? MiddleName { get; set; }
|
||||
[MaxLength(256)] public string? LastName { get; set; }
|
||||
[MaxLength(1024)] public string? Gender { get; set; }
|
||||
[MaxLength(1024)] public string? Pronouns { get; set; }
|
||||
[MaxLength(1024)] public string? TimeZone { get; set; }
|
||||
[MaxLength(1024)] public string? Location { get; set; }
|
||||
[MaxLength(4096)] public string? Bio { get; set; }
|
||||
public Instant? Birthday { get; set; }
|
||||
|
||||
[MaxLength(32)] public string? PictureId { get; set; }
|
||||
[MaxLength(32)] public string? BackgroundId { get; set; }
|
||||
}
|
||||
|
||||
public class BotCreateRequest : CommonBotRequest
|
||||
{
|
||||
[Required]
|
||||
[MinLength(2)]
|
||||
[MaxLength(256)]
|
||||
[RegularExpression(@"^[A-Za-z0-9_-]+$",
|
||||
ErrorMessage = "Name can only contain letters, numbers, underscores, and hyphens.")
|
||||
]
|
||||
public string Name { get; set; } = string.Empty;
|
||||
|
||||
[Required][MaxLength(256)] public string Nick { get; set; } = string.Empty;
|
||||
|
||||
[Required][MaxLength(1024)] public string Slug { get; set; } = string.Empty;
|
||||
|
||||
[MaxLength(128)] public string Language { get; set; } = "en-us";
|
||||
}
|
||||
|
||||
public class UpdateBotRequest : CommonBotRequest
|
||||
{
|
||||
[MinLength(2)]
|
||||
[MaxLength(256)]
|
||||
[RegularExpression(@"^[A-Za-z0-9_-]+$",
|
||||
ErrorMessage = "Name can only contain letters, numbers, underscores, and hyphens.")
|
||||
]
|
||||
public string? Name { get; set; } = string.Empty;
|
||||
|
||||
[MaxLength(256)] public string? Nick { get; set; } = string.Empty;
|
||||
|
||||
[Required][MaxLength(1024)] public string? Slug { get; set; } = string.Empty;
|
||||
|
||||
[MaxLength(128)] public string? Language { get; set; }
|
||||
|
||||
public bool? IsActive { get; set; }
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
public async Task<IActionResult> ListBots(
|
||||
[FromRoute] string pubName,
|
||||
[FromRoute] Guid projectId)
|
||||
{
|
||||
if (HttpContext.Items["CurrentUser"] is not Account currentUser)
|
||||
return Unauthorized();
|
||||
|
||||
var developer = await ds.GetDeveloperByName(pubName);
|
||||
if (developer is null)
|
||||
return NotFound("Developer not found");
|
||||
|
||||
if (!await ds.IsMemberWithRole(developer.PublisherId, Guid.Parse(currentUser.Id),
|
||||
Shared.Proto.PublisherMemberRole.Viewer))
|
||||
return StatusCode(403, "You must be an viewer of the developer to list bots");
|
||||
|
||||
var project = await projectService.GetProjectAsync(projectId, developer.Id);
|
||||
if (project is null)
|
||||
return NotFound("Project not found or you don't have access");
|
||||
|
||||
var bots = await botService.GetBotsByProjectAsync(projectId);
|
||||
return Ok(await botService.LoadBotsAccountAsync(bots));
|
||||
}
|
||||
|
||||
[HttpGet("{botId:guid}")]
|
||||
public async Task<IActionResult> GetBot(
|
||||
[FromRoute] string pubName,
|
||||
[FromRoute] Guid projectId,
|
||||
[FromRoute] Guid botId)
|
||||
{
|
||||
if (HttpContext.Items["CurrentUser"] is not Account currentUser)
|
||||
return Unauthorized();
|
||||
|
||||
var developer = await ds.GetDeveloperByName(pubName);
|
||||
if (developer is null)
|
||||
return NotFound("Developer not found");
|
||||
|
||||
if (!await ds.IsMemberWithRole(developer.PublisherId, Guid.Parse(currentUser.Id),
|
||||
Shared.Proto.PublisherMemberRole.Viewer))
|
||||
return StatusCode(403, "You must be an viewer of the developer to view bot details");
|
||||
|
||||
var project = await projectService.GetProjectAsync(projectId, developer.Id);
|
||||
if (project is null)
|
||||
return NotFound("Project not found or you don't have access");
|
||||
|
||||
var bot = await botService.GetBotByIdAsync(botId);
|
||||
if (bot is null || bot.ProjectId != projectId)
|
||||
return NotFound("Bot not found");
|
||||
|
||||
return Ok(await botService.LoadBotAccountAsync(bot));
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
public async Task<IActionResult> CreateBot(
|
||||
[FromRoute] string pubName,
|
||||
[FromRoute] Guid projectId,
|
||||
[FromBody] BotCreateRequest createRequest
|
||||
)
|
||||
{
|
||||
if (HttpContext.Items["CurrentUser"] is not Account currentUser)
|
||||
return Unauthorized();
|
||||
|
||||
var developer = await ds.GetDeveloperByName(pubName);
|
||||
if (developer is null)
|
||||
return NotFound("Developer not found");
|
||||
|
||||
if (!await ds.IsMemberWithRole(developer.PublisherId, Guid.Parse(currentUser.Id),
|
||||
Shared.Proto.PublisherMemberRole.Editor))
|
||||
return StatusCode(403, "You must be an editor of the developer to create a bot");
|
||||
|
||||
var project = await projectService.GetProjectAsync(projectId, developer.Id);
|
||||
if (project is null)
|
||||
return NotFound("Project not found or you don't have access");
|
||||
|
||||
var now = SystemClock.Instance.GetCurrentInstant();
|
||||
var accountId = Guid.NewGuid();
|
||||
var account = new Account()
|
||||
{
|
||||
Id = accountId.ToString(),
|
||||
Name = createRequest.Name,
|
||||
Nick = createRequest.Nick,
|
||||
Language = createRequest.Language,
|
||||
Profile = new AccountProfile()
|
||||
{
|
||||
Id = Guid.NewGuid().ToString(),
|
||||
Bio = createRequest.Bio,
|
||||
Gender = createRequest.Gender,
|
||||
FirstName = createRequest.FirstName,
|
||||
MiddleName = createRequest.MiddleName,
|
||||
LastName = createRequest.LastName,
|
||||
TimeZone = createRequest.TimeZone,
|
||||
Pronouns = createRequest.Pronouns,
|
||||
Location = createRequest.Location,
|
||||
Birthday = createRequest.Birthday?.ToTimestamp(),
|
||||
AccountId = accountId.ToString(),
|
||||
CreatedAt = now.ToTimestamp(),
|
||||
UpdatedAt = now.ToTimestamp()
|
||||
},
|
||||
CreatedAt = now.ToTimestamp(),
|
||||
UpdatedAt = now.ToTimestamp()
|
||||
};
|
||||
|
||||
try
|
||||
{
|
||||
var bot = await botService.CreateBotAsync(
|
||||
project,
|
||||
createRequest.Slug,
|
||||
account,
|
||||
createRequest.PictureId,
|
||||
createRequest.BackgroundId
|
||||
);
|
||||
return Ok(bot);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError(ex, "Error creating bot account");
|
||||
return StatusCode(500, "An error occurred while creating the bot account");
|
||||
}
|
||||
}
|
||||
|
||||
[HttpPatch("{botId:guid}")]
|
||||
public async Task<IActionResult> UpdateBot(
|
||||
[FromRoute] string pubName,
|
||||
[FromRoute] Guid projectId,
|
||||
[FromRoute] Guid botId,
|
||||
[FromBody] UpdateBotRequest request
|
||||
)
|
||||
{
|
||||
if (HttpContext.Items["CurrentUser"] is not Account currentUser)
|
||||
return Unauthorized();
|
||||
|
||||
var developer = await ds.GetDeveloperByName(pubName);
|
||||
if (developer is null)
|
||||
return NotFound("Developer not found");
|
||||
|
||||
if (!await ds.IsMemberWithRole(developer.PublisherId, Guid.Parse(currentUser.Id),
|
||||
Shared.Proto.PublisherMemberRole.Editor))
|
||||
return StatusCode(403, "You must be an editor of the developer to update a bot");
|
||||
|
||||
var project = await projectService.GetProjectAsync(projectId, developer.Id);
|
||||
if (project is null)
|
||||
return NotFound("Project not found or you don't have access");
|
||||
|
||||
var bot = await botService.GetBotByIdAsync(botId);
|
||||
if (bot is null || bot.ProjectId != projectId)
|
||||
return NotFound("Bot not found");
|
||||
|
||||
var botAccount = await remoteAccounts.GetBotAccount(bot.Id);
|
||||
|
||||
if (request.Name is not null) botAccount.Name = request.Name;
|
||||
if (request.Nick is not null) botAccount.Nick = request.Nick;
|
||||
if (request.Language is not null) botAccount.Language = request.Language;
|
||||
if (request.Bio is not null) botAccount.Profile.Bio = request.Bio;
|
||||
if (request.Gender is not null) botAccount.Profile.Gender = request.Gender;
|
||||
if (request.FirstName is not null) botAccount.Profile.FirstName = request.FirstName;
|
||||
if (request.MiddleName is not null) botAccount.Profile.MiddleName = request.MiddleName;
|
||||
if (request.LastName is not null) botAccount.Profile.LastName = request.LastName;
|
||||
if (request.TimeZone is not null) botAccount.Profile.TimeZone = request.TimeZone;
|
||||
if (request.Pronouns is not null) botAccount.Profile.Pronouns = request.Pronouns;
|
||||
if (request.Location is not null) botAccount.Profile.Location = request.Location;
|
||||
if (request.Birthday is not null) botAccount.Profile.Birthday = request.Birthday?.ToTimestamp();
|
||||
|
||||
if (request.Slug is not null) bot.Slug = request.Slug;
|
||||
if (request.IsActive is not null) bot.IsActive = request.IsActive.Value;
|
||||
|
||||
try
|
||||
{
|
||||
var updatedBot = await botService.UpdateBotAsync(
|
||||
bot,
|
||||
botAccount,
|
||||
request.PictureId,
|
||||
request.BackgroundId
|
||||
);
|
||||
|
||||
return Ok(updatedBot);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError(ex, "Error updating bot account {BotId}", botId);
|
||||
return StatusCode(500, "An error occurred while updating the bot account");
|
||||
}
|
||||
}
|
||||
|
||||
[HttpDelete("{botId:guid}")]
|
||||
public async Task<IActionResult> DeleteBot(
|
||||
[FromRoute] string pubName,
|
||||
[FromRoute] Guid projectId,
|
||||
[FromRoute] Guid botId)
|
||||
{
|
||||
if (HttpContext.Items["CurrentUser"] is not Account currentUser)
|
||||
return Unauthorized();
|
||||
|
||||
var developer = await ds.GetDeveloperByName(pubName);
|
||||
if (developer is null)
|
||||
return NotFound("Developer not found");
|
||||
|
||||
if (!await ds.IsMemberWithRole(developer.PublisherId, Guid.Parse(currentUser.Id),
|
||||
Shared.Proto.PublisherMemberRole.Editor))
|
||||
return StatusCode(403, "You must be an editor of the developer to delete a bot");
|
||||
|
||||
var project = await projectService.GetProjectAsync(projectId, developer.Id);
|
||||
if (project is null)
|
||||
return NotFound("Project not found or you don't have access");
|
||||
|
||||
var bot = await botService.GetBotByIdAsync(botId);
|
||||
if (bot is null || bot.ProjectId != projectId)
|
||||
return NotFound("Bot not found");
|
||||
|
||||
try
|
||||
{
|
||||
await botService.DeleteBotAsync(bot);
|
||||
return NoContent();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError(ex, "Error deleting bot {BotId}", botId);
|
||||
return StatusCode(500, "An error occurred while deleting the bot account");
|
||||
}
|
||||
}
|
||||
|
||||
[HttpGet("{botId:guid}/keys")]
|
||||
public async Task<ActionResult<List<SnApiKey>>> ListBotKeys(
|
||||
[FromRoute] string pubName,
|
||||
[FromRoute] Guid projectId,
|
||||
[FromRoute] Guid botId
|
||||
)
|
||||
{
|
||||
if (HttpContext.Items["CurrentUser"] is not Account currentUser)
|
||||
return Unauthorized();
|
||||
|
||||
var (developer, project, bot) = await ValidateBotAccess(pubName, projectId, botId, currentUser, Shared.Proto.PublisherMemberRole.Viewer);
|
||||
if (developer == null) return NotFound("Developer not found");
|
||||
if (project == null) return NotFound("Project not found or you don't have access");
|
||||
if (bot == null) return NotFound("Bot not found");
|
||||
|
||||
var keys = await accountsReceiver.ListApiKeyAsync(new ListApiKeyRequest
|
||||
{
|
||||
AutomatedId = bot.Id.ToString()
|
||||
});
|
||||
var data = keys.Data.Select(SnApiKey.FromProtoValue).ToList();
|
||||
|
||||
return Ok(data);
|
||||
}
|
||||
|
||||
[HttpGet("{botId:guid}/keys/{keyId:guid}")]
|
||||
public async Task<ActionResult<SnApiKey>> GetBotKey(
|
||||
[FromRoute] string pubName,
|
||||
[FromRoute] Guid projectId,
|
||||
[FromRoute] Guid botId,
|
||||
[FromRoute] Guid keyId)
|
||||
{
|
||||
if (HttpContext.Items["CurrentUser"] is not Account currentUser)
|
||||
return Unauthorized();
|
||||
|
||||
var (developer, project, bot) = await ValidateBotAccess(pubName, projectId, botId, currentUser, Shared.Proto.PublisherMemberRole.Viewer);
|
||||
if (developer == null) return NotFound("Developer not found");
|
||||
if (project == null) return NotFound("Project not found or you don't have access");
|
||||
if (bot == null) return NotFound("Bot not found");
|
||||
|
||||
try
|
||||
{
|
||||
var key = await accountsReceiver.GetApiKeyAsync(new GetApiKeyRequest { Id = keyId.ToString() });
|
||||
if (key == null) return NotFound("API key not found");
|
||||
return Ok(SnApiKey.FromProtoValue(key));
|
||||
}
|
||||
catch (RpcException ex) when (ex.StatusCode == Grpc.Core.StatusCode.NotFound)
|
||||
{
|
||||
return NotFound("API key not found");
|
||||
}
|
||||
}
|
||||
|
||||
public class CreateApiKeyRequest
|
||||
{
|
||||
[Required, MaxLength(1024)]
|
||||
public string Label { get; set; } = null!;
|
||||
}
|
||||
|
||||
[HttpPost("{botId:guid}/keys")]
|
||||
public async Task<ActionResult<SnApiKey>> CreateBotKey(
|
||||
[FromRoute] string pubName,
|
||||
[FromRoute] Guid projectId,
|
||||
[FromRoute] Guid botId,
|
||||
[FromBody] CreateApiKeyRequest request)
|
||||
{
|
||||
if (HttpContext.Items["CurrentUser"] is not Account currentUser)
|
||||
return Unauthorized();
|
||||
|
||||
var (developer, project, bot) = await ValidateBotAccess(pubName, projectId, botId, currentUser, Shared.Proto.PublisherMemberRole.Editor);
|
||||
if (developer == null) return NotFound("Developer not found");
|
||||
if (project == null) return NotFound("Project not found or you don't have access");
|
||||
if (bot == null) return NotFound("Bot not found");
|
||||
|
||||
try
|
||||
{
|
||||
var newKey = new ApiKey
|
||||
{
|
||||
AccountId = bot.Id.ToString(),
|
||||
Label = request.Label
|
||||
};
|
||||
|
||||
var createdKey = await accountsReceiver.CreateApiKeyAsync(newKey);
|
||||
return Ok(SnApiKey.FromProtoValue(createdKey));
|
||||
}
|
||||
catch (RpcException ex) when (ex.StatusCode == Grpc.Core.StatusCode.InvalidArgument)
|
||||
{
|
||||
return BadRequest(ex.Status.Detail);
|
||||
}
|
||||
}
|
||||
|
||||
[HttpPost("{botId:guid}/keys/{keyId:guid}/rotate")]
|
||||
public async Task<ActionResult<SnApiKey>> RotateBotKey(
|
||||
[FromRoute] string pubName,
|
||||
[FromRoute] Guid projectId,
|
||||
[FromRoute] Guid botId,
|
||||
[FromRoute] Guid keyId)
|
||||
{
|
||||
if (HttpContext.Items["CurrentUser"] is not Account currentUser)
|
||||
return Unauthorized();
|
||||
|
||||
var (developer, project, bot) = await ValidateBotAccess(pubName, projectId, botId, currentUser, Shared.Proto.PublisherMemberRole.Editor);
|
||||
if (developer == null) return NotFound("Developer not found");
|
||||
if (project == null) return NotFound("Project not found or you don't have access");
|
||||
if (bot == null) return NotFound("Bot not found");
|
||||
|
||||
try
|
||||
{
|
||||
var rotatedKey = await accountsReceiver.RotateApiKeyAsync(new GetApiKeyRequest { Id = keyId.ToString() });
|
||||
return Ok(SnApiKey.FromProtoValue(rotatedKey));
|
||||
}
|
||||
catch (RpcException ex) when (ex.StatusCode == Grpc.Core.StatusCode.NotFound)
|
||||
{
|
||||
return NotFound("API key not found");
|
||||
}
|
||||
}
|
||||
|
||||
[HttpDelete("{botId:guid}/keys/{keyId:guid}")]
|
||||
public async Task<IActionResult> DeleteBotKey(
|
||||
[FromRoute] string pubName,
|
||||
[FromRoute] Guid projectId,
|
||||
[FromRoute] Guid botId,
|
||||
[FromRoute] Guid keyId)
|
||||
{
|
||||
if (HttpContext.Items["CurrentUser"] is not Account currentUser)
|
||||
return Unauthorized();
|
||||
|
||||
var (developer, project, bot) = await ValidateBotAccess(pubName, projectId, botId, currentUser, Shared.Proto.PublisherMemberRole.Editor);
|
||||
if (developer == null) return NotFound("Developer not found");
|
||||
if (project == null) return NotFound("Project not found or you don't have access");
|
||||
if (bot == null) return NotFound("Bot not found");
|
||||
|
||||
try
|
||||
{
|
||||
await accountsReceiver.DeleteApiKeyAsync(new GetApiKeyRequest { Id = keyId.ToString() });
|
||||
return NoContent();
|
||||
}
|
||||
catch (RpcException ex) when (ex.StatusCode == Grpc.Core.StatusCode.NotFound)
|
||||
{
|
||||
return NotFound("API key not found");
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<(SnDeveloper?, SnDevProject?, SnBotAccount?)> ValidateBotAccess(
|
||||
string pubName,
|
||||
Guid projectId,
|
||||
Guid botId,
|
||||
Account currentUser,
|
||||
Shared.Proto.PublisherMemberRole requiredRole)
|
||||
{
|
||||
var developer = await ds.GetDeveloperByName(pubName);
|
||||
if (developer == null) return (null, null, null);
|
||||
|
||||
if (!await ds.IsMemberWithRole(developer.PublisherId, Guid.Parse(currentUser.Id), requiredRole))
|
||||
return (null, null, null);
|
||||
|
||||
var project = await projectService.GetProjectAsync(projectId, developer.Id);
|
||||
if (project == null) return (developer, null, null);
|
||||
|
||||
var bot = await botService.GetBotByIdAsync(botId);
|
||||
if (bot == null || bot.ProjectId != projectId) return (developer, project, null);
|
||||
|
||||
return (developer, project, bot);
|
||||
}
|
||||
}
|
||||
36
DysonNetwork.Develop/Identity/BotAccountPublicController.cs
Normal file
36
DysonNetwork.Develop/Identity/BotAccountPublicController.cs
Normal file
@@ -0,0 +1,36 @@
|
||||
using DysonNetwork.Shared.Models;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace DysonNetwork.Develop.Identity;
|
||||
|
||||
[ApiController]
|
||||
[Route("api/bots")]
|
||||
public class BotAccountPublicController(BotAccountService botService, DeveloperService developerService) : ControllerBase
|
||||
{
|
||||
[HttpGet("{botId:guid}")]
|
||||
public async Task<ActionResult<SnBotAccount>> GetBotTransparentInfo([FromRoute] Guid botId)
|
||||
{
|
||||
var bot = await botService.GetBotByIdAsync(botId);
|
||||
if (bot is null) return NotFound("Bot not found");
|
||||
bot = await botService.LoadBotAccountAsync(bot);
|
||||
|
||||
var developer = await developerService.GetDeveloperById(bot!.Project.DeveloperId);
|
||||
if (developer is null) return NotFound("Developer not found");
|
||||
bot.Developer = await developerService.LoadDeveloperPublisher(developer);
|
||||
|
||||
return Ok(bot);
|
||||
}
|
||||
|
||||
[HttpGet("{botId:guid}/developer")]
|
||||
public async Task<ActionResult<SnDeveloper>> GetBotDeveloper([FromRoute] Guid botId)
|
||||
{
|
||||
var bot = await botService.GetBotByIdAsync(botId);
|
||||
if (bot is null) return NotFound("Bot not found");
|
||||
|
||||
var developer = await developerService.GetDeveloperById(bot!.Project.DeveloperId);
|
||||
if (developer is null) return NotFound("Developer not found");
|
||||
developer = await developerService.LoadDeveloperPublisher(developer);
|
||||
|
||||
return Ok(developer);
|
||||
}
|
||||
}
|
||||
172
DysonNetwork.Develop/Identity/BotAccountService.cs
Normal file
172
DysonNetwork.Develop/Identity/BotAccountService.cs
Normal file
@@ -0,0 +1,172 @@
|
||||
using DysonNetwork.Shared.Models;
|
||||
using DysonNetwork.Shared.Proto;
|
||||
using DysonNetwork.Shared.Registry;
|
||||
using Grpc.Core;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using NodaTime.Serialization.Protobuf;
|
||||
|
||||
namespace DysonNetwork.Develop.Identity;
|
||||
|
||||
public class BotAccountService(
|
||||
AppDatabase db,
|
||||
BotAccountReceiverService.BotAccountReceiverServiceClient accountReceiver,
|
||||
RemoteAccountService remoteAccounts
|
||||
)
|
||||
{
|
||||
public async Task<SnBotAccount?> GetBotByIdAsync(Guid id)
|
||||
{
|
||||
return await db.BotAccounts
|
||||
.Include(b => b.Project)
|
||||
.FirstOrDefaultAsync(b => b.Id == id);
|
||||
}
|
||||
|
||||
public async Task<List<SnBotAccount>> GetBotsByProjectAsync(Guid projectId)
|
||||
{
|
||||
return await db.BotAccounts
|
||||
.Where(b => b.ProjectId == projectId)
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
public async Task<SnBotAccount> CreateBotAsync(
|
||||
SnDevProject project,
|
||||
string slug,
|
||||
Account account,
|
||||
string? pictureId,
|
||||
string? backgroundId
|
||||
)
|
||||
{
|
||||
// First, check if a bot with this slug already exists in this project
|
||||
var existingBot = await db.BotAccounts
|
||||
.FirstOrDefaultAsync(b => b.ProjectId == project.Id && b.Slug == slug);
|
||||
|
||||
if (existingBot != null)
|
||||
throw new InvalidOperationException("A bot with this slug already exists in this project.");
|
||||
|
||||
try
|
||||
{
|
||||
var automatedId = Guid.NewGuid();
|
||||
var createRequest = new CreateBotAccountRequest
|
||||
{
|
||||
AutomatedId = automatedId.ToString(),
|
||||
Account = account,
|
||||
PictureId = pictureId,
|
||||
BackgroundId = backgroundId
|
||||
};
|
||||
|
||||
var createResponse = await accountReceiver.CreateBotAccountAsync(createRequest);
|
||||
var botAccount = createResponse.Bot;
|
||||
|
||||
// Then create the local bot account
|
||||
var bot = new SnBotAccount
|
||||
{
|
||||
Id = automatedId,
|
||||
Slug = slug,
|
||||
ProjectId = project.Id,
|
||||
Project = project,
|
||||
IsActive = botAccount.IsActive,
|
||||
CreatedAt = botAccount.CreatedAt.ToInstant(),
|
||||
UpdatedAt = botAccount.UpdatedAt.ToInstant()
|
||||
};
|
||||
|
||||
db.BotAccounts.Add(bot);
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
return bot;
|
||||
}
|
||||
catch (RpcException ex) when (ex.StatusCode == StatusCode.AlreadyExists)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"A bot account with this ID already exists in the authentication service.", ex);
|
||||
}
|
||||
catch (RpcException ex) when (ex.StatusCode == StatusCode.InvalidArgument)
|
||||
{
|
||||
throw new ArgumentException($"Invalid bot account data: {ex.Status.Detail}", ex);
|
||||
}
|
||||
catch (RpcException ex)
|
||||
{
|
||||
throw new Exception($"Failed to create bot account: {ex.Status.Detail}", ex);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<SnBotAccount> UpdateBotAsync(
|
||||
SnBotAccount bot,
|
||||
Account account,
|
||||
string? pictureId,
|
||||
string? backgroundId
|
||||
)
|
||||
{
|
||||
db.Update(bot);
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
try
|
||||
{
|
||||
// Update the bot account in the Pass service
|
||||
var updateRequest = new UpdateBotAccountRequest
|
||||
{
|
||||
AutomatedId = bot.Id.ToString(),
|
||||
Account = account,
|
||||
PictureId = pictureId,
|
||||
BackgroundId = backgroundId
|
||||
};
|
||||
|
||||
var updateResponse = await accountReceiver.UpdateBotAccountAsync(updateRequest);
|
||||
var updatedBot = updateResponse.Bot;
|
||||
|
||||
// Update local bot account
|
||||
bot.UpdatedAt = updatedBot.UpdatedAt.ToInstant();
|
||||
bot.IsActive = updatedBot.IsActive;
|
||||
await db.SaveChangesAsync();
|
||||
}
|
||||
catch (RpcException ex) when (ex.StatusCode == StatusCode.NotFound)
|
||||
{
|
||||
throw new Exception("Bot account not found in the authentication service", ex);
|
||||
}
|
||||
catch (RpcException ex)
|
||||
{
|
||||
throw new Exception($"Failed to update bot account: {ex.Status.Detail}", ex);
|
||||
}
|
||||
|
||||
return bot;
|
||||
}
|
||||
|
||||
public async Task DeleteBotAsync(SnBotAccount bot)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Delete the bot account from the Pass service
|
||||
var deleteRequest = new DeleteBotAccountRequest
|
||||
{
|
||||
AutomatedId = bot.Id.ToString(),
|
||||
Force = false
|
||||
};
|
||||
|
||||
await accountReceiver.DeleteBotAccountAsync(deleteRequest);
|
||||
}
|
||||
catch (RpcException ex) when (ex.StatusCode == StatusCode.NotFound)
|
||||
{
|
||||
// Account not found in Pass service, continue with local deletion
|
||||
}
|
||||
|
||||
// Delete the local bot account
|
||||
db.BotAccounts.Remove(bot);
|
||||
await db.SaveChangesAsync();
|
||||
}
|
||||
|
||||
public async Task<SnBotAccount?> LoadBotAccountAsync(SnBotAccount bot) =>
|
||||
(await LoadBotsAccountAsync([bot])).FirstOrDefault();
|
||||
|
||||
public async Task<List<SnBotAccount>> LoadBotsAccountAsync(List<SnBotAccount> bots)
|
||||
{
|
||||
var automatedIds = bots.Select(b => b.Id).ToList();
|
||||
var data = await remoteAccounts.GetBotAccountBatch(automatedIds);
|
||||
|
||||
foreach (var bot in bots)
|
||||
{
|
||||
bot.Account = data
|
||||
.Select(SnAccount.FromProtoValue)
|
||||
.FirstOrDefault(e => e.AutomatedId == bot.Id);
|
||||
}
|
||||
|
||||
return bots;
|
||||
}
|
||||
}
|
||||
@@ -1,161 +0,0 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
using System.Text.Json.Serialization;
|
||||
using DysonNetwork.Shared.Data;
|
||||
using DysonNetwork.Shared.Proto;
|
||||
using Google.Protobuf;
|
||||
using Google.Protobuf.WellKnownTypes;
|
||||
using NodaTime.Serialization.Protobuf;
|
||||
using NodaTime;
|
||||
|
||||
using VerificationMark = DysonNetwork.Shared.Data.VerificationMark;
|
||||
|
||||
namespace DysonNetwork.Develop.Identity;
|
||||
|
||||
public enum CustomAppStatus
|
||||
{
|
||||
Developing,
|
||||
Staging,
|
||||
Production,
|
||||
Suspended
|
||||
}
|
||||
|
||||
public class CustomApp : ModelBase, IIdentifiedResource
|
||||
{
|
||||
public Guid Id { get; set; } = Guid.NewGuid();
|
||||
[MaxLength(1024)] public string Slug { get; set; } = null!;
|
||||
[MaxLength(1024)] public string Name { get; set; } = null!;
|
||||
[MaxLength(4096)] public string? Description { get; set; }
|
||||
public CustomAppStatus Status { get; set; } = CustomAppStatus.Developing;
|
||||
|
||||
[Column(TypeName = "jsonb")] public CloudFileReferenceObject? Picture { get; set; }
|
||||
[Column(TypeName = "jsonb")] public CloudFileReferenceObject? Background { get; set; }
|
||||
|
||||
[Column(TypeName = "jsonb")] public DysonNetwork.Shared.Data.VerificationMark? Verification { get; set; }
|
||||
[Column(TypeName = "jsonb")] public CustomAppOauthConfig? OauthConfig { get; set; }
|
||||
[Column(TypeName = "jsonb")] public CustomAppLinks? Links { get; set; }
|
||||
|
||||
[JsonIgnore] public ICollection<CustomAppSecret> Secrets { get; set; } = new List<CustomAppSecret>();
|
||||
|
||||
public Guid DeveloperId { get; set; }
|
||||
public Developer Developer { get; set; } = null!;
|
||||
|
||||
[NotMapped] public string ResourceIdentifier => "custom-app:" + Id;
|
||||
|
||||
public Shared.Proto.CustomApp ToProto()
|
||||
{
|
||||
return new Shared.Proto.CustomApp
|
||||
{
|
||||
Id = Id.ToString(),
|
||||
Slug = Slug,
|
||||
Name = Name,
|
||||
Description = Description ?? string.Empty,
|
||||
Status = Status switch
|
||||
{
|
||||
CustomAppStatus.Developing => Shared.Proto.CustomAppStatus.Developing,
|
||||
CustomAppStatus.Staging => Shared.Proto.CustomAppStatus.Staging,
|
||||
CustomAppStatus.Production => Shared.Proto.CustomAppStatus.Production,
|
||||
CustomAppStatus.Suspended => Shared.Proto.CustomAppStatus.Suspended,
|
||||
_ => Shared.Proto.CustomAppStatus.Unspecified
|
||||
},
|
||||
Picture = Picture is null ? ByteString.Empty : ByteString.CopyFromUtf8(System.Text.Json.JsonSerializer.Serialize(Picture)),
|
||||
Background = Background is null ? ByteString.Empty : ByteString.CopyFromUtf8(System.Text.Json.JsonSerializer.Serialize(Background)),
|
||||
Verification = Verification is null ? ByteString.Empty : ByteString.CopyFromUtf8(System.Text.Json.JsonSerializer.Serialize(Verification)),
|
||||
Links = Links is null ? ByteString.Empty : ByteString.CopyFromUtf8(System.Text.Json.JsonSerializer.Serialize(Links)),
|
||||
OauthConfig = OauthConfig is null ? null : new DysonNetwork.Shared.Proto.CustomAppOauthConfig
|
||||
{
|
||||
ClientUri = OauthConfig.ClientUri ?? string.Empty,
|
||||
RedirectUris = { OauthConfig.RedirectUris ?? Array.Empty<string>() },
|
||||
PostLogoutRedirectUris = { OauthConfig.PostLogoutRedirectUris ?? Array.Empty<string>() },
|
||||
AllowedScopes = { OauthConfig.AllowedScopes ?? Array.Empty<string>() },
|
||||
AllowedGrantTypes = { OauthConfig.AllowedGrantTypes ?? Array.Empty<string>() },
|
||||
RequirePkce = OauthConfig.RequirePkce,
|
||||
AllowOfflineAccess = OauthConfig.AllowOfflineAccess
|
||||
},
|
||||
DeveloperId = DeveloperId.ToString(),
|
||||
CreatedAt = CreatedAt.ToTimestamp(),
|
||||
UpdatedAt = UpdatedAt.ToTimestamp()
|
||||
};
|
||||
}
|
||||
|
||||
public CustomApp FromProtoValue(Shared.Proto.CustomApp p)
|
||||
{
|
||||
Id = Guid.Parse(p.Id);
|
||||
Slug = p.Slug;
|
||||
Name = p.Name;
|
||||
Description = string.IsNullOrEmpty(p.Description) ? null : p.Description;
|
||||
Status = p.Status switch
|
||||
{
|
||||
Shared.Proto.CustomAppStatus.Developing => CustomAppStatus.Developing,
|
||||
Shared.Proto.CustomAppStatus.Staging => CustomAppStatus.Staging,
|
||||
Shared.Proto.CustomAppStatus.Production => CustomAppStatus.Production,
|
||||
Shared.Proto.CustomAppStatus.Suspended => CustomAppStatus.Suspended,
|
||||
_ => CustomAppStatus.Developing
|
||||
};
|
||||
DeveloperId = string.IsNullOrEmpty(p.DeveloperId) ? Guid.Empty : Guid.Parse(p.DeveloperId);
|
||||
CreatedAt = p.CreatedAt.ToInstant();
|
||||
UpdatedAt = p.UpdatedAt.ToInstant();
|
||||
if (p.Picture.Length > 0) Picture = System.Text.Json.JsonSerializer.Deserialize<CloudFileReferenceObject>(p.Picture.ToStringUtf8());
|
||||
if (p.Background.Length > 0) Background = System.Text.Json.JsonSerializer.Deserialize<CloudFileReferenceObject>(p.Background.ToStringUtf8());
|
||||
if (p.Verification.Length > 0) Verification = System.Text.Json.JsonSerializer.Deserialize<DysonNetwork.Shared.Data.VerificationMark>(p.Verification.ToStringUtf8());
|
||||
if (p.Links.Length > 0) Links = System.Text.Json.JsonSerializer.Deserialize<CustomAppLinks>(p.Links.ToStringUtf8());
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
public class CustomAppLinks
|
||||
{
|
||||
[MaxLength(8192)] public string? HomePage { get; set; }
|
||||
[MaxLength(8192)] public string? PrivacyPolicy { get; set; }
|
||||
[MaxLength(8192)] public string? TermsOfService { get; set; }
|
||||
}
|
||||
|
||||
public class CustomAppOauthConfig
|
||||
{
|
||||
[MaxLength(1024)] public string? ClientUri { get; set; }
|
||||
[MaxLength(4096)] public string[] RedirectUris { get; set; } = [];
|
||||
[MaxLength(4096)] public string[]? PostLogoutRedirectUris { get; set; }
|
||||
[MaxLength(256)] public string[]? AllowedScopes { get; set; } = ["openid", "profile", "email"];
|
||||
[MaxLength(256)] public string[] AllowedGrantTypes { get; set; } = ["authorization_code", "refresh_token"];
|
||||
public bool RequirePkce { get; set; } = true;
|
||||
public bool AllowOfflineAccess { get; set; } = false;
|
||||
}
|
||||
|
||||
public class CustomAppSecret : ModelBase
|
||||
{
|
||||
public Guid Id { get; set; } = Guid.NewGuid();
|
||||
[MaxLength(1024)] public string Secret { get; set; } = null!;
|
||||
[MaxLength(4096)] public string? Description { get; set; } = null!;
|
||||
public Instant? ExpiredAt { get; set; }
|
||||
public bool IsOidc { get; set; } = false; // Indicates if this secret is for OIDC/OAuth
|
||||
|
||||
public Guid AppId { get; set; }
|
||||
public CustomApp App { get; set; } = null!;
|
||||
|
||||
|
||||
public static CustomAppSecret FromProtoValue(DysonNetwork.Shared.Proto.CustomAppSecret p)
|
||||
{
|
||||
return new CustomAppSecret
|
||||
{
|
||||
Id = Guid.Parse(p.Id),
|
||||
Secret = p.Secret,
|
||||
Description = p.Description,
|
||||
ExpiredAt = p.ExpiredAt?.ToInstant(),
|
||||
IsOidc = p.IsOidc,
|
||||
AppId = Guid.Parse(p.AppId),
|
||||
};
|
||||
}
|
||||
|
||||
public DysonNetwork.Shared.Proto.CustomAppSecret ToProto()
|
||||
{
|
||||
return new DysonNetwork.Shared.Proto.CustomAppSecret
|
||||
{
|
||||
Id = Id.ToString(),
|
||||
Secret = Secret,
|
||||
Description = Description,
|
||||
ExpiredAt = ExpiredAt?.ToTimestamp(),
|
||||
IsOidc = IsOidc,
|
||||
AppId = Id.ToString(),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,13 +1,17 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using DysonNetwork.Develop.Project;
|
||||
using DysonNetwork.Shared.Models;
|
||||
using DysonNetwork.Shared.Proto;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using NodaTime;
|
||||
|
||||
namespace DysonNetwork.Develop.Identity;
|
||||
|
||||
[ApiController]
|
||||
[Route("/api/developers/{pubName}/apps")]
|
||||
public class CustomAppController(CustomAppService customApps, DeveloperService ds) : ControllerBase
|
||||
[Route("/api/developers/{pubName}/projects/{projectId:guid}/apps")]
|
||||
public class CustomAppController(CustomAppService customApps, DeveloperService ds, DevProjectService projectService)
|
||||
: ControllerBase
|
||||
{
|
||||
public record CustomAppRequest(
|
||||
[MaxLength(1024)] string? Slug,
|
||||
@@ -15,27 +19,67 @@ public class CustomAppController(CustomAppService customApps, DeveloperService d
|
||||
[MaxLength(4096)] string? Description,
|
||||
string? PictureId,
|
||||
string? BackgroundId,
|
||||
CustomAppStatus? Status,
|
||||
CustomAppLinks? Links,
|
||||
CustomAppOauthConfig? OauthConfig
|
||||
Shared.Models.CustomAppStatus? Status,
|
||||
SnCustomAppLinks? Links,
|
||||
SnCustomAppOauthConfig? OauthConfig
|
||||
);
|
||||
|
||||
public record CreateSecretRequest(
|
||||
[MaxLength(4096)] string? Description,
|
||||
TimeSpan? ExpiresIn = null,
|
||||
bool IsOidc = false
|
||||
);
|
||||
|
||||
public record SecretResponse(
|
||||
string Id,
|
||||
string? Secret,
|
||||
string? Description,
|
||||
Instant? ExpiresAt,
|
||||
bool IsOidc,
|
||||
Instant CreatedAt,
|
||||
Instant UpdatedAt
|
||||
);
|
||||
|
||||
[HttpGet]
|
||||
public async Task<IActionResult> ListApps([FromRoute] string pubName)
|
||||
[Authorize]
|
||||
public async Task<IActionResult> ListApps([FromRoute] string pubName, [FromRoute] Guid projectId)
|
||||
{
|
||||
if (HttpContext.Items["CurrentUser"] is not Account currentUser)
|
||||
return Unauthorized();
|
||||
|
||||
var developer = await ds.GetDeveloperByName(pubName);
|
||||
if (developer is null) return NotFound();
|
||||
var apps = await customApps.GetAppsByPublisherAsync(developer.Id);
|
||||
|
||||
var accountId = Guid.Parse(currentUser.Id);
|
||||
if (!await ds.IsMemberWithRole(developer.PublisherId, accountId, Shared.Proto.PublisherMemberRole.Viewer))
|
||||
return StatusCode(403, "You must be a viewer of the developer to list custom apps");
|
||||
|
||||
var project = await projectService.GetProjectAsync(projectId, developer.Id);
|
||||
if (project is null) return NotFound();
|
||||
|
||||
var apps = await customApps.GetAppsByProjectAsync(projectId);
|
||||
return Ok(apps);
|
||||
}
|
||||
|
||||
[HttpGet("{id:guid}")]
|
||||
public async Task<IActionResult> GetApp([FromRoute] string pubName, Guid id)
|
||||
[HttpGet("{appId:guid}")]
|
||||
[Authorize]
|
||||
public async Task<IActionResult> GetApp([FromRoute] string pubName, [FromRoute] Guid projectId,
|
||||
[FromRoute] Guid appId)
|
||||
{
|
||||
if (HttpContext.Items["CurrentUser"] is not Account currentUser)
|
||||
return Unauthorized();
|
||||
|
||||
var developer = await ds.GetDeveloperByName(pubName);
|
||||
if (developer is null) return NotFound();
|
||||
|
||||
var accountId = Guid.Parse(currentUser.Id);
|
||||
if (!await ds.IsMemberWithRole(developer.PublisherId, accountId, Shared.Proto.PublisherMemberRole.Viewer))
|
||||
return StatusCode(403, "You must be a viewer of the developer to list custom apps");
|
||||
|
||||
var app = await customApps.GetAppAsync(id, developerId: developer.Id);
|
||||
var project = await projectService.GetProjectAsync(projectId, developer.Id);
|
||||
if (project is null) return NotFound();
|
||||
|
||||
var app = await customApps.GetAppAsync(appId, projectId);
|
||||
if (app == null)
|
||||
return NotFound();
|
||||
|
||||
@@ -44,23 +88,39 @@ public class CustomAppController(CustomAppService customApps, DeveloperService d
|
||||
|
||||
[HttpPost]
|
||||
[Authorize]
|
||||
public async Task<IActionResult> CreateApp([FromRoute] string pubName, [FromBody] CustomAppRequest request)
|
||||
public async Task<IActionResult> CreateApp(
|
||||
[FromRoute] string pubName,
|
||||
[FromRoute] Guid projectId,
|
||||
[FromBody] CustomAppRequest request)
|
||||
{
|
||||
if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
|
||||
if (HttpContext.Items["CurrentUser"] is not Account currentUser)
|
||||
return Unauthorized();
|
||||
|
||||
var developer = await ds.GetDeveloperByName(pubName);
|
||||
if (developer is null)
|
||||
return NotFound("Developer not found");
|
||||
|
||||
if (!await ds.IsMemberWithRole(developer.PublisherId, Guid.Parse(currentUser.Id), Shared.Proto.PublisherMemberRole.Editor))
|
||||
return StatusCode(403, "You must be an editor of the developer to create a custom app");
|
||||
|
||||
var project = await projectService.GetProjectAsync(projectId, developer.Id);
|
||||
if (project is null)
|
||||
return NotFound("Project not found or you don't have access");
|
||||
|
||||
if (string.IsNullOrWhiteSpace(request.Name) || string.IsNullOrWhiteSpace(request.Slug))
|
||||
return BadRequest("Name and slug are required");
|
||||
|
||||
var developer = await ds.GetDeveloperByName(pubName);
|
||||
if (developer is null) return NotFound();
|
||||
|
||||
if (!await ds.IsMemberWithRole(developer.PublisherId, Guid.Parse(currentUser.Id), PublisherMemberRole.Editor))
|
||||
return StatusCode(403, "You must be an editor of the developer to create a custom app");
|
||||
|
||||
try
|
||||
{
|
||||
var app = await customApps.CreateAppAsync(developer, request);
|
||||
return Ok(app);
|
||||
var app = await customApps.CreateAppAsync(projectId, request);
|
||||
if (app == null)
|
||||
return BadRequest("Failed to create app");
|
||||
|
||||
return CreatedAtAction(
|
||||
nameof(GetApp),
|
||||
new { pubName, projectId, appId = app.Id },
|
||||
app
|
||||
);
|
||||
}
|
||||
catch (InvalidOperationException ex)
|
||||
{
|
||||
@@ -68,23 +128,30 @@ public class CustomAppController(CustomAppService customApps, DeveloperService d
|
||||
}
|
||||
}
|
||||
|
||||
[HttpPatch("{id:guid}")]
|
||||
[HttpPatch("{appId:guid}")]
|
||||
[Authorize]
|
||||
public async Task<IActionResult> UpdateApp(
|
||||
[FromRoute] string pubName,
|
||||
[FromRoute] Guid id,
|
||||
[FromRoute] Guid projectId,
|
||||
[FromRoute] Guid appId,
|
||||
[FromBody] CustomAppRequest request
|
||||
)
|
||||
{
|
||||
if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
|
||||
|
||||
if (HttpContext.Items["CurrentUser"] is not Account currentUser)
|
||||
return Unauthorized();
|
||||
|
||||
var developer = await ds.GetDeveloperByName(pubName);
|
||||
if (developer is null) return NotFound();
|
||||
|
||||
if (!await ds.IsMemberWithRole(developer.PublisherId, Guid.Parse(currentUser.Id), PublisherMemberRole.Editor))
|
||||
if (developer is null)
|
||||
return NotFound("Developer not found");
|
||||
|
||||
if (!await ds.IsMemberWithRole(developer.PublisherId, Guid.Parse(currentUser.Id), Shared.Proto.PublisherMemberRole.Editor))
|
||||
return StatusCode(403, "You must be an editor of the developer to update a custom app");
|
||||
|
||||
var app = await customApps.GetAppAsync(id, developerId: developer.Id);
|
||||
var project = await projectService.GetProjectAsync(projectId, developer.Id);
|
||||
if (project is null)
|
||||
return NotFound("Project not found or you don't have access");
|
||||
|
||||
var app = await customApps.GetAppAsync(appId, projectId);
|
||||
if (app == null)
|
||||
return NotFound();
|
||||
|
||||
@@ -99,28 +166,267 @@ public class CustomAppController(CustomAppService customApps, DeveloperService d
|
||||
}
|
||||
}
|
||||
|
||||
[HttpDelete("{id:guid}")]
|
||||
[HttpDelete("{appId:guid}")]
|
||||
[Authorize]
|
||||
public async Task<IActionResult> DeleteApp(
|
||||
[FromRoute] string pubName,
|
||||
[FromRoute] Guid id
|
||||
[FromRoute] Guid projectId,
|
||||
[FromRoute] Guid appId
|
||||
)
|
||||
{
|
||||
if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
|
||||
|
||||
if (HttpContext.Items["CurrentUser"] is not Account currentUser)
|
||||
return Unauthorized();
|
||||
|
||||
var developer = await ds.GetDeveloperByName(pubName);
|
||||
if (developer is null) return NotFound();
|
||||
|
||||
if (!await ds.IsMemberWithRole(developer.PublisherId, Guid.Parse(currentUser.Id), PublisherMemberRole.Editor))
|
||||
if (developer is null)
|
||||
return NotFound("Developer not found");
|
||||
|
||||
if (!await ds.IsMemberWithRole(developer.PublisherId, Guid.Parse(currentUser.Id), Shared.Proto.PublisherMemberRole.Editor))
|
||||
return StatusCode(403, "You must be an editor of the developer to delete a custom app");
|
||||
|
||||
var app = await customApps.GetAppAsync(id, developerId: developer.Id);
|
||||
var project = await projectService.GetProjectAsync(projectId, developer.Id);
|
||||
if (project is null)
|
||||
return NotFound("Project not found or you don't have access");
|
||||
|
||||
var app = await customApps.GetAppAsync(appId, projectId);
|
||||
if (app == null)
|
||||
return NotFound();
|
||||
|
||||
var result = await customApps.DeleteAppAsync(id);
|
||||
var result = await customApps.DeleteAppAsync(appId);
|
||||
if (!result)
|
||||
return NotFound();
|
||||
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
[HttpGet("{appId:guid}/secrets")]
|
||||
[Authorize]
|
||||
public async Task<IActionResult> ListSecrets(
|
||||
[FromRoute] string pubName,
|
||||
[FromRoute] Guid projectId,
|
||||
[FromRoute] Guid appId)
|
||||
{
|
||||
if (HttpContext.Items["CurrentUser"] is not Account currentUser)
|
||||
return Unauthorized();
|
||||
|
||||
var developer = await ds.GetDeveloperByName(pubName);
|
||||
if (developer is null)
|
||||
return NotFound("Developer not found");
|
||||
|
||||
if (!await ds.IsMemberWithRole(developer.PublisherId, Guid.Parse(currentUser.Id), Shared.Proto.PublisherMemberRole.Editor))
|
||||
return StatusCode(403, "You must be an editor of the developer to view app secrets");
|
||||
|
||||
var project = await projectService.GetProjectAsync(projectId, developer.Id);
|
||||
if (project is null)
|
||||
return NotFound("Project not found or you don't have access");
|
||||
|
||||
var app = await customApps.GetAppAsync(appId, projectId);
|
||||
if (app == null)
|
||||
return NotFound("App not found");
|
||||
|
||||
var secrets = await customApps.GetAppSecretsAsync(appId);
|
||||
return Ok(secrets.Select(s => new SecretResponse(
|
||||
s.Id.ToString(),
|
||||
null,
|
||||
s.Description,
|
||||
s.ExpiredAt,
|
||||
s.IsOidc,
|
||||
s.CreatedAt,
|
||||
s.UpdatedAt
|
||||
)));
|
||||
}
|
||||
|
||||
[HttpPost("{appId:guid}/secrets")]
|
||||
[Authorize]
|
||||
public async Task<IActionResult> CreateSecret(
|
||||
[FromRoute] string pubName,
|
||||
[FromRoute] Guid projectId,
|
||||
[FromRoute] Guid appId,
|
||||
[FromBody] CreateSecretRequest request)
|
||||
{
|
||||
if (HttpContext.Items["CurrentUser"] is not Account currentUser)
|
||||
return Unauthorized();
|
||||
|
||||
var developer = await ds.GetDeveloperByName(pubName);
|
||||
if (developer is null)
|
||||
return NotFound("Developer not found");
|
||||
|
||||
if (!await ds.IsMemberWithRole(developer.PublisherId, Guid.Parse(currentUser.Id), Shared.Proto.PublisherMemberRole.Editor))
|
||||
return StatusCode(403, "You must be an editor of the developer to create app secrets");
|
||||
|
||||
var project = await projectService.GetProjectAsync(projectId, developer.Id);
|
||||
if (project is null)
|
||||
return NotFound("Project not found or you don't have access");
|
||||
|
||||
var app = await customApps.GetAppAsync(appId, projectId);
|
||||
if (app == null)
|
||||
return NotFound("App not found");
|
||||
|
||||
try
|
||||
{
|
||||
var secret = await customApps.CreateAppSecretAsync(new SnCustomAppSecret
|
||||
{
|
||||
AppId = appId,
|
||||
Description = request.Description,
|
||||
ExpiredAt = request.ExpiresIn.HasValue
|
||||
? NodaTime.SystemClock.Instance.GetCurrentInstant()
|
||||
.Plus(Duration.FromTimeSpan(request.ExpiresIn.Value))
|
||||
: (NodaTime.Instant?)null,
|
||||
IsOidc = request.IsOidc
|
||||
});
|
||||
|
||||
return CreatedAtAction(
|
||||
nameof(GetSecret),
|
||||
new { pubName, projectId, appId, secretId = secret.Id },
|
||||
new SecretResponse(
|
||||
secret.Id.ToString(),
|
||||
secret.Secret,
|
||||
secret.Description,
|
||||
secret.ExpiredAt,
|
||||
secret.IsOidc,
|
||||
secret.CreatedAt,
|
||||
secret.UpdatedAt
|
||||
)
|
||||
);
|
||||
}
|
||||
catch (InvalidOperationException ex)
|
||||
{
|
||||
return BadRequest(ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
[HttpGet("{appId:guid}/secrets/{secretId:guid}")]
|
||||
[Authorize]
|
||||
public async Task<IActionResult> GetSecret(
|
||||
[FromRoute] string pubName,
|
||||
[FromRoute] Guid projectId,
|
||||
[FromRoute] Guid appId,
|
||||
[FromRoute] Guid secretId)
|
||||
{
|
||||
if (HttpContext.Items["CurrentUser"] is not Account currentUser)
|
||||
return Unauthorized();
|
||||
|
||||
var developer = await ds.GetDeveloperByName(pubName);
|
||||
if (developer is null)
|
||||
return NotFound("Developer not found");
|
||||
|
||||
if (!await ds.IsMemberWithRole(developer.PublisherId, Guid.Parse(currentUser.Id), Shared.Proto.PublisherMemberRole.Editor))
|
||||
return StatusCode(403, "You must be an editor of the developer to view app secrets");
|
||||
|
||||
var project = await projectService.GetProjectAsync(projectId, developer.Id);
|
||||
if (project is null)
|
||||
return NotFound("Project not found or you don't have access");
|
||||
|
||||
var app = await customApps.GetAppAsync(appId, projectId);
|
||||
if (app == null)
|
||||
return NotFound("App not found");
|
||||
|
||||
var secret = await customApps.GetAppSecretAsync(secretId, appId);
|
||||
if (secret == null)
|
||||
return NotFound("Secret not found");
|
||||
|
||||
return Ok(new SecretResponse(
|
||||
secret.Id.ToString(),
|
||||
null,
|
||||
secret.Description,
|
||||
secret.ExpiredAt,
|
||||
secret.IsOidc,
|
||||
secret.CreatedAt,
|
||||
secret.UpdatedAt
|
||||
));
|
||||
}
|
||||
|
||||
[HttpDelete("{appId:guid}/secrets/{secretId:guid}")]
|
||||
[Authorize]
|
||||
public async Task<IActionResult> DeleteSecret(
|
||||
[FromRoute] string pubName,
|
||||
[FromRoute] Guid projectId,
|
||||
[FromRoute] Guid appId,
|
||||
[FromRoute] Guid secretId)
|
||||
{
|
||||
if (HttpContext.Items["CurrentUser"] is not Account currentUser)
|
||||
return Unauthorized();
|
||||
|
||||
var developer = await ds.GetDeveloperByName(pubName);
|
||||
if (developer is null)
|
||||
return NotFound("Developer not found");
|
||||
|
||||
if (!await ds.IsMemberWithRole(developer.PublisherId, Guid.Parse(currentUser.Id), Shared.Proto.PublisherMemberRole.Editor))
|
||||
return StatusCode(403, "You must be an editor of the developer to delete app secrets");
|
||||
|
||||
var project = await projectService.GetProjectAsync(projectId, developer.Id);
|
||||
if (project is null)
|
||||
return NotFound("Project not found or you don't have access");
|
||||
|
||||
var app = await customApps.GetAppAsync(appId, projectId);
|
||||
if (app == null)
|
||||
return NotFound("App not found");
|
||||
|
||||
var secret = await customApps.GetAppSecretAsync(secretId, appId);
|
||||
if (secret == null)
|
||||
return NotFound("Secret not found");
|
||||
|
||||
var result = await customApps.DeleteAppSecretAsync(secretId, appId);
|
||||
if (!result)
|
||||
return NotFound("Failed to delete secret");
|
||||
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
[HttpPost("{appId:guid}/secrets/{secretId:guid}/rotate")]
|
||||
[Authorize]
|
||||
public async Task<IActionResult> RotateSecret(
|
||||
[FromRoute] string pubName,
|
||||
[FromRoute] Guid projectId,
|
||||
[FromRoute] Guid appId,
|
||||
[FromRoute] Guid secretId,
|
||||
[FromBody] CreateSecretRequest? request = null)
|
||||
{
|
||||
if (HttpContext.Items["CurrentUser"] is not Account currentUser)
|
||||
return Unauthorized();
|
||||
|
||||
var developer = await ds.GetDeveloperByName(pubName);
|
||||
if (developer is null)
|
||||
return NotFound("Developer not found");
|
||||
|
||||
if (!await ds.IsMemberWithRole(developer.PublisherId, Guid.Parse(currentUser.Id), Shared.Proto.PublisherMemberRole.Editor))
|
||||
return StatusCode(403, "You must be an editor of the developer to rotate app secrets");
|
||||
|
||||
var project = await projectService.GetProjectAsync(projectId, developer.Id);
|
||||
if (project is null)
|
||||
return NotFound("Project not found or you don't have access");
|
||||
|
||||
var app = await customApps.GetAppAsync(appId, projectId);
|
||||
if (app == null)
|
||||
return NotFound("App not found");
|
||||
|
||||
try
|
||||
{
|
||||
var secret = await customApps.RotateAppSecretAsync(new SnCustomAppSecret
|
||||
{
|
||||
Id = secretId,
|
||||
AppId = appId,
|
||||
Description = request?.Description,
|
||||
ExpiredAt = request?.ExpiresIn.HasValue == true
|
||||
? NodaTime.SystemClock.Instance.GetCurrentInstant()
|
||||
.Plus(Duration.FromTimeSpan(request.ExpiresIn.Value))
|
||||
: (NodaTime.Instant?)null,
|
||||
IsOidc = request?.IsOidc ?? false
|
||||
});
|
||||
|
||||
return Ok(new SecretResponse(
|
||||
secret.Id.ToString(),
|
||||
secret.Secret,
|
||||
secret.Description,
|
||||
secret.ExpiredAt,
|
||||
secret.IsOidc,
|
||||
secret.CreatedAt,
|
||||
secret.UpdatedAt
|
||||
));
|
||||
}
|
||||
catch (InvalidOperationException ex)
|
||||
{
|
||||
return BadRequest(ex.Message);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,8 @@
|
||||
using DysonNetwork.Shared.Data;
|
||||
using DysonNetwork.Shared.Models;
|
||||
using DysonNetwork.Shared.Proto;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
|
||||
namespace DysonNetwork.Develop.Identity;
|
||||
|
||||
@@ -10,20 +12,27 @@ public class CustomAppService(
|
||||
FileService.FileServiceClient files
|
||||
)
|
||||
{
|
||||
public async Task<CustomApp?> CreateAppAsync(
|
||||
Developer pub,
|
||||
public async Task<SnCustomApp?> CreateAppAsync(
|
||||
Guid projectId,
|
||||
CustomAppController.CustomAppRequest request
|
||||
)
|
||||
{
|
||||
var app = new CustomApp
|
||||
var project = await db.DevProjects
|
||||
.Include(p => p.Developer)
|
||||
.FirstOrDefaultAsync(p => p.Id == projectId);
|
||||
|
||||
if (project == null)
|
||||
return null;
|
||||
|
||||
var app = new SnCustomApp
|
||||
{
|
||||
Slug = request.Slug!,
|
||||
Name = request.Name!,
|
||||
Description = request.Description,
|
||||
Status = request.Status ?? CustomAppStatus.Developing,
|
||||
Status = request.Status ?? Shared.Models.CustomAppStatus.Developing,
|
||||
Links = request.Links,
|
||||
OauthConfig = request.OauthConfig,
|
||||
DeveloperId = pub.Id
|
||||
ProjectId = projectId
|
||||
};
|
||||
|
||||
if (request.PictureId is not null)
|
||||
@@ -36,7 +45,7 @@ public class CustomAppService(
|
||||
);
|
||||
if (picture is null)
|
||||
throw new InvalidOperationException("Invalid picture id, unable to find the file on cloud.");
|
||||
app.Picture = CloudFileReferenceObject.FromProtoValue(picture);
|
||||
app.Picture = SnCloudFileReferenceObject.FromProtoValue(picture);
|
||||
|
||||
// Create a new reference
|
||||
await fileRefs.CreateReferenceAsync(
|
||||
@@ -55,7 +64,7 @@ public class CustomAppService(
|
||||
);
|
||||
if (background is null)
|
||||
throw new InvalidOperationException("Invalid picture id, unable to find the file on cloud.");
|
||||
app.Background = CloudFileReferenceObject.FromProtoValue(background);
|
||||
app.Background = SnCloudFileReferenceObject.FromProtoValue(background);
|
||||
|
||||
// Create a new reference
|
||||
await fileRefs.CreateReferenceAsync(
|
||||
@@ -74,20 +83,107 @@ public class CustomAppService(
|
||||
return app;
|
||||
}
|
||||
|
||||
public async Task<CustomApp?> GetAppAsync(Guid id, Guid? developerId = null)
|
||||
public async Task<SnCustomApp?> GetAppAsync(Guid id, Guid? projectId = null)
|
||||
{
|
||||
var query = db.CustomApps.Where(a => a.Id == id).AsQueryable();
|
||||
if (developerId.HasValue)
|
||||
query = query.Where(a => a.DeveloperId == developerId.Value);
|
||||
return await query.FirstOrDefaultAsync();
|
||||
var query = db.CustomApps.AsQueryable();
|
||||
|
||||
if (projectId.HasValue)
|
||||
{
|
||||
query = query.Where(a => a.ProjectId == projectId.Value);
|
||||
}
|
||||
|
||||
return await query.FirstOrDefaultAsync(a => a.Id == id);
|
||||
}
|
||||
|
||||
public async Task<List<CustomApp>> GetAppsByPublisherAsync(Guid publisherId)
|
||||
public async Task<List<SnCustomAppSecret>> GetAppSecretsAsync(Guid appId)
|
||||
{
|
||||
return await db.CustomApps.Where(a => a.DeveloperId == publisherId).ToListAsync();
|
||||
return await db.CustomAppSecrets
|
||||
.Where(s => s.AppId == appId)
|
||||
.OrderByDescending(s => s.CreatedAt)
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
public async Task<CustomApp?> UpdateAppAsync(CustomApp app, CustomAppController.CustomAppRequest request)
|
||||
public async Task<SnCustomAppSecret?> GetAppSecretAsync(Guid secretId, Guid appId)
|
||||
{
|
||||
return await db.CustomAppSecrets
|
||||
.FirstOrDefaultAsync(s => s.Id == secretId && s.AppId == appId);
|
||||
}
|
||||
|
||||
public async Task<SnCustomAppSecret> CreateAppSecretAsync(SnCustomAppSecret secret)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(secret.Secret))
|
||||
{
|
||||
// Generate a new random secret if not provided
|
||||
secret.Secret = GenerateRandomSecret();
|
||||
}
|
||||
|
||||
secret.Id = Guid.NewGuid();
|
||||
secret.CreatedAt = NodaTime.SystemClock.Instance.GetCurrentInstant();
|
||||
secret.UpdatedAt = secret.CreatedAt;
|
||||
|
||||
db.CustomAppSecrets.Add(secret);
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
return secret;
|
||||
}
|
||||
|
||||
public async Task<bool> DeleteAppSecretAsync(Guid secretId, Guid appId)
|
||||
{
|
||||
var secret = await db.CustomAppSecrets
|
||||
.FirstOrDefaultAsync(s => s.Id == secretId && s.AppId == appId);
|
||||
|
||||
if (secret == null)
|
||||
return false;
|
||||
|
||||
db.CustomAppSecrets.Remove(secret);
|
||||
await db.SaveChangesAsync();
|
||||
return true;
|
||||
}
|
||||
|
||||
public async Task<SnCustomAppSecret> RotateAppSecretAsync(SnCustomAppSecret secretUpdate)
|
||||
{
|
||||
var existingSecret = await db.CustomAppSecrets
|
||||
.FirstOrDefaultAsync(s => s.Id == secretUpdate.Id && s.AppId == secretUpdate.AppId);
|
||||
|
||||
if (existingSecret == null)
|
||||
throw new InvalidOperationException("Secret not found");
|
||||
|
||||
// Update the existing secret with new values
|
||||
existingSecret.Secret = GenerateRandomSecret();
|
||||
existingSecret.Description = secretUpdate.Description ?? existingSecret.Description;
|
||||
existingSecret.ExpiredAt = secretUpdate.ExpiredAt ?? existingSecret.ExpiredAt;
|
||||
existingSecret.IsOidc = secretUpdate.IsOidc;
|
||||
existingSecret.UpdatedAt = NodaTime.SystemClock.Instance.GetCurrentInstant();
|
||||
|
||||
await db.SaveChangesAsync();
|
||||
return existingSecret;
|
||||
}
|
||||
|
||||
private static string GenerateRandomSecret(int length = 64)
|
||||
{
|
||||
const string valid = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890-._~+";
|
||||
var res = new StringBuilder();
|
||||
using (var rng = RandomNumberGenerator.Create())
|
||||
{
|
||||
var uintBuffer = new byte[sizeof(uint)];
|
||||
while (length-- > 0)
|
||||
{
|
||||
rng.GetBytes(uintBuffer);
|
||||
var num = BitConverter.ToUInt32(uintBuffer, 0);
|
||||
res.Append(valid[(int)(num % (uint)valid.Length)]);
|
||||
}
|
||||
}
|
||||
return res.ToString();
|
||||
}
|
||||
|
||||
public async Task<List<SnCustomApp>> GetAppsByProjectAsync(Guid projectId)
|
||||
{
|
||||
return await db.CustomApps
|
||||
.Where(a => a.ProjectId == projectId)
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
public async Task<SnCustomApp?> UpdateAppAsync(SnCustomApp app, CustomAppController.CustomAppRequest request)
|
||||
{
|
||||
if (request.Slug is not null)
|
||||
app.Slug = request.Slug;
|
||||
@@ -112,7 +208,7 @@ public class CustomAppService(
|
||||
);
|
||||
if (picture is null)
|
||||
throw new InvalidOperationException("Invalid picture id, unable to find the file on cloud.");
|
||||
app.Picture = CloudFileReferenceObject.FromProtoValue(picture);
|
||||
app.Picture = SnCloudFileReferenceObject.FromProtoValue(picture);
|
||||
|
||||
// Create a new reference
|
||||
await fileRefs.CreateReferenceAsync(
|
||||
@@ -131,7 +227,7 @@ public class CustomAppService(
|
||||
);
|
||||
if (background is null)
|
||||
throw new InvalidOperationException("Invalid picture id, unable to find the file on cloud.");
|
||||
app.Background = CloudFileReferenceObject.FromProtoValue(background);
|
||||
app.Background = SnCloudFileReferenceObject.FromProtoValue(background);
|
||||
|
||||
// Create a new reference
|
||||
await fileRefs.CreateReferenceAsync(
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using DysonNetwork.Shared.Models;
|
||||
using DysonNetwork.Shared.Proto;
|
||||
using Grpc.Core;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
@@ -37,7 +38,7 @@ public class CustomAppServiceGrpc(AppDatabase db) : Shared.Proto.CustomAppServic
|
||||
if (string.IsNullOrEmpty(request.Secret))
|
||||
throw new RpcException(new Status(StatusCode.InvalidArgument, "secret required"));
|
||||
|
||||
IQueryable<CustomAppSecret> q = db.CustomAppSecrets;
|
||||
IQueryable<SnCustomAppSecret> q = db.CustomAppSecrets;
|
||||
switch (request.SecretIdentifierCase)
|
||||
{
|
||||
case CheckCustomAppSecretRequest.SecretIdentifierOneofCase.SecretId:
|
||||
|
||||
@@ -1,75 +0,0 @@
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
using DysonNetwork.Shared.Proto;
|
||||
using DysonNetwork.Shared.Data;
|
||||
using VerificationMark = DysonNetwork.Shared.Data.VerificationMark;
|
||||
|
||||
namespace DysonNetwork.Develop.Identity;
|
||||
|
||||
public class Developer
|
||||
{
|
||||
public Guid Id { get; set; } = Guid.NewGuid();
|
||||
public Guid PublisherId { get; set; }
|
||||
|
||||
[NotMapped] public PublisherInfo? Publisher { get; set; }
|
||||
}
|
||||
|
||||
public class PublisherInfo
|
||||
{
|
||||
public Guid Id { get; set; }
|
||||
public PublisherType Type { get; set; }
|
||||
public string Name { get; set; } = string.Empty;
|
||||
public string Nick { get; set; } = string.Empty;
|
||||
public string? Bio { get; set; }
|
||||
|
||||
public CloudFileReferenceObject? Picture { get; set; }
|
||||
public CloudFileReferenceObject? Background { get; set; }
|
||||
|
||||
public VerificationMark? Verification { get; set; }
|
||||
public Guid? AccountId { get; set; }
|
||||
public Guid? RealmId { get; set; }
|
||||
|
||||
public static PublisherInfo FromProto(Publisher proto)
|
||||
{
|
||||
var info = new PublisherInfo
|
||||
{
|
||||
Id = Guid.Parse(proto.Id),
|
||||
Type = proto.Type == PublisherType.PubIndividual
|
||||
? PublisherType.PubIndividual
|
||||
: PublisherType.PubOrganizational,
|
||||
Name = proto.Name,
|
||||
Nick = proto.Nick,
|
||||
Bio = string.IsNullOrEmpty(proto.Bio) ? null : proto.Bio,
|
||||
Verification = proto.VerificationMark is not null
|
||||
? VerificationMark.FromProtoValue(proto.VerificationMark)
|
||||
: null,
|
||||
AccountId = string.IsNullOrEmpty(proto.AccountId) ? null : Guid.Parse(proto.AccountId),
|
||||
RealmId = string.IsNullOrEmpty(proto.RealmId) ? null : Guid.Parse(proto.RealmId)
|
||||
};
|
||||
|
||||
if (proto.Picture != null)
|
||||
{
|
||||
info.Picture = new CloudFileReferenceObject
|
||||
{
|
||||
Id = proto.Picture.Id,
|
||||
Name = proto.Picture.Name,
|
||||
MimeType = proto.Picture.MimeType,
|
||||
Hash = proto.Picture.Hash,
|
||||
Size = proto.Picture.Size
|
||||
};
|
||||
}
|
||||
|
||||
if (proto.Background != null)
|
||||
{
|
||||
info.Background = new CloudFileReferenceObject
|
||||
{
|
||||
Id = proto.Background.Id,
|
||||
Name = proto.Background.Name,
|
||||
MimeType = proto.Background.MimeType,
|
||||
Hash = proto.Background.Hash,
|
||||
Size = (long)proto.Background.Size
|
||||
};
|
||||
}
|
||||
|
||||
return info;
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
using DysonNetwork.Shared.Auth;
|
||||
using DysonNetwork.Shared.Models;
|
||||
using DysonNetwork.Shared.Proto;
|
||||
using Grpc.Core;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
@@ -18,7 +19,7 @@ public class DeveloperController(
|
||||
: ControllerBase
|
||||
{
|
||||
[HttpGet("{name}")]
|
||||
public async Task<ActionResult<Developer>> GetDeveloper(string name)
|
||||
public async Task<ActionResult<SnDeveloper>> GetDeveloper(string name)
|
||||
{
|
||||
var developer = await ds.GetDeveloperByName(name);
|
||||
if (developer is null) return NotFound();
|
||||
@@ -33,7 +34,8 @@ public class DeveloperController(
|
||||
|
||||
// Get custom apps count
|
||||
var customAppsCount = await db.CustomApps
|
||||
.Where(a => a.DeveloperId == developer.Id)
|
||||
.Include(a => a.Project)
|
||||
.Where(a => a.Project.DeveloperId == developer.Id)
|
||||
.CountAsync();
|
||||
|
||||
var stats = new DeveloperStats
|
||||
@@ -46,10 +48,9 @@ public class DeveloperController(
|
||||
|
||||
[HttpGet]
|
||||
[Authorize]
|
||||
public async Task<ActionResult<List<Developer>>> ListJoinedDevelopers()
|
||||
public async Task<ActionResult<List<SnDeveloper>>> ListJoinedDevelopers()
|
||||
{
|
||||
if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
|
||||
var accountId = Guid.Parse(currentUser.Id);
|
||||
|
||||
var pubResponse = await ps.ListPublishersAsync(new ListPublishersRequest { AccountId = currentUser.Id });
|
||||
var pubIds = pubResponse.Publishers.Select(p => p.Id).Select(Guid.Parse).ToList();
|
||||
@@ -68,17 +69,17 @@ public class DeveloperController(
|
||||
|
||||
[HttpPost("{name}/enroll")]
|
||||
[Authorize]
|
||||
[RequiredPermission("global", "developers.create")]
|
||||
public async Task<ActionResult<Developer>> EnrollDeveloperProgram(string name)
|
||||
[AskPermission("developers.create")]
|
||||
public async Task<ActionResult<SnDeveloper>> EnrollDeveloperProgram(string name)
|
||||
{
|
||||
if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
|
||||
var accountId = Guid.Parse(currentUser.Id);
|
||||
|
||||
PublisherInfo? pub;
|
||||
SnPublisher? pub;
|
||||
try
|
||||
{
|
||||
var pubResponse = await ps.GetPublisherAsync(new GetPublisherRequest { Name = name });
|
||||
pub = PublisherInfo.FromProto(pubResponse.Publisher);
|
||||
pub = SnPublisher.FromProtoValue(pubResponse.Publisher);
|
||||
} catch (RpcException ex)
|
||||
{
|
||||
return NotFound(ex.Status.Detail);
|
||||
@@ -89,14 +90,14 @@ public class DeveloperController(
|
||||
{
|
||||
PublisherId = pub.Id.ToString(),
|
||||
AccountId = currentUser.Id,
|
||||
Role = PublisherMemberRole.Owner
|
||||
Role = Shared.Proto.PublisherMemberRole.Owner
|
||||
});
|
||||
if (!permResponse.Valid) return StatusCode(403, "You must be the owner of the publisher to join the developer program");
|
||||
|
||||
var hasDeveloper = await db.Developers.AnyAsync(d => d.PublisherId == pub.Id);
|
||||
if (hasDeveloper) return BadRequest("Publisher is already in the developer program");
|
||||
|
||||
var developer = new Developer
|
||||
var developer = new SnDeveloper
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
PublisherId = pub.Id
|
||||
|
||||
@@ -1,27 +1,31 @@
|
||||
using DysonNetwork.Shared.Models;
|
||||
using DysonNetwork.Shared.Proto;
|
||||
using Grpc.Core;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace DysonNetwork.Develop.Identity;
|
||||
|
||||
public class DeveloperService(AppDatabase db, PublisherService.PublisherServiceClient ps, ILogger<DeveloperService> logger)
|
||||
public class DeveloperService(
|
||||
AppDatabase db,
|
||||
PublisherService.PublisherServiceClient ps,
|
||||
ILogger<DeveloperService> logger)
|
||||
{
|
||||
public async Task<Developer> LoadDeveloperPublisher(Developer developer)
|
||||
public async Task<SnDeveloper> LoadDeveloperPublisher(SnDeveloper developer)
|
||||
{
|
||||
var pubResponse = await ps.GetPublisherAsync(new GetPublisherRequest { Id = developer.PublisherId.ToString() });
|
||||
developer.Publisher = PublisherInfo.FromProto(pubResponse.Publisher);
|
||||
developer.Publisher = SnPublisher.FromProtoValue(pubResponse.Publisher);
|
||||
return developer;
|
||||
}
|
||||
|
||||
|
||||
public async Task<IEnumerable<Developer>> LoadDeveloperPublisher(IEnumerable<Developer> developers)
|
||||
public async Task<IEnumerable<SnDeveloper>> LoadDeveloperPublisher(IEnumerable<SnDeveloper> developers)
|
||||
{
|
||||
var enumerable = developers.ToList();
|
||||
var pubIds = enumerable.Select(d => d.PublisherId).ToList();
|
||||
var pubRequest = new GetPublisherBatchRequest();
|
||||
pubIds.ForEach(x => pubRequest.Ids.Add(x.ToString()));
|
||||
var pubResponse = await ps.GetPublisherBatchAsync(pubRequest);
|
||||
var pubs = pubResponse.Publishers.ToDictionary(p => Guid.Parse(p.Id), PublisherInfo.FromProto);
|
||||
var pubs = pubResponse.Publishers.ToDictionary(p => Guid.Parse(p.Id), SnPublisher.FromProtoValue);
|
||||
|
||||
return enumerable.Select(d =>
|
||||
{
|
||||
@@ -30,7 +34,7 @@ public class DeveloperService(AppDatabase db, PublisherService.PublisherServiceC
|
||||
});
|
||||
}
|
||||
|
||||
public async Task<Developer?> GetDeveloperByName(string name)
|
||||
public async Task<SnDeveloper?> GetDeveloperByName(string name)
|
||||
{
|
||||
try
|
||||
{
|
||||
@@ -47,7 +51,12 @@ public class DeveloperService(AppDatabase db, PublisherService.PublisherServiceC
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<bool> IsMemberWithRole(Guid pubId, Guid accountId, PublisherMemberRole role)
|
||||
public async Task<SnDeveloper?> GetDeveloperById(Guid id)
|
||||
{
|
||||
return await db.Developers.FirstOrDefaultAsync(d => d.Id == id);
|
||||
}
|
||||
|
||||
public async Task<bool> IsMemberWithRole(Guid pubId, Guid accountId, Shared.Proto.PublisherMemberRole role)
|
||||
{
|
||||
try
|
||||
{
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using DysonNetwork.Develop;
|
||||
using DysonNetwork.Develop.Identity;
|
||||
using DysonNetwork.Shared.Data;
|
||||
using DysonNetwork.Shared.Models;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
@@ -35,7 +34,7 @@ namespace DysonNetwork.Develop.Migrations
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<CloudFileReferenceObject>("Background")
|
||||
b.Property<SnCloudFileReferenceObject>("Background")
|
||||
.HasColumnType("jsonb")
|
||||
.HasColumnName("background");
|
||||
|
||||
@@ -56,7 +55,7 @@ namespace DysonNetwork.Develop.Migrations
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("developer_id");
|
||||
|
||||
b.Property<CustomAppLinks>("Links")
|
||||
b.Property<SnCustomAppLinks>("Links")
|
||||
.HasColumnType("jsonb")
|
||||
.HasColumnName("links");
|
||||
|
||||
@@ -66,11 +65,11 @@ namespace DysonNetwork.Develop.Migrations
|
||||
.HasColumnType("character varying(1024)")
|
||||
.HasColumnName("name");
|
||||
|
||||
b.Property<CustomAppOauthConfig>("OauthConfig")
|
||||
b.Property<SnCustomAppOauthConfig>("OauthConfig")
|
||||
.HasColumnType("jsonb")
|
||||
.HasColumnName("oauth_config");
|
||||
|
||||
b.Property<CloudFileReferenceObject>("Picture")
|
||||
b.Property<SnCloudFileReferenceObject>("Picture")
|
||||
.HasColumnType("jsonb")
|
||||
.HasColumnName("picture");
|
||||
|
||||
@@ -88,7 +87,7 @@ namespace DysonNetwork.Develop.Migrations
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("updated_at");
|
||||
|
||||
b.Property<VerificationMark>("Verification")
|
||||
b.Property<SnVerificationMark>("Verification")
|
||||
.HasColumnType("jsonb")
|
||||
.HasColumnName("verification");
|
||||
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
using System;
|
||||
using DysonNetwork.Develop.Identity;
|
||||
using DysonNetwork.Shared.Data;
|
||||
using DysonNetwork.Shared.Models;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using NodaTime;
|
||||
|
||||
@@ -35,11 +33,11 @@ namespace DysonNetwork.Develop.Migrations
|
||||
name = table.Column<string>(type: "character varying(1024)", maxLength: 1024, nullable: false),
|
||||
description = table.Column<string>(type: "character varying(4096)", maxLength: 4096, nullable: true),
|
||||
status = table.Column<int>(type: "integer", nullable: false),
|
||||
picture = table.Column<CloudFileReferenceObject>(type: "jsonb", nullable: true),
|
||||
background = table.Column<CloudFileReferenceObject>(type: "jsonb", nullable: true),
|
||||
verification = table.Column<VerificationMark>(type: "jsonb", nullable: true),
|
||||
oauth_config = table.Column<CustomAppOauthConfig>(type: "jsonb", nullable: true),
|
||||
links = table.Column<CustomAppLinks>(type: "jsonb", nullable: true),
|
||||
picture = table.Column<SnCloudFileReferenceObject>(type: "jsonb", nullable: true),
|
||||
background = table.Column<SnCloudFileReferenceObject>(type: "jsonb", nullable: true),
|
||||
verification = table.Column<SnVerificationMark>(type: "jsonb", nullable: true),
|
||||
oauth_config = table.Column<SnCustomAppOauthConfig>(type: "jsonb", nullable: true),
|
||||
links = table.Column<SnCustomAppLinks>(type: "jsonb", nullable: true),
|
||||
developer_id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
created_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false),
|
||||
updated_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false),
|
||||
|
||||
269
DysonNetwork.Develop/Migrations/20250818124844_AddDevProject.Designer.cs
generated
Normal file
269
DysonNetwork.Develop/Migrations/20250818124844_AddDevProject.Designer.cs
generated
Normal file
@@ -0,0 +1,269 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using DysonNetwork.Develop;
|
||||
using DysonNetwork.Shared.Models;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
using NodaTime;
|
||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace DysonNetwork.Develop.Migrations
|
||||
{
|
||||
[DbContext(typeof(AppDatabase))]
|
||||
[Migration("20250818124844_AddDevProject")]
|
||||
partial class AddDevProject
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "9.0.7")
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||
|
||||
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||
|
||||
modelBuilder.Entity("DysonNetwork.Develop.Identity.CustomApp", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<SnCloudFileReferenceObject>("Background")
|
||||
.HasColumnType("jsonb")
|
||||
.HasColumnName("background");
|
||||
|
||||
b.Property<Instant>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("created_at");
|
||||
|
||||
b.Property<Instant?>("DeletedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("deleted_at");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.HasMaxLength(4096)
|
||||
.HasColumnType("character varying(4096)")
|
||||
.HasColumnName("description");
|
||||
|
||||
b.Property<SnCustomAppLinks>("Links")
|
||||
.HasColumnType("jsonb")
|
||||
.HasColumnName("links");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(1024)
|
||||
.HasColumnType("character varying(1024)")
|
||||
.HasColumnName("name");
|
||||
|
||||
b.Property<SnCustomAppOauthConfig>("OauthConfig")
|
||||
.HasColumnType("jsonb")
|
||||
.HasColumnName("oauth_config");
|
||||
|
||||
b.Property<SnCloudFileReferenceObject>("Picture")
|
||||
.HasColumnType("jsonb")
|
||||
.HasColumnName("picture");
|
||||
|
||||
b.Property<Guid>("ProjectId")
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("project_id");
|
||||
|
||||
b.Property<string>("Slug")
|
||||
.IsRequired()
|
||||
.HasMaxLength(1024)
|
||||
.HasColumnType("character varying(1024)")
|
||||
.HasColumnName("slug");
|
||||
|
||||
b.Property<int>("Status")
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("status");
|
||||
|
||||
b.Property<Instant>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("updated_at");
|
||||
|
||||
b.Property<SnVerificationMark>("Verification")
|
||||
.HasColumnType("jsonb")
|
||||
.HasColumnName("verification");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_custom_apps");
|
||||
|
||||
b.HasIndex("ProjectId")
|
||||
.HasDatabaseName("ix_custom_apps_project_id");
|
||||
|
||||
b.ToTable("custom_apps", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DysonNetwork.Develop.Identity.CustomAppSecret", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<Guid>("AppId")
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("app_id");
|
||||
|
||||
b.Property<Instant>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("created_at");
|
||||
|
||||
b.Property<Instant?>("DeletedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("deleted_at");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.HasMaxLength(4096)
|
||||
.HasColumnType("character varying(4096)")
|
||||
.HasColumnName("description");
|
||||
|
||||
b.Property<Instant?>("ExpiredAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("expired_at");
|
||||
|
||||
b.Property<bool>("IsOidc")
|
||||
.HasColumnType("boolean")
|
||||
.HasColumnName("is_oidc");
|
||||
|
||||
b.Property<string>("Secret")
|
||||
.IsRequired()
|
||||
.HasMaxLength(1024)
|
||||
.HasColumnType("character varying(1024)")
|
||||
.HasColumnName("secret");
|
||||
|
||||
b.Property<Instant>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("updated_at");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_custom_app_secrets");
|
||||
|
||||
b.HasIndex("AppId")
|
||||
.HasDatabaseName("ix_custom_app_secrets_app_id");
|
||||
|
||||
b.ToTable("custom_app_secrets", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DysonNetwork.Develop.Identity.Developer", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<Guid>("PublisherId")
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("publisher_id");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_developers");
|
||||
|
||||
b.ToTable("developers", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DysonNetwork.Develop.Project.DevProject", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<Instant>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("created_at");
|
||||
|
||||
b.Property<Instant?>("DeletedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("deleted_at");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.IsRequired()
|
||||
.HasMaxLength(4096)
|
||||
.HasColumnType("character varying(4096)")
|
||||
.HasColumnName("description");
|
||||
|
||||
b.Property<Guid>("DeveloperId")
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("developer_id");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(1024)
|
||||
.HasColumnType("character varying(1024)")
|
||||
.HasColumnName("name");
|
||||
|
||||
b.Property<string>("Slug")
|
||||
.IsRequired()
|
||||
.HasMaxLength(1024)
|
||||
.HasColumnType("character varying(1024)")
|
||||
.HasColumnName("slug");
|
||||
|
||||
b.Property<Instant>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("updated_at");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_dev_projects");
|
||||
|
||||
b.HasIndex("DeveloperId")
|
||||
.HasDatabaseName("ix_dev_projects_developer_id");
|
||||
|
||||
b.ToTable("dev_projects", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DysonNetwork.Develop.Identity.CustomApp", b =>
|
||||
{
|
||||
b.HasOne("DysonNetwork.Develop.Project.DevProject", "Project")
|
||||
.WithMany()
|
||||
.HasForeignKey("ProjectId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired()
|
||||
.HasConstraintName("fk_custom_apps_dev_projects_project_id");
|
||||
|
||||
b.Navigation("Project");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DysonNetwork.Develop.Identity.CustomAppSecret", b =>
|
||||
{
|
||||
b.HasOne("DysonNetwork.Develop.Identity.CustomApp", "App")
|
||||
.WithMany("Secrets")
|
||||
.HasForeignKey("AppId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired()
|
||||
.HasConstraintName("fk_custom_app_secrets_custom_apps_app_id");
|
||||
|
||||
b.Navigation("App");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DysonNetwork.Develop.Project.DevProject", b =>
|
||||
{
|
||||
b.HasOne("DysonNetwork.Develop.Identity.Developer", "Developer")
|
||||
.WithMany("Projects")
|
||||
.HasForeignKey("DeveloperId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired()
|
||||
.HasConstraintName("fk_dev_projects_developers_developer_id");
|
||||
|
||||
b.Navigation("Developer");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DysonNetwork.Develop.Identity.CustomApp", b =>
|
||||
{
|
||||
b.Navigation("Secrets");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DysonNetwork.Develop.Identity.Developer", b =>
|
||||
{
|
||||
b.Navigation("Projects");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using NodaTime;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace DysonNetwork.Develop.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddDevProject : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropForeignKey(
|
||||
name: "fk_custom_apps_developers_developer_id",
|
||||
table: "custom_apps");
|
||||
|
||||
migrationBuilder.RenameColumn(
|
||||
name: "developer_id",
|
||||
table: "custom_apps",
|
||||
newName: "project_id");
|
||||
|
||||
migrationBuilder.RenameIndex(
|
||||
name: "ix_custom_apps_developer_id",
|
||||
table: "custom_apps",
|
||||
newName: "ix_custom_apps_project_id");
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "dev_projects",
|
||||
columns: table => new
|
||||
{
|
||||
id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
slug = table.Column<string>(type: "character varying(1024)", maxLength: 1024, nullable: false),
|
||||
name = table.Column<string>(type: "character varying(1024)", maxLength: 1024, nullable: false),
|
||||
description = table.Column<string>(type: "character varying(4096)", maxLength: 4096, nullable: false),
|
||||
developer_id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
created_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false),
|
||||
updated_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false),
|
||||
deleted_at = table.Column<Instant>(type: "timestamp with time zone", nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("pk_dev_projects", x => x.id);
|
||||
table.ForeignKey(
|
||||
name: "fk_dev_projects_developers_developer_id",
|
||||
column: x => x.developer_id,
|
||||
principalTable: "developers",
|
||||
principalColumn: "id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "ix_dev_projects_developer_id",
|
||||
table: "dev_projects",
|
||||
column: "developer_id");
|
||||
|
||||
migrationBuilder.AddForeignKey(
|
||||
name: "fk_custom_apps_dev_projects_project_id",
|
||||
table: "custom_apps",
|
||||
column: "project_id",
|
||||
principalTable: "dev_projects",
|
||||
principalColumn: "id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropForeignKey(
|
||||
name: "fk_custom_apps_dev_projects_project_id",
|
||||
table: "custom_apps");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "dev_projects");
|
||||
|
||||
migrationBuilder.RenameColumn(
|
||||
name: "project_id",
|
||||
table: "custom_apps",
|
||||
newName: "developer_id");
|
||||
|
||||
migrationBuilder.RenameIndex(
|
||||
name: "ix_custom_apps_project_id",
|
||||
table: "custom_apps",
|
||||
newName: "ix_custom_apps_developer_id");
|
||||
|
||||
migrationBuilder.AddForeignKey(
|
||||
name: "fk_custom_apps_developers_developer_id",
|
||||
table: "custom_apps",
|
||||
column: "developer_id",
|
||||
principalTable: "developers",
|
||||
principalColumn: "id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
}
|
||||
}
|
||||
}
|
||||
323
DysonNetwork.Develop/Migrations/20250819163227_AddBotAccount.Designer.cs
generated
Normal file
323
DysonNetwork.Develop/Migrations/20250819163227_AddBotAccount.Designer.cs
generated
Normal file
@@ -0,0 +1,323 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using DysonNetwork.Develop;
|
||||
using DysonNetwork.Shared.Models;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
using NodaTime;
|
||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace DysonNetwork.Develop.Migrations
|
||||
{
|
||||
[DbContext(typeof(AppDatabase))]
|
||||
[Migration("20250819163227_AddBotAccount")]
|
||||
partial class AddBotAccount
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "9.0.7")
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||
|
||||
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||
|
||||
modelBuilder.Entity("DysonNetwork.Develop.Identity.BotAccount", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<Instant>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("created_at");
|
||||
|
||||
b.Property<Instant?>("DeletedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("deleted_at");
|
||||
|
||||
b.Property<bool>("IsActive")
|
||||
.HasColumnType("boolean")
|
||||
.HasColumnName("is_active");
|
||||
|
||||
b.Property<Guid>("ProjectId")
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("project_id");
|
||||
|
||||
b.Property<string>("Slug")
|
||||
.IsRequired()
|
||||
.HasMaxLength(1024)
|
||||
.HasColumnType("character varying(1024)")
|
||||
.HasColumnName("slug");
|
||||
|
||||
b.Property<Instant>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("updated_at");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_bot_accounts");
|
||||
|
||||
b.HasIndex("ProjectId")
|
||||
.HasDatabaseName("ix_bot_accounts_project_id");
|
||||
|
||||
b.ToTable("bot_accounts", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DysonNetwork.Develop.Identity.CustomApp", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<SnCloudFileReferenceObject>("Background")
|
||||
.HasColumnType("jsonb")
|
||||
.HasColumnName("background");
|
||||
|
||||
b.Property<Instant>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("created_at");
|
||||
|
||||
b.Property<Instant?>("DeletedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("deleted_at");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.HasMaxLength(4096)
|
||||
.HasColumnType("character varying(4096)")
|
||||
.HasColumnName("description");
|
||||
|
||||
b.Property<SnCustomAppLinks>("Links")
|
||||
.HasColumnType("jsonb")
|
||||
.HasColumnName("links");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(1024)
|
||||
.HasColumnType("character varying(1024)")
|
||||
.HasColumnName("name");
|
||||
|
||||
b.Property<SnCustomAppOauthConfig>("OauthConfig")
|
||||
.HasColumnType("jsonb")
|
||||
.HasColumnName("oauth_config");
|
||||
|
||||
b.Property<SnCloudFileReferenceObject>("Picture")
|
||||
.HasColumnType("jsonb")
|
||||
.HasColumnName("picture");
|
||||
|
||||
b.Property<Guid>("ProjectId")
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("project_id");
|
||||
|
||||
b.Property<string>("Slug")
|
||||
.IsRequired()
|
||||
.HasMaxLength(1024)
|
||||
.HasColumnType("character varying(1024)")
|
||||
.HasColumnName("slug");
|
||||
|
||||
b.Property<int>("Status")
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("status");
|
||||
|
||||
b.Property<Instant>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("updated_at");
|
||||
|
||||
b.Property<SnVerificationMark>("Verification")
|
||||
.HasColumnType("jsonb")
|
||||
.HasColumnName("verification");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_custom_apps");
|
||||
|
||||
b.HasIndex("ProjectId")
|
||||
.HasDatabaseName("ix_custom_apps_project_id");
|
||||
|
||||
b.ToTable("custom_apps", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DysonNetwork.Develop.Identity.CustomAppSecret", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<Guid>("AppId")
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("app_id");
|
||||
|
||||
b.Property<Instant>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("created_at");
|
||||
|
||||
b.Property<Instant?>("DeletedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("deleted_at");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.HasMaxLength(4096)
|
||||
.HasColumnType("character varying(4096)")
|
||||
.HasColumnName("description");
|
||||
|
||||
b.Property<Instant?>("ExpiredAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("expired_at");
|
||||
|
||||
b.Property<bool>("IsOidc")
|
||||
.HasColumnType("boolean")
|
||||
.HasColumnName("is_oidc");
|
||||
|
||||
b.Property<string>("Secret")
|
||||
.IsRequired()
|
||||
.HasMaxLength(1024)
|
||||
.HasColumnType("character varying(1024)")
|
||||
.HasColumnName("secret");
|
||||
|
||||
b.Property<Instant>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("updated_at");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_custom_app_secrets");
|
||||
|
||||
b.HasIndex("AppId")
|
||||
.HasDatabaseName("ix_custom_app_secrets_app_id");
|
||||
|
||||
b.ToTable("custom_app_secrets", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DysonNetwork.Develop.Identity.Developer", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<Guid>("PublisherId")
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("publisher_id");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_developers");
|
||||
|
||||
b.ToTable("developers", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DysonNetwork.Develop.Project.DevProject", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<Instant>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("created_at");
|
||||
|
||||
b.Property<Instant?>("DeletedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("deleted_at");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.IsRequired()
|
||||
.HasMaxLength(4096)
|
||||
.HasColumnType("character varying(4096)")
|
||||
.HasColumnName("description");
|
||||
|
||||
b.Property<Guid>("DeveloperId")
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("developer_id");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(1024)
|
||||
.HasColumnType("character varying(1024)")
|
||||
.HasColumnName("name");
|
||||
|
||||
b.Property<string>("Slug")
|
||||
.IsRequired()
|
||||
.HasMaxLength(1024)
|
||||
.HasColumnType("character varying(1024)")
|
||||
.HasColumnName("slug");
|
||||
|
||||
b.Property<Instant>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("updated_at");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_dev_projects");
|
||||
|
||||
b.HasIndex("DeveloperId")
|
||||
.HasDatabaseName("ix_dev_projects_developer_id");
|
||||
|
||||
b.ToTable("dev_projects", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DysonNetwork.Develop.Identity.BotAccount", b =>
|
||||
{
|
||||
b.HasOne("DysonNetwork.Develop.Project.DevProject", "Project")
|
||||
.WithMany()
|
||||
.HasForeignKey("ProjectId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired()
|
||||
.HasConstraintName("fk_bot_accounts_dev_projects_project_id");
|
||||
|
||||
b.Navigation("Project");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DysonNetwork.Develop.Identity.CustomApp", b =>
|
||||
{
|
||||
b.HasOne("DysonNetwork.Develop.Project.DevProject", "Project")
|
||||
.WithMany()
|
||||
.HasForeignKey("ProjectId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired()
|
||||
.HasConstraintName("fk_custom_apps_dev_projects_project_id");
|
||||
|
||||
b.Navigation("Project");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DysonNetwork.Develop.Identity.CustomAppSecret", b =>
|
||||
{
|
||||
b.HasOne("DysonNetwork.Develop.Identity.CustomApp", "App")
|
||||
.WithMany("Secrets")
|
||||
.HasForeignKey("AppId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired()
|
||||
.HasConstraintName("fk_custom_app_secrets_custom_apps_app_id");
|
||||
|
||||
b.Navigation("App");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DysonNetwork.Develop.Project.DevProject", b =>
|
||||
{
|
||||
b.HasOne("DysonNetwork.Develop.Identity.Developer", "Developer")
|
||||
.WithMany("Projects")
|
||||
.HasForeignKey("DeveloperId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired()
|
||||
.HasConstraintName("fk_dev_projects_developers_developer_id");
|
||||
|
||||
b.Navigation("Developer");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DysonNetwork.Develop.Identity.CustomApp", b =>
|
||||
{
|
||||
b.Navigation("Secrets");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DysonNetwork.Develop.Identity.Developer", b =>
|
||||
{
|
||||
b.Navigation("Projects");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using NodaTime;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace DysonNetwork.Develop.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddBotAccount : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.CreateTable(
|
||||
name: "bot_accounts",
|
||||
columns: table => new
|
||||
{
|
||||
id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
slug = table.Column<string>(type: "character varying(1024)", maxLength: 1024, nullable: false),
|
||||
is_active = table.Column<bool>(type: "boolean", nullable: false),
|
||||
project_id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
created_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false),
|
||||
updated_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false),
|
||||
deleted_at = table.Column<Instant>(type: "timestamp with time zone", nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("pk_bot_accounts", x => x.id);
|
||||
table.ForeignKey(
|
||||
name: "fk_bot_accounts_dev_projects_project_id",
|
||||
column: x => x.project_id,
|
||||
principalTable: "dev_projects",
|
||||
principalColumn: "id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "ix_bot_accounts_project_id",
|
||||
table: "bot_accounts",
|
||||
column: "project_id");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "bot_accounts");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,7 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using DysonNetwork.Develop;
|
||||
using DysonNetwork.Develop.Identity;
|
||||
using DysonNetwork.Shared.Data;
|
||||
using DysonNetwork.Shared.Models;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
@@ -25,6 +24,48 @@ namespace DysonNetwork.Develop.Migrations
|
||||
|
||||
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||
|
||||
modelBuilder.Entity("DysonNetwork.Develop.Identity.BotAccount", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<Instant>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("created_at");
|
||||
|
||||
b.Property<Instant?>("DeletedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("deleted_at");
|
||||
|
||||
b.Property<bool>("IsActive")
|
||||
.HasColumnType("boolean")
|
||||
.HasColumnName("is_active");
|
||||
|
||||
b.Property<Guid>("ProjectId")
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("project_id");
|
||||
|
||||
b.Property<string>("Slug")
|
||||
.IsRequired()
|
||||
.HasMaxLength(1024)
|
||||
.HasColumnType("character varying(1024)")
|
||||
.HasColumnName("slug");
|
||||
|
||||
b.Property<Instant>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("updated_at");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_bot_accounts");
|
||||
|
||||
b.HasIndex("ProjectId")
|
||||
.HasDatabaseName("ix_bot_accounts_project_id");
|
||||
|
||||
b.ToTable("bot_accounts", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DysonNetwork.Develop.Identity.CustomApp", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
@@ -32,7 +73,7 @@ namespace DysonNetwork.Develop.Migrations
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<CloudFileReferenceObject>("Background")
|
||||
b.Property<SnCloudFileReferenceObject>("Background")
|
||||
.HasColumnType("jsonb")
|
||||
.HasColumnName("background");
|
||||
|
||||
@@ -49,11 +90,7 @@ namespace DysonNetwork.Develop.Migrations
|
||||
.HasColumnType("character varying(4096)")
|
||||
.HasColumnName("description");
|
||||
|
||||
b.Property<Guid>("DeveloperId")
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("developer_id");
|
||||
|
||||
b.Property<CustomAppLinks>("Links")
|
||||
b.Property<SnCustomAppLinks>("Links")
|
||||
.HasColumnType("jsonb")
|
||||
.HasColumnName("links");
|
||||
|
||||
@@ -63,14 +100,18 @@ namespace DysonNetwork.Develop.Migrations
|
||||
.HasColumnType("character varying(1024)")
|
||||
.HasColumnName("name");
|
||||
|
||||
b.Property<CustomAppOauthConfig>("OauthConfig")
|
||||
b.Property<SnCustomAppOauthConfig>("OauthConfig")
|
||||
.HasColumnType("jsonb")
|
||||
.HasColumnName("oauth_config");
|
||||
|
||||
b.Property<CloudFileReferenceObject>("Picture")
|
||||
b.Property<SnCloudFileReferenceObject>("Picture")
|
||||
.HasColumnType("jsonb")
|
||||
.HasColumnName("picture");
|
||||
|
||||
b.Property<Guid>("ProjectId")
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("project_id");
|
||||
|
||||
b.Property<string>("Slug")
|
||||
.IsRequired()
|
||||
.HasMaxLength(1024)
|
||||
@@ -85,15 +126,15 @@ namespace DysonNetwork.Develop.Migrations
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("updated_at");
|
||||
|
||||
b.Property<VerificationMark>("Verification")
|
||||
b.Property<SnVerificationMark>("Verification")
|
||||
.HasColumnType("jsonb")
|
||||
.HasColumnName("verification");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_custom_apps");
|
||||
|
||||
b.HasIndex("DeveloperId")
|
||||
.HasDatabaseName("ix_custom_apps_developer_id");
|
||||
b.HasIndex("ProjectId")
|
||||
.HasDatabaseName("ix_custom_apps_project_id");
|
||||
|
||||
b.ToTable("custom_apps", (string)null);
|
||||
});
|
||||
@@ -166,16 +207,78 @@ namespace DysonNetwork.Develop.Migrations
|
||||
b.ToTable("developers", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DysonNetwork.Develop.Identity.CustomApp", b =>
|
||||
modelBuilder.Entity("DysonNetwork.Develop.Project.DevProject", b =>
|
||||
{
|
||||
b.HasOne("DysonNetwork.Develop.Identity.Developer", "Developer")
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<Instant>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("created_at");
|
||||
|
||||
b.Property<Instant?>("DeletedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("deleted_at");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.IsRequired()
|
||||
.HasMaxLength(4096)
|
||||
.HasColumnType("character varying(4096)")
|
||||
.HasColumnName("description");
|
||||
|
||||
b.Property<Guid>("DeveloperId")
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("developer_id");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(1024)
|
||||
.HasColumnType("character varying(1024)")
|
||||
.HasColumnName("name");
|
||||
|
||||
b.Property<string>("Slug")
|
||||
.IsRequired()
|
||||
.HasMaxLength(1024)
|
||||
.HasColumnType("character varying(1024)")
|
||||
.HasColumnName("slug");
|
||||
|
||||
b.Property<Instant>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("updated_at");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_dev_projects");
|
||||
|
||||
b.HasIndex("DeveloperId")
|
||||
.HasDatabaseName("ix_dev_projects_developer_id");
|
||||
|
||||
b.ToTable("dev_projects", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DysonNetwork.Develop.Identity.BotAccount", b =>
|
||||
{
|
||||
b.HasOne("DysonNetwork.Develop.Project.DevProject", "Project")
|
||||
.WithMany()
|
||||
.HasForeignKey("DeveloperId")
|
||||
.HasForeignKey("ProjectId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired()
|
||||
.HasConstraintName("fk_custom_apps_developers_developer_id");
|
||||
.HasConstraintName("fk_bot_accounts_dev_projects_project_id");
|
||||
|
||||
b.Navigation("Developer");
|
||||
b.Navigation("Project");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DysonNetwork.Develop.Identity.CustomApp", b =>
|
||||
{
|
||||
b.HasOne("DysonNetwork.Develop.Project.DevProject", "Project")
|
||||
.WithMany()
|
||||
.HasForeignKey("ProjectId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired()
|
||||
.HasConstraintName("fk_custom_apps_dev_projects_project_id");
|
||||
|
||||
b.Navigation("Project");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DysonNetwork.Develop.Identity.CustomAppSecret", b =>
|
||||
@@ -190,10 +293,27 @@ namespace DysonNetwork.Develop.Migrations
|
||||
b.Navigation("App");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DysonNetwork.Develop.Project.DevProject", b =>
|
||||
{
|
||||
b.HasOne("DysonNetwork.Develop.Identity.Developer", "Developer")
|
||||
.WithMany("Projects")
|
||||
.HasForeignKey("DeveloperId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired()
|
||||
.HasConstraintName("fk_dev_projects_developers_developer_id");
|
||||
|
||||
b.Navigation("Developer");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DysonNetwork.Develop.Identity.CustomApp", b =>
|
||||
{
|
||||
b.Navigation("Secrets");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DysonNetwork.Develop.Identity.Developer", b =>
|
||||
{
|
||||
b.Navigation("Projects");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,25 +1,31 @@
|
||||
using DysonNetwork.Develop;
|
||||
using DysonNetwork.Shared.Auth;
|
||||
using DysonNetwork.Shared.Http;
|
||||
using DysonNetwork.Shared.Registry;
|
||||
using DysonNetwork.Develop.Startup;
|
||||
using DysonNetwork.Shared.Registry;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
|
||||
builder.AddServiceDefaults("develop");
|
||||
|
||||
builder.Services.Configure<ServiceRegistrationOptions>(opts => { opts.Name = "develop"; });
|
||||
|
||||
builder.ConfigureAppKestrel(builder.Configuration);
|
||||
|
||||
builder.Services.AddRegistryService(builder.Configuration);
|
||||
builder.Services.AddAppServices(builder.Configuration);
|
||||
builder.Services.AddAppAuthentication();
|
||||
builder.Services.AddAppSwagger();
|
||||
builder.Services.AddDysonAuth();
|
||||
builder.Services.AddPublisherService();
|
||||
builder.Services.AddAccountService();
|
||||
builder.Services.AddDriveService();
|
||||
|
||||
builder.AddSwaggerManifest(
|
||||
"DysonNetwork.Develop",
|
||||
"The developer portal in the Solar Network."
|
||||
);
|
||||
|
||||
var app = builder.Build();
|
||||
|
||||
app.MapDefaultEndpoints();
|
||||
|
||||
using (var scope = app.Services.CreateScope())
|
||||
{
|
||||
var db = scope.ServiceProvider.GetRequiredService<AppDatabase>();
|
||||
@@ -28,4 +34,6 @@ using (var scope = app.Services.CreateScope())
|
||||
|
||||
app.ConfigureAppMiddleware(builder.Configuration);
|
||||
|
||||
app.UseSwaggerManifest("DysonNetwork.Develop");
|
||||
|
||||
app.Run();
|
||||
107
DysonNetwork.Develop/Project/DevProjectController.cs
Normal file
107
DysonNetwork.Develop/Project/DevProjectController.cs
Normal file
@@ -0,0 +1,107 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using DysonNetwork.Develop.Identity;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using DysonNetwork.Shared.Proto;
|
||||
|
||||
namespace DysonNetwork.Develop.Project;
|
||||
|
||||
[ApiController]
|
||||
[Route("/api/developers/{pubName}/projects")]
|
||||
public class DevProjectController(DevProjectService projectService, DeveloperService developerService) : ControllerBase
|
||||
{
|
||||
public record DevProjectRequest(
|
||||
[MaxLength(1024)] string? Slug,
|
||||
[MaxLength(1024)] string? Name,
|
||||
[MaxLength(4096)] string? Description
|
||||
);
|
||||
|
||||
[HttpGet]
|
||||
public async Task<IActionResult> ListProjects([FromRoute] string pubName)
|
||||
{
|
||||
var developer = await developerService.GetDeveloperByName(pubName);
|
||||
if (developer is null) return NotFound();
|
||||
|
||||
var projects = await projectService.GetProjectsByDeveloperAsync(developer.Id);
|
||||
return Ok(projects);
|
||||
}
|
||||
|
||||
[HttpGet("{id:guid}")]
|
||||
public async Task<IActionResult> GetProject([FromRoute] string pubName, Guid id)
|
||||
{
|
||||
var developer = await developerService.GetDeveloperByName(pubName);
|
||||
if (developer is null) return NotFound();
|
||||
|
||||
var project = await projectService.GetProjectAsync(id, developer.Id);
|
||||
if (project is null) return NotFound();
|
||||
|
||||
return Ok(project);
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[Authorize]
|
||||
public async Task<IActionResult> CreateProject([FromRoute] string pubName, [FromBody] DevProjectRequest request)
|
||||
{
|
||||
if (HttpContext.Items["CurrentUser"] is not Account currentUser)
|
||||
return Unauthorized();
|
||||
|
||||
var developer = await developerService.GetDeveloperByName(pubName);
|
||||
if (developer is null)
|
||||
return NotFound("Developer not found");
|
||||
|
||||
if (!await developerService.IsMemberWithRole(developer.PublisherId, Guid.Parse(currentUser.Id), PublisherMemberRole.Editor))
|
||||
return StatusCode(403, "You must be an editor of the developer to create a project");
|
||||
|
||||
if (string.IsNullOrWhiteSpace(request.Slug) || string.IsNullOrWhiteSpace(request.Name))
|
||||
return BadRequest("Slug and Name are required");
|
||||
|
||||
var project = await projectService.CreateProjectAsync(developer, request);
|
||||
return CreatedAtAction(
|
||||
nameof(GetProject),
|
||||
new { pubName, id = project.Id },
|
||||
project
|
||||
);
|
||||
}
|
||||
|
||||
[HttpPut("{id:guid}")]
|
||||
[Authorize]
|
||||
public async Task<IActionResult> UpdateProject(
|
||||
[FromRoute] string pubName,
|
||||
[FromRoute] Guid id,
|
||||
[FromBody] DevProjectRequest request
|
||||
)
|
||||
{
|
||||
if (HttpContext.Items["CurrentUser"] is not Account currentUser)
|
||||
return Unauthorized();
|
||||
|
||||
var developer = await developerService.GetDeveloperByName(pubName);
|
||||
var accountId = Guid.Parse(currentUser.Id);
|
||||
if (developer is null || developer.Id != accountId)
|
||||
return Forbid();
|
||||
|
||||
var project = await projectService.UpdateProjectAsync(id, developer.Id, request);
|
||||
if (project is null)
|
||||
return NotFound();
|
||||
|
||||
return Ok(project);
|
||||
}
|
||||
|
||||
[HttpDelete("{id:guid}")]
|
||||
[Authorize]
|
||||
public async Task<IActionResult> DeleteProject([FromRoute] string pubName, [FromRoute] Guid id)
|
||||
{
|
||||
if (HttpContext.Items["CurrentUser"] is not Account currentUser)
|
||||
return Unauthorized();
|
||||
|
||||
var developer = await developerService.GetDeveloperByName(pubName);
|
||||
var accountId = Guid.Parse(currentUser.Id);
|
||||
if (developer is null || developer.Id != accountId)
|
||||
return Forbid();
|
||||
|
||||
var success = await projectService.DeleteProjectAsync(id, developer.Id);
|
||||
if (!success)
|
||||
return NotFound();
|
||||
|
||||
return NoContent();
|
||||
}
|
||||
}
|
||||
73
DysonNetwork.Develop/Project/DevProjectService.cs
Normal file
73
DysonNetwork.Develop/Project/DevProjectService.cs
Normal file
@@ -0,0 +1,73 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using DysonNetwork.Shared.Proto;
|
||||
using DysonNetwork.Shared.Models;
|
||||
|
||||
namespace DysonNetwork.Develop.Project;
|
||||
|
||||
public class DevProjectService(AppDatabase db )
|
||||
{
|
||||
public async Task<SnDevProject> CreateProjectAsync(
|
||||
SnDeveloper developer,
|
||||
DevProjectController.DevProjectRequest request
|
||||
)
|
||||
{
|
||||
var project = new SnDevProject
|
||||
{
|
||||
Slug = request.Slug!,
|
||||
Name = request.Name!,
|
||||
Description = request.Description ?? string.Empty,
|
||||
DeveloperId = developer.Id
|
||||
};
|
||||
|
||||
db.DevProjects.Add(project);
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
return project;
|
||||
}
|
||||
|
||||
public async Task<SnDevProject?> GetProjectAsync(Guid id, Guid? developerId = null)
|
||||
{
|
||||
var query = db.DevProjects.AsQueryable();
|
||||
|
||||
if (developerId.HasValue)
|
||||
{
|
||||
query = query.Where(p => p.DeveloperId == developerId.Value);
|
||||
}
|
||||
|
||||
return await query.FirstOrDefaultAsync(p => p.Id == id);
|
||||
}
|
||||
|
||||
public async Task<List<SnDevProject>> GetProjectsByDeveloperAsync(Guid developerId)
|
||||
{
|
||||
return await db.DevProjects
|
||||
.Where(p => p.DeveloperId == developerId)
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
public async Task<SnDevProject?> UpdateProjectAsync(
|
||||
Guid id,
|
||||
Guid developerId,
|
||||
DevProjectController.DevProjectRequest request
|
||||
)
|
||||
{
|
||||
var project = await GetProjectAsync(id, developerId);
|
||||
if (project == null) return null;
|
||||
|
||||
if (request.Slug != null) project.Slug = request.Slug;
|
||||
if (request.Name != null) project.Name = request.Name;
|
||||
if (request.Description != null) project.Description = request.Description;
|
||||
|
||||
await db.SaveChangesAsync();
|
||||
return project;
|
||||
}
|
||||
|
||||
public async Task<bool> DeleteProjectAsync(Guid id, Guid developerId)
|
||||
{
|
||||
var project = await GetProjectAsync(id, developerId);
|
||||
if (project == null) return false;
|
||||
|
||||
db.DevProjects.Remove(project);
|
||||
await db.SaveChangesAsync();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,6 @@
|
||||
"commandName": "Project",
|
||||
"dotnetRunMessages": true,
|
||||
"launchBrowser": false,
|
||||
"applicationUrl": "http://localhost:5156",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
}
|
||||
@@ -14,7 +13,6 @@
|
||||
"commandName": "Project",
|
||||
"dotnetRunMessages": true,
|
||||
"launchBrowser": false,
|
||||
"applicationUrl": "https://localhost:7192;http://localhost:5156",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
}
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
using System.Net;
|
||||
using DysonNetwork.Develop.Identity;
|
||||
using DysonNetwork.Shared.Auth;
|
||||
using Microsoft.AspNetCore.HttpOverrides;
|
||||
using Prometheus;
|
||||
using DysonNetwork.Shared.Http;
|
||||
|
||||
namespace DysonNetwork.Develop.Startup;
|
||||
|
||||
@@ -10,46 +8,21 @@ public static class ApplicationConfiguration
|
||||
{
|
||||
public static WebApplication ConfigureAppMiddleware(this WebApplication app, IConfiguration configuration)
|
||||
{
|
||||
app.MapMetrics();
|
||||
app.MapOpenApi();
|
||||
|
||||
app.UseSwagger();
|
||||
app.UseSwaggerUI();
|
||||
|
||||
app.UseRequestLocalization();
|
||||
|
||||
ConfigureForwardedHeaders(app, configuration);
|
||||
app.ConfigureForwardedHeaders(configuration);
|
||||
|
||||
app.UseAuthentication();
|
||||
app.UseAuthorization();
|
||||
app.UseMiddleware<PermissionMiddleware>();
|
||||
app.UseMiddleware<RemotePermissionMiddleware>();
|
||||
|
||||
app.MapControllers();
|
||||
|
||||
app.MapGrpcService<CustomAppServiceGrpc>();
|
||||
app.MapGrpcReflectionService();
|
||||
|
||||
return app;
|
||||
}
|
||||
|
||||
private static void ConfigureForwardedHeaders(WebApplication app, IConfiguration configuration)
|
||||
{
|
||||
var knownProxiesSection = configuration.GetSection("KnownProxies");
|
||||
var forwardedHeadersOptions = new ForwardedHeadersOptions { ForwardedHeaders = ForwardedHeaders.All };
|
||||
|
||||
if (knownProxiesSection.Exists())
|
||||
{
|
||||
var proxyAddresses = knownProxiesSection.Get<string[]>();
|
||||
if (proxyAddresses != null)
|
||||
foreach (var proxy in proxyAddresses)
|
||||
if (IPAddress.TryParse(proxy, out var ipAddress))
|
||||
forwardedHeadersOptions.KnownProxies.Add(ipAddress);
|
||||
}
|
||||
else
|
||||
{
|
||||
forwardedHeadersOptions.KnownProxies.Add(IPAddress.Any);
|
||||
forwardedHeadersOptions.KnownProxies.Add(IPAddress.IPv6Any);
|
||||
}
|
||||
|
||||
app.UseForwardedHeaders(forwardedHeadersOptions);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
using System.Globalization;
|
||||
using Microsoft.OpenApi.Models;
|
||||
using NodaTime;
|
||||
using NodaTime.Serialization.SystemTextJson;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using DysonNetwork.Develop.Identity;
|
||||
using DysonNetwork.Develop.Project;
|
||||
using DysonNetwork.Shared.Cache;
|
||||
using StackExchange.Redis;
|
||||
|
||||
namespace DysonNetwork.Develop.Startup;
|
||||
|
||||
@@ -16,25 +16,21 @@ public static class ServiceCollectionExtensions
|
||||
services.AddLocalization();
|
||||
|
||||
services.AddDbContext<AppDatabase>();
|
||||
services.AddSingleton<IClock>(SystemClock.Instance);
|
||||
services.AddHttpContextAccessor();
|
||||
services.AddSingleton<IConnectionMultiplexer>(_ =>
|
||||
{
|
||||
var connection = configuration.GetConnectionString("FastRetrieve")!;
|
||||
return ConnectionMultiplexer.Connect(connection);
|
||||
});
|
||||
services.AddSingleton<ICacheService, CacheServiceRedis>();
|
||||
|
||||
services.AddHttpClient();
|
||||
|
||||
services.AddControllers().AddJsonOptions(options =>
|
||||
{
|
||||
options.JsonSerializerOptions.NumberHandling = JsonNumberHandling.AllowNamedFloatingPointLiterals;
|
||||
options.JsonSerializerOptions.PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower;
|
||||
options.JsonSerializerOptions.DictionaryKeyPolicy = JsonNamingPolicy.SnakeCaseLower;
|
||||
|
||||
options.JsonSerializerOptions.ConfigureForNodaTime(DateTimeZoneProviders.Tzdb);
|
||||
});
|
||||
|
||||
services.AddGrpc(options => { options.EnableDetailedErrors = true; });
|
||||
services.AddGrpcReflection();
|
||||
|
||||
services.Configure<RequestLocalizationOptions>(options =>
|
||||
{
|
||||
@@ -50,29 +46,15 @@ public static class ServiceCollectionExtensions
|
||||
|
||||
services.AddScoped<DeveloperService>();
|
||||
services.AddScoped<CustomAppService>();
|
||||
services.AddScoped<DevProjectService>();
|
||||
services.AddScoped<BotAccountService>();
|
||||
|
||||
return services;
|
||||
}
|
||||
|
||||
public static IServiceCollection AddAppAuthentication(this IServiceCollection services)
|
||||
{
|
||||
services.AddCors();
|
||||
services.AddAuthorization();
|
||||
return services;
|
||||
}
|
||||
|
||||
public static IServiceCollection AddAppSwagger(this IServiceCollection services)
|
||||
{
|
||||
services.AddEndpointsApiExplorer();
|
||||
services.AddSwaggerGen(options =>
|
||||
{
|
||||
options.SwaggerDoc("v1", new OpenApiInfo
|
||||
{
|
||||
Version = "v1",
|
||||
Title = "Develop API",
|
||||
});
|
||||
});
|
||||
services.AddOpenApi();
|
||||
return services;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,30 +1,31 @@
|
||||
{
|
||||
"Debug": true,
|
||||
"BaseUrl": "http://localhost:5071",
|
||||
"SiteUrl": "https://solian.app",
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.AspNetCore": "Warning"
|
||||
"Debug": true,
|
||||
"BaseUrl": "http://localhost:5071",
|
||||
"SiteUrl": "https://solian.app",
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.AspNetCore": "Warning"
|
||||
}
|
||||
},
|
||||
"AllowedHosts": "*",
|
||||
"ConnectionStrings": {
|
||||
"App": "Host=localhost;Port=5432;Database=dyson_develop;Username=postgres;Password=postgres;Include Error Detail=True;Maximum Pool Size=20;Connection Idle Lifetime=60",
|
||||
"Registrar": "127.0.0.1:2379",
|
||||
"Cache": "127.0.0.1:6379",
|
||||
"Queue": "127.0.0.1:4222"
|
||||
},
|
||||
"KnownProxies": [
|
||||
"127.0.0.1",
|
||||
"::1"
|
||||
],
|
||||
"Swagger": {
|
||||
"PublicBasePath": "/develop"
|
||||
},
|
||||
"Cache": {
|
||||
"Serializer": "MessagePack"
|
||||
},
|
||||
"Etcd": {
|
||||
"Insecure": true
|
||||
}
|
||||
},
|
||||
"AllowedHosts": "*",
|
||||
"ConnectionStrings": {
|
||||
"App": "Host=localhost;Port=5432;Database=dyson_network_dev;Username=postgres;Password=postgres;Include Error Detail=True;Maximum Pool Size=20;Connection Idle Lifetime=60",
|
||||
"FastRetrieve": "localhost:6379",
|
||||
"Etcd": "etcd.orb.local:2379"
|
||||
},
|
||||
"KnownProxies": [
|
||||
"127.0.0.1",
|
||||
"::1"
|
||||
],
|
||||
"Etcd": {
|
||||
"Insecure": true
|
||||
},
|
||||
"Service": {
|
||||
"Name": "DysonNetwork.Develop",
|
||||
"Url": "https://localhost:7099",
|
||||
"ClientCert": "../Certificates/client.crt",
|
||||
"ClientKey": "../Certificates/client.key"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
using System.Linq.Expressions;
|
||||
using System.Reflection;
|
||||
using DysonNetwork.Drive.Billing;
|
||||
using DysonNetwork.Drive.Storage;
|
||||
using DysonNetwork.Drive.Storage.Model;
|
||||
using DysonNetwork.Shared.Data;
|
||||
using DysonNetwork.Shared.Models;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Design;
|
||||
using Microsoft.EntityFrameworkCore.Query;
|
||||
using NodaTime;
|
||||
using Quartz;
|
||||
using TaskStatus = DysonNetwork.Drive.Storage.Model.TaskStatus;
|
||||
|
||||
namespace DysonNetwork.Drive;
|
||||
|
||||
@@ -17,12 +18,16 @@ public class AppDatabase(
|
||||
) : DbContext(options)
|
||||
{
|
||||
public DbSet<FilePool> Pools { get; set; } = null!;
|
||||
public DbSet<FileBundle> Bundles { get; set; } = null!;
|
||||
public DbSet<SnFileBundle> Bundles { get; set; } = null!;
|
||||
|
||||
public DbSet<QuotaRecord> QuotaRecords { get; set; } = null!;
|
||||
|
||||
public DbSet<CloudFile> Files { get; set; } = null!;
|
||||
public DbSet<CloudFileReference> FileReferences { get; set; } = null!;
|
||||
public DbSet<SnCloudFile> Files { get; set; } = null!;
|
||||
public DbSet<SnCloudFileReference> FileReferences { get; set; } = null!;
|
||||
public DbSet<SnCloudFileIndex> FileIndexes { get; set; }
|
||||
|
||||
public DbSet<PersistentTask> Tasks { get; set; } = null!;
|
||||
public DbSet<PersistentUploadTask> UploadTasks { get; set; } = null!; // Backward compatibility
|
||||
|
||||
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
|
||||
{
|
||||
@@ -31,7 +36,6 @@ public class AppDatabase(
|
||||
opt => opt
|
||||
.ConfigureDataSource(optSource => optSource.EnableDynamicJson())
|
||||
.UseQuerySplittingBehavior(QuerySplittingBehavior.SplitQuery)
|
||||
.UseNetTopologySuite()
|
||||
.UseNodaTime()
|
||||
).UseSnakeCaseNamingConvention();
|
||||
|
||||
@@ -41,52 +45,12 @@ public class AppDatabase(
|
||||
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||
{
|
||||
base.OnModelCreating(modelBuilder);
|
||||
|
||||
// Automatically apply soft-delete filter to all entities inheriting BaseModel
|
||||
foreach (var entityType in modelBuilder.Model.GetEntityTypes())
|
||||
{
|
||||
if (!typeof(ModelBase).IsAssignableFrom(entityType.ClrType)) continue;
|
||||
var method = typeof(AppDatabase)
|
||||
.GetMethod(nameof(SetSoftDeleteFilter),
|
||||
BindingFlags.NonPublic | BindingFlags.Static)!
|
||||
.MakeGenericMethod(entityType.ClrType);
|
||||
|
||||
method.Invoke(null, [modelBuilder]);
|
||||
}
|
||||
}
|
||||
|
||||
private static void SetSoftDeleteFilter<TEntity>(ModelBuilder modelBuilder)
|
||||
where TEntity : ModelBase
|
||||
{
|
||||
modelBuilder.Entity<TEntity>().HasQueryFilter(e => e.DeletedAt == null);
|
||||
modelBuilder.ApplySoftDeleteFilters();
|
||||
}
|
||||
|
||||
public override async Task<int> SaveChangesAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
var now = SystemClock.Instance.GetCurrentInstant();
|
||||
|
||||
foreach (var entry in ChangeTracker.Entries<ModelBase>())
|
||||
{
|
||||
switch (entry.State)
|
||||
{
|
||||
case EntityState.Added:
|
||||
entry.Entity.CreatedAt = now;
|
||||
entry.Entity.UpdatedAt = now;
|
||||
break;
|
||||
case EntityState.Modified:
|
||||
entry.Entity.UpdatedAt = now;
|
||||
break;
|
||||
case EntityState.Deleted:
|
||||
entry.State = EntityState.Modified;
|
||||
entry.Entity.DeletedAt = now;
|
||||
break;
|
||||
case EntityState.Detached:
|
||||
case EntityState.Unchanged:
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
this.ApplyAuditableAndSoftDelete();
|
||||
return await base.SaveChangesAsync(cancellationToken);
|
||||
}
|
||||
}
|
||||
@@ -138,6 +102,45 @@ public class AppDatabaseRecyclingJob(AppDatabase db, ILogger<AppDatabaseRecyclin
|
||||
}
|
||||
}
|
||||
|
||||
public class PersistentTaskCleanupJob(
|
||||
IServiceProvider serviceProvider,
|
||||
ILogger<PersistentTaskCleanupJob> logger
|
||||
) : IJob
|
||||
{
|
||||
public async Task Execute(IJobExecutionContext context)
|
||||
{
|
||||
logger.LogInformation("Cleaning up stale persistent tasks...");
|
||||
|
||||
// Get the PersistentTaskService from DI
|
||||
using var scope = serviceProvider.CreateScope();
|
||||
var persistentTaskService = scope.ServiceProvider.GetService(typeof(PersistentTaskService));
|
||||
|
||||
if (persistentTaskService is PersistentTaskService service)
|
||||
{
|
||||
// Clean up tasks for all users (you might want to add user-specific logic here)
|
||||
// For now, we'll clean up tasks older than 30 days for all users
|
||||
var cutoff = SystemClock.Instance.GetCurrentInstant() - Duration.FromDays(30);
|
||||
var tasksToClean = await service.GetUserTasksAsync(
|
||||
Guid.Empty, // This would need to be adjusted for multi-user cleanup
|
||||
status: TaskStatus.Completed | TaskStatus.Failed | TaskStatus.Cancelled | TaskStatus.Expired
|
||||
);
|
||||
|
||||
var cleanedCount = 0;
|
||||
foreach (var task in tasksToClean.Items.Where(t => t.UpdatedAt < cutoff))
|
||||
{
|
||||
await service.CancelTaskAsync(task.TaskId); // Or implement a proper cleanup method
|
||||
cleanedCount++;
|
||||
}
|
||||
|
||||
logger.LogInformation("Cleaned up {Count} stale persistent tasks", cleanedCount);
|
||||
}
|
||||
else
|
||||
{
|
||||
logger.LogWarning("PersistentTaskService not found in DI container");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public class AppDatabaseFactory : IDesignTimeDbContextFactory<AppDatabase>
|
||||
{
|
||||
public AppDatabase CreateDbContext(string[] args)
|
||||
@@ -151,35 +154,3 @@ public class AppDatabaseFactory : IDesignTimeDbContextFactory<AppDatabase>
|
||||
return new AppDatabase(optionsBuilder.Options, configuration);
|
||||
}
|
||||
}
|
||||
|
||||
public static class OptionalQueryExtensions
|
||||
{
|
||||
public static IQueryable<T> If<T>(
|
||||
this IQueryable<T> source,
|
||||
bool condition,
|
||||
Func<IQueryable<T>, IQueryable<T>> transform
|
||||
)
|
||||
{
|
||||
return condition ? transform(source) : source;
|
||||
}
|
||||
|
||||
public static IQueryable<T> If<T, TP>(
|
||||
this IIncludableQueryable<T, TP> source,
|
||||
bool condition,
|
||||
Func<IIncludableQueryable<T, TP>, IQueryable<T>> transform
|
||||
)
|
||||
where T : class
|
||||
{
|
||||
return condition ? transform(source) : source;
|
||||
}
|
||||
|
||||
public static IQueryable<T> If<T, TP>(
|
||||
this IIncludableQueryable<T, IEnumerable<TP>> source,
|
||||
bool condition,
|
||||
Func<IIncludableQueryable<T, IEnumerable<TP>>, IQueryable<T>> transform
|
||||
)
|
||||
where T : class
|
||||
{
|
||||
return condition ? transform(source) : source;
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
using DysonNetwork.Shared.Data;
|
||||
using DysonNetwork.Shared.Models;
|
||||
using NodaTime;
|
||||
|
||||
namespace DysonNetwork.Drive.Billing;
|
||||
|
||||
@@ -30,7 +30,7 @@ public class QuotaService(
|
||||
|
||||
var (based, extra) = await GetQuotaVerbose(accountId);
|
||||
var quota = based + extra;
|
||||
await cache.SetAsync(cacheKey, quota);
|
||||
await cache.SetAsync(cacheKey, quota, expiry: TimeSpan.FromMinutes(30));
|
||||
return quota;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
[*.{js,jsx,mjs,cjs,ts,tsx,mts,cts,vue,css,scss,sass,less,styl}]
|
||||
charset = utf-8
|
||||
indent_size = 2
|
||||
indent_style = space
|
||||
insert_final_newline = true
|
||||
trim_trailing_whitespace = true
|
||||
|
||||
end_of_line = lf
|
||||
max_line_length = 100
|
||||
1
DysonNetwork.Drive/Client/.gitattributes
vendored
1
DysonNetwork.Drive/Client/.gitattributes
vendored
@@ -1 +0,0 @@
|
||||
* text=auto eol=lf
|
||||
31
DysonNetwork.Drive/Client/.gitignore
vendored
31
DysonNetwork.Drive/Client/.gitignore
vendored
@@ -1,31 +0,0 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
**/node_modules/highlight.js/
|
||||
.DS_Store
|
||||
dist
|
||||
dist-ssr
|
||||
coverage
|
||||
*.local
|
||||
|
||||
/cypress/videos/
|
||||
/cypress/screenshots/
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
|
||||
*.tsbuildinfo
|
||||
@@ -1,6 +0,0 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/prettierrc",
|
||||
"semi": false,
|
||||
"singleQuote": true,
|
||||
"printWidth": 100
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
{
|
||||
"recommendations": [
|
||||
"Vue.volar",
|
||||
"dbaeumer.vscode-eslint",
|
||||
"EditorConfig.EditorConfig",
|
||||
"oxc.oxc-vscode",
|
||||
"esbenp.prettier-vscode"
|
||||
]
|
||||
}
|
||||
@@ -1,955 +0,0 @@
|
||||
{
|
||||
"lockfileVersion": 1,
|
||||
"workspaces": {
|
||||
"": {
|
||||
"name": "@solar-network/pass",
|
||||
"dependencies": {
|
||||
"@fingerprintjs/fingerprintjs": "^4.6.2",
|
||||
"@fontsource-variable/nunito": "^5.2.6",
|
||||
"@hcaptcha/vue3-hcaptcha": "^1.3.0",
|
||||
"@tailwindcss/vite": "^4.1.11",
|
||||
"@vueuse/core": "^13.5.0",
|
||||
"aspnet-prerendering": "^3.0.1",
|
||||
"cfturnstile-vue3": "^2.0.0",
|
||||
"chart.js": "^4.5.0",
|
||||
"pinia": "^3.0.3",
|
||||
"tailwindcss": "^4.1.11",
|
||||
"tus-js-client": "^4.3.1",
|
||||
"vue": "^3.5.17",
|
||||
"vue-chartjs": "^5.3.2",
|
||||
"vue-router": "^4.5.1",
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tsconfig/node22": "^22.0.2",
|
||||
"@types/node": "^22.16.4",
|
||||
"@vicons/material": "^0.13.0",
|
||||
"@vitejs/plugin-vue": "^6.0.0",
|
||||
"@vitejs/plugin-vue-jsx": "^5.0.1",
|
||||
"@vue/eslint-config-prettier": "^10.2.0",
|
||||
"@vue/eslint-config-typescript": "^14.6.0",
|
||||
"@vue/tsconfig": "^0.7.0",
|
||||
"eslint": "^9.31.0",
|
||||
"eslint-plugin-oxlint": "~1.1.0",
|
||||
"eslint-plugin-vue": "~10.2.0",
|
||||
"jiti": "^2.4.2",
|
||||
"naive-ui": "^2.42.0",
|
||||
"npm-run-all2": "^8.0.4",
|
||||
"oxlint": "~1.1.0",
|
||||
"prettier": "3.5.3",
|
||||
"typescript": "~5.8.3",
|
||||
"vite": "npm:rolldown-vite@latest",
|
||||
"vite-plugin-vue-devtools": "^7.7.7",
|
||||
"vue-tsc": "^2.2.12",
|
||||
},
|
||||
},
|
||||
},
|
||||
"packages": {
|
||||
"@ampproject/remapping": ["@ampproject/remapping@2.3.0", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw=="],
|
||||
|
||||
"@antfu/utils": ["@antfu/utils@0.7.10", "", {}, "sha512-+562v9k4aI80m1+VuMHehNJWLOFjBnXn3tdOitzD0il5b7smkSBal4+a3oKiQTbrwMmN/TBUMDvbdoWDehgOww=="],
|
||||
|
||||
"@babel/code-frame": ["@babel/code-frame@7.27.1", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.27.1", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg=="],
|
||||
|
||||
"@babel/compat-data": ["@babel/compat-data@7.28.0", "", {}, "sha512-60X7qkglvrap8mn1lh2ebxXdZYtUcpd7gsmy9kLaBJ4i/WdY8PqTSdxyA8qraikqKQK5C1KRBKXqznrVapyNaw=="],
|
||||
|
||||
"@babel/core": ["@babel/core@7.28.0", "", { "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.0", "@babel/helper-compilation-targets": "^7.27.2", "@babel/helper-module-transforms": "^7.27.3", "@babel/helpers": "^7.27.6", "@babel/parser": "^7.28.0", "@babel/template": "^7.27.2", "@babel/traverse": "^7.28.0", "@babel/types": "^7.28.0", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-UlLAnTPrFdNGoFtbSXwcGFQBtQZJCNjaN6hQNP3UPvuNXT1i82N26KL3dZeIpNalWywr9IuQuncaAfUaS1g6sQ=="],
|
||||
|
||||
"@babel/generator": ["@babel/generator@7.28.0", "", { "dependencies": { "@babel/parser": "^7.28.0", "@babel/types": "^7.28.0", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-lJjzvrbEeWrhB4P3QBsH7tey117PjLZnDbLiQEKjQ/fNJTjuq4HSqgFA+UNSwZT8D7dxxbnuSBMsa1lrWzKlQg=="],
|
||||
|
||||
"@babel/helper-annotate-as-pure": ["@babel/helper-annotate-as-pure@7.27.3", "", { "dependencies": { "@babel/types": "^7.27.3" } }, "sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg=="],
|
||||
|
||||
"@babel/helper-compilation-targets": ["@babel/helper-compilation-targets@7.27.2", "", { "dependencies": { "@babel/compat-data": "^7.27.2", "@babel/helper-validator-option": "^7.27.1", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" } }, "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ=="],
|
||||
|
||||
"@babel/helper-create-class-features-plugin": ["@babel/helper-create-class-features-plugin@7.27.1", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.1", "@babel/helper-member-expression-to-functions": "^7.27.1", "@babel/helper-optimise-call-expression": "^7.27.1", "@babel/helper-replace-supers": "^7.27.1", "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", "@babel/traverse": "^7.27.1", "semver": "^6.3.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-QwGAmuvM17btKU5VqXfb+Giw4JcN0hjuufz3DYnpeVDvZLAObloM77bhMXiqry3Iio+Ai4phVRDwl6WU10+r5A=="],
|
||||
|
||||
"@babel/helper-globals": ["@babel/helper-globals@7.28.0", "", {}, "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw=="],
|
||||
|
||||
"@babel/helper-member-expression-to-functions": ["@babel/helper-member-expression-to-functions@7.27.1", "", { "dependencies": { "@babel/traverse": "^7.27.1", "@babel/types": "^7.27.1" } }, "sha512-E5chM8eWjTp/aNoVpcbfM7mLxu9XGLWYise2eBKGQomAk/Mb4XoxyqXTZbuTohbsl8EKqdlMhnDI2CCLfcs9wA=="],
|
||||
|
||||
"@babel/helper-module-imports": ["@babel/helper-module-imports@7.27.1", "", { "dependencies": { "@babel/traverse": "^7.27.1", "@babel/types": "^7.27.1" } }, "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w=="],
|
||||
|
||||
"@babel/helper-module-transforms": ["@babel/helper-module-transforms@7.27.3", "", { "dependencies": { "@babel/helper-module-imports": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1", "@babel/traverse": "^7.27.3" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-dSOvYwvyLsWBeIRyOeHXp5vPj5l1I011r52FM1+r1jCERv+aFXYk4whgQccYEGYxK2H3ZAIA8nuPkQ0HaUo3qg=="],
|
||||
|
||||
"@babel/helper-optimise-call-expression": ["@babel/helper-optimise-call-expression@7.27.1", "", { "dependencies": { "@babel/types": "^7.27.1" } }, "sha512-URMGH08NzYFhubNSGJrpUEphGKQwMQYBySzat5cAByY1/YgIRkULnIy3tAMeszlL/so2HbeilYloUmSpd7GdVw=="],
|
||||
|
||||
"@babel/helper-plugin-utils": ["@babel/helper-plugin-utils@7.27.1", "", {}, "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw=="],
|
||||
|
||||
"@babel/helper-replace-supers": ["@babel/helper-replace-supers@7.27.1", "", { "dependencies": { "@babel/helper-member-expression-to-functions": "^7.27.1", "@babel/helper-optimise-call-expression": "^7.27.1", "@babel/traverse": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-7EHz6qDZc8RYS5ElPoShMheWvEgERonFCs7IAonWLLUTXW59DP14bCZt89/GKyreYn8g3S83m21FelHKbeDCKA=="],
|
||||
|
||||
"@babel/helper-skip-transparent-expression-wrappers": ["@babel/helper-skip-transparent-expression-wrappers@7.27.1", "", { "dependencies": { "@babel/traverse": "^7.27.1", "@babel/types": "^7.27.1" } }, "sha512-Tub4ZKEXqbPjXgWLl2+3JpQAYBJ8+ikpQ2Ocj/q/r0LwE3UhENh7EUabyHjz2kCEsrRY83ew2DQdHluuiDQFzg=="],
|
||||
|
||||
"@babel/helper-string-parser": ["@babel/helper-string-parser@7.27.1", "", {}, "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA=="],
|
||||
|
||||
"@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.27.1", "", {}, "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow=="],
|
||||
|
||||
"@babel/helper-validator-option": ["@babel/helper-validator-option@7.27.1", "", {}, "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg=="],
|
||||
|
||||
"@babel/helpers": ["@babel/helpers@7.27.6", "", { "dependencies": { "@babel/template": "^7.27.2", "@babel/types": "^7.27.6" } }, "sha512-muE8Tt8M22638HU31A3CgfSUciwz1fhATfoVai05aPXGor//CdWDCbnlY1yvBPo07njuVOCNGCSp/GTt12lIug=="],
|
||||
|
||||
"@babel/parser": ["@babel/parser@7.28.0", "", { "dependencies": { "@babel/types": "^7.28.0" }, "bin": "./bin/babel-parser.js" }, "sha512-jVZGvOxOuNSsuQuLRTh13nU0AogFlw32w/MT+LV6D3sP5WdbW61E77RnkbaO2dUvmPAYrBDJXGn5gGS6tH4j8g=="],
|
||||
|
||||
"@babel/plugin-proposal-decorators": ["@babel/plugin-proposal-decorators@7.28.0", "", { "dependencies": { "@babel/helper-create-class-features-plugin": "^7.27.1", "@babel/helper-plugin-utils": "^7.27.1", "@babel/plugin-syntax-decorators": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-zOiZqvANjWDUaUS9xMxbMcK/Zccztbe/6ikvUXaG9nsPH3w6qh5UaPGAnirI/WhIbZ8m3OHU0ReyPrknG+ZKeg=="],
|
||||
|
||||
"@babel/plugin-syntax-decorators": ["@babel/plugin-syntax-decorators@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-YMq8Z87Lhl8EGkmb0MwYkt36QnxC+fzCgrl66ereamPlYToRpIk5nUjKUY3QKLWq8mwUB1BgbeXcTJhZOCDg5A=="],
|
||||
|
||||
"@babel/plugin-syntax-import-attributes": ["@babel/plugin-syntax-import-attributes@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-oFT0FrKHgF53f4vOsZGi2Hh3I35PfSmVs4IBFLFj4dnafP+hIWDLg3VyKmUHfLoLHlyxY4C7DGtmHuJgn+IGww=="],
|
||||
|
||||
"@babel/plugin-syntax-import-meta": ["@babel/plugin-syntax-import-meta@7.10.4", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.10.4" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g=="],
|
||||
|
||||
"@babel/plugin-syntax-jsx": ["@babel/plugin-syntax-jsx@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-y8YTNIeKoyhGd9O0Jiyzyyqk8gdjnumGTQPsz0xOZOQ2RmkVJeZ1vmmfIvFEKqucBG6axJGBZDE/7iI5suUI/w=="],
|
||||
|
||||
"@babel/plugin-syntax-typescript": ["@babel/plugin-syntax-typescript@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-xfYCBMxveHrRMnAWl1ZlPXOZjzkN82THFvLhQhFXFt81Z5HnN+EtUkZhv/zcKpmT3fzmWZB0ywiBrbC3vogbwQ=="],
|
||||
|
||||
"@babel/plugin-transform-typescript": ["@babel/plugin-transform-typescript@7.28.0", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.3", "@babel/helper-create-class-features-plugin": "^7.27.1", "@babel/helper-plugin-utils": "^7.27.1", "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", "@babel/plugin-syntax-typescript": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-4AEiDEBPIZvLQaWlc9liCavE0xRM0dNca41WtBeM3jgFptfUOSG9z0uteLhq6+3rq+WB6jIvUwKDTpXEHPJ2Vg=="],
|
||||
|
||||
"@babel/template": ["@babel/template@7.27.2", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/parser": "^7.27.2", "@babel/types": "^7.27.1" } }, "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw=="],
|
||||
|
||||
"@babel/traverse": ["@babel/traverse@7.28.0", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.0", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.28.0", "@babel/template": "^7.27.2", "@babel/types": "^7.28.0", "debug": "^4.3.1" } }, "sha512-mGe7UK5wWyh0bKRfupsUchrQGqvDbZDbKJw+kcRGSmdHVYrv+ltd0pnpDTVpiTqnaBru9iEvA8pz8W46v0Amwg=="],
|
||||
|
||||
"@babel/types": ["@babel/types@7.28.1", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1" } }, "sha512-x0LvFTekgSX+83TI28Y9wYPUfzrnl2aT5+5QLnO6v7mSJYtEEevuDRN0F0uSHRk1G1IWZC43o00Y0xDDrpBGPQ=="],
|
||||
|
||||
"@css-render/plugin-bem": ["@css-render/plugin-bem@0.15.14", "", { "peerDependencies": { "css-render": "~0.15.14" } }, "sha512-QK513CJ7yEQxm/P3EwsI+d+ha8kSOcjGvD6SevM41neEMxdULE+18iuQK6tEChAWMOQNQPLG/Rw3Khb69r5neg=="],
|
||||
|
||||
"@css-render/vue3-ssr": ["@css-render/vue3-ssr@0.15.14", "", { "peerDependencies": { "vue": "^3.0.11" } }, "sha512-//8027GSbxE9n3QlD73xFY6z4ZbHbvrOVB7AO6hsmrEzGbg+h2A09HboUyDgu+xsmj7JnvJD39Irt+2D0+iV8g=="],
|
||||
|
||||
"@emnapi/core": ["@emnapi/core@1.4.4", "", { "dependencies": { "@emnapi/wasi-threads": "1.0.3", "tslib": "^2.4.0" } }, "sha512-A9CnAbC6ARNMKcIcrQwq6HeHCjpcBZ5wSx4U01WXCqEKlrzB9F9315WDNHkrs2xbx7YjjSxbUYxuN6EQzpcY2g=="],
|
||||
|
||||
"@emnapi/runtime": ["@emnapi/runtime@1.4.4", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-hHyapA4A3gPaDCNfiqyZUStTMqIkKRshqPIuDOXv1hcBnD4U3l8cP0T1HMCfGRxQ6V64TGCcoswChANyOAwbQg=="],
|
||||
|
||||
"@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.0.3", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-8K5IFFsQqF9wQNJptGbS6FNKgUTsSRYnTqNCG1vPP8jFdjSv18n2mQfJpkt2Oibo9iBEzcDnDxNwKTzC7svlJw=="],
|
||||
|
||||
"@emotion/hash": ["@emotion/hash@0.8.0", "", {}, "sha512-kBJtf7PH6aWwZ6fka3zQ0p6SBYzx4fl1LoZXE2RrnYST9Xljm7WfKJrU4g/Xr3Beg72MLrp1AWNUmuYJTL7Cow=="],
|
||||
|
||||
"@eslint-community/eslint-utils": ["@eslint-community/eslint-utils@4.7.0", "", { "dependencies": { "eslint-visitor-keys": "^3.4.3" }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, "sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw=="],
|
||||
|
||||
"@eslint-community/regexpp": ["@eslint-community/regexpp@4.12.1", "", {}, "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ=="],
|
||||
|
||||
"@eslint/config-array": ["@eslint/config-array@0.21.0", "", { "dependencies": { "@eslint/object-schema": "^2.1.6", "debug": "^4.3.1", "minimatch": "^3.1.2" } }, "sha512-ENIdc4iLu0d93HeYirvKmrzshzofPw6VkZRKQGe9Nv46ZnWUzcF1xV01dcvEg/1wXUR61OmmlSfyeyO7EvjLxQ=="],
|
||||
|
||||
"@eslint/config-helpers": ["@eslint/config-helpers@0.3.0", "", {}, "sha512-ViuymvFmcJi04qdZeDc2whTHryouGcDlaxPqarTD0ZE10ISpxGUVZGZDx4w01upyIynL3iu6IXH2bS1NhclQMw=="],
|
||||
|
||||
"@eslint/core": ["@eslint/core@0.15.1", "", { "dependencies": { "@types/json-schema": "^7.0.15" } }, "sha512-bkOp+iumZCCbt1K1CmWf0R9pM5yKpDv+ZXtvSyQpudrI9kuFLp+bM2WOPXImuD/ceQuaa8f5pj93Y7zyECIGNA=="],
|
||||
|
||||
"@eslint/eslintrc": ["@eslint/eslintrc@3.3.1", "", { "dependencies": { "ajv": "^6.12.4", "debug": "^4.3.2", "espree": "^10.0.1", "globals": "^14.0.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", "js-yaml": "^4.1.0", "minimatch": "^3.1.2", "strip-json-comments": "^3.1.1" } }, "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ=="],
|
||||
|
||||
"@eslint/js": ["@eslint/js@9.31.0", "", {}, "sha512-LOm5OVt7D4qiKCqoiPbA7LWmI+tbw1VbTUowBcUMgQSuM6poJufkFkYDcQpo5KfgD39TnNySV26QjOh7VFpSyw=="],
|
||||
|
||||
"@eslint/object-schema": ["@eslint/object-schema@2.1.6", "", {}, "sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA=="],
|
||||
|
||||
"@eslint/plugin-kit": ["@eslint/plugin-kit@0.3.3", "", { "dependencies": { "@eslint/core": "^0.15.1", "levn": "^0.4.1" } }, "sha512-1+WqvgNMhmlAambTvT3KPtCl/Ibr68VldY2XY40SL1CE0ZXiakFR/cbTspaF5HsnpDMvcYYoJHfl4980NBjGag=="],
|
||||
|
||||
"@fingerprintjs/fingerprintjs": ["@fingerprintjs/fingerprintjs@4.6.2", "", { "dependencies": { "tslib": "^2.4.1" } }, "sha512-g8mXuqcFKbgH2CZKwPfVtsUJDHyvcgIABQI7Y0tzWEFXpGxJaXuAuzlifT2oTakjDBLTK4Gaa9/5PERDhqUjtw=="],
|
||||
|
||||
"@fontsource-variable/nunito": ["@fontsource-variable/nunito@5.2.6", "", {}, "sha512-dGYTQ0Hl94jjfMraYefrURHGH8fk/vL/1zYAZGofiPJVs6C0OkM8T87Te5Gwrbe6HG/XEMm5lib8AqasTN3ucw=="],
|
||||
|
||||
"@hcaptcha/vue3-hcaptcha": ["@hcaptcha/vue3-hcaptcha@1.3.0", "", { "dependencies": { "vue": "^3.2.19" } }, "sha512-IEonS6JiYdU7uy6aeib8cYtMO4nj8utwStbA9bWHyYbOvOvhpkV+AW8vfSKh6SntYxqle/TRwhv+kU9p92CfsA=="],
|
||||
|
||||
"@humanfs/core": ["@humanfs/core@0.19.1", "", {}, "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA=="],
|
||||
|
||||
"@humanfs/node": ["@humanfs/node@0.16.6", "", { "dependencies": { "@humanfs/core": "^0.19.1", "@humanwhocodes/retry": "^0.3.0" } }, "sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw=="],
|
||||
|
||||
"@humanwhocodes/module-importer": ["@humanwhocodes/module-importer@1.0.1", "", {}, "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA=="],
|
||||
|
||||
"@humanwhocodes/retry": ["@humanwhocodes/retry@0.4.3", "", {}, "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ=="],
|
||||
|
||||
"@isaacs/fs-minipass": ["@isaacs/fs-minipass@4.0.1", "", { "dependencies": { "minipass": "^7.0.4" } }, "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w=="],
|
||||
|
||||
"@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.12", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-OuLGC46TjB5BbN1dH8JULVVZY4WTdkF7tV9Ys6wLL1rubZnCMstOhNHueU5bLCrnRuDhKPDM4g6sw4Bel5Gzqg=="],
|
||||
|
||||
"@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="],
|
||||
|
||||
"@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.4", "", {}, "sha512-VT2+G1VQs/9oz078bLrYbecdZKs912zQlkelYpuf+SXF+QvZDYJlbx/LSx+meSAwdDFnF8FVXW92AVjjkVmgFw=="],
|
||||
|
||||
"@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.29", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-uw6guiW/gcAGPDhLmd77/6lW8QLeiV5RUTsAX46Db6oLhGaVj4lhnPwb184s1bkc8kdVg/+h988dro8GRDpmYQ=="],
|
||||
|
||||
"@juggle/resize-observer": ["@juggle/resize-observer@3.4.0", "", {}, "sha512-dfLbk+PwWvFzSxwk3n5ySL0hfBog779o8h68wK/7/APo/7cgyWp5jcXockbxdk5kFRkbeXWm4Fbi9FrdN381sA=="],
|
||||
|
||||
"@kurkle/color": ["@kurkle/color@0.3.4", "", {}, "sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w=="],
|
||||
|
||||
"@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@0.2.12", "", { "dependencies": { "@emnapi/core": "^1.4.3", "@emnapi/runtime": "^1.4.3", "@tybys/wasm-util": "^0.10.0" } }, "sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ=="],
|
||||
|
||||
"@nodelib/fs.scandir": ["@nodelib/fs.scandir@2.1.5", "", { "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" } }, "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g=="],
|
||||
|
||||
"@nodelib/fs.stat": ["@nodelib/fs.stat@2.0.5", "", {}, "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A=="],
|
||||
|
||||
"@nodelib/fs.walk": ["@nodelib/fs.walk@1.2.8", "", { "dependencies": { "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" } }, "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg=="],
|
||||
|
||||
"@oxc-project/runtime": ["@oxc-project/runtime@0.77.0", "", {}, "sha512-cMbHs/DaomWSjxeJ79G10GA5hzJW9A7CZ+/cO+KuPZ7Trf3Rr07qSLauC4Ns8ba4DKVDjd8VSC9nVLpw6jpoGQ=="],
|
||||
|
||||
"@oxc-project/types": ["@oxc-project/types@0.77.0", "", {}, "sha512-iUQj185VvCPnSba+ltUV5tVDrPX6LeZVtQywnnoGbe4oJ1VKvDKisjGkD/AvVtdm98b/BdsVS35IlJV1m2mBBA=="],
|
||||
|
||||
"@oxlint/darwin-arm64": ["@oxlint/darwin-arm64@1.1.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-sSnR3SOxIU/QfaqXrcQ0UVUkzJO0bcInQ7dMhHa102gVAgWjp1fBeMVCM0adEY0UNmEXrRkgD/rQtQgn9YAU+w=="],
|
||||
|
||||
"@oxlint/darwin-x64": ["@oxlint/darwin-x64@1.1.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-Jvd3fHnzY2OYbmsg9NSGPoBkGViDGHSFnBKyJQ9LOIw7lxAyQBG2Quxc3GYPFR/f9OYho9C3p4+dIaAJfKhnsw=="],
|
||||
|
||||
"@oxlint/linux-arm64-gnu": ["@oxlint/linux-arm64-gnu@1.1.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-MgW4iskOdXuoR+wDXIJUfbdnTg2eo2FnQRaD6ZqhnDTDa7LnV+06rp/Cg3aGj2X9jSEcKDv/bMbYQuot7WRs6Q=="],
|
||||
|
||||
"@oxlint/linux-arm64-musl": ["@oxlint/linux-arm64-musl@1.1.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-a+pkEKmDRdrW+y0gtZ/m68ElVW2VZgATGbMxDgDYFpdiMx9Y0pUPwTMZ2EX/17Aslop4c1BiDSFDK7aEBxKR2g=="],
|
||||
|
||||
"@oxlint/linux-x64-gnu": ["@oxlint/linux-x64-gnu@1.1.0", "", { "os": "linux", "cpu": "x64" }, "sha512-wNBsXCKVZMvUTcFitrV1wTsdhUAv8l+XQxHxciZ2SO6dpNnWEb2YCxSAIOXeyzBLdO4pIODYcSy38CvGue7TwA=="],
|
||||
|
||||
"@oxlint/linux-x64-musl": ["@oxlint/linux-x64-musl@1.1.0", "", { "os": "linux", "cpu": "x64" }, "sha512-pZD0lt6A5j2Wp70fgIYk4GoPfKTZ8mHWamWIpKFT7aSkFkiOi6nhLWDFvMEIHWRTK3LgkWUNcnWPp4brvin4wQ=="],
|
||||
|
||||
"@oxlint/win32-arm64": ["@oxlint/win32-arm64@1.1.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-rT6uXQvE80+B+L04HJf30uF26426FPI9i9DAY2AxBUhrpNwhqkDEhQdd9ilFWVC7SSbpHgAs50lo+ImSAAkHPQ=="],
|
||||
|
||||
"@oxlint/win32-x64": ["@oxlint/win32-x64@1.1.0", "", { "os": "win32", "cpu": "x64" }, "sha512-x6r5yvM3wEty93Bx0NuNK+kutUyS/K55itkUrxdExoK6GcmVDboGGuhju9HyU2cM/IWLEWO8RHcXSyaxr9GR5g=="],
|
||||
|
||||
"@pkgr/core": ["@pkgr/core@0.2.7", "", {}, "sha512-YLT9Zo3oNPJoBjBc4q8G2mjU4tqIbf5CEOORbUUr48dCD9q3umJ3IPlVqOqDakPfd2HuwccBaqlGhN4Gmr5OWg=="],
|
||||
|
||||
"@polka/url": ["@polka/url@1.0.0-next.29", "", {}, "sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww=="],
|
||||
|
||||
"@rolldown/binding-android-arm64": ["@rolldown/binding-android-arm64@1.0.0-beta.27", "", { "os": "android", "cpu": "arm64" }, "sha512-IJL3efUJmvb5MfTEi7bGK4jq3ZFAzVbSy+vmul0DcdrglUd81Tfyy7Zzq2oM0tUgmACG32d8Jz/ykbpbf+3C5A=="],
|
||||
|
||||
"@rolldown/binding-darwin-arm64": ["@rolldown/binding-darwin-arm64@1.0.0-beta.27", "", { "os": "darwin", "cpu": "arm64" }, "sha512-TXTiuHbtnHfb0c44vNfWfIyEFJ0BFUf63ip9Z4mj8T2zRcZXQYVger4OuAxnwGNGBgDyHo1VaNBG+Vxn2VrpqQ=="],
|
||||
|
||||
"@rolldown/binding-darwin-x64": ["@rolldown/binding-darwin-x64@1.0.0-beta.27", "", { "os": "darwin", "cpu": "x64" }, "sha512-Jpjflgvbolh+fAaaEajPJQCOpZMawYMbNVzuZp3nidX1B7kMAP7NEKp9CWzthoL2Y8RfD7OApN6bx4+vFurTaw=="],
|
||||
|
||||
"@rolldown/binding-freebsd-x64": ["@rolldown/binding-freebsd-x64@1.0.0-beta.27", "", { "os": "freebsd", "cpu": "x64" }, "sha512-07ZNlXIunyS1jCTnene7aokkzCZNBUnmnJWu4Nz5X5XQvVHJNjsDhPFJTlNmneSDzA3vGkRNwdECKXiDTH/CqA=="],
|
||||
|
||||
"@rolldown/binding-linux-arm-gnueabihf": ["@rolldown/binding-linux-arm-gnueabihf@1.0.0-beta.27", "", { "os": "linux", "cpu": "arm" }, "sha512-z74ah00oyKnTUtaIbg34TaIU1PYM8tGE1bK6aUs8OLZ9sWW4g3Xo5A0nit2zyeanmYFvrAUxnt3Bpk+mTZCtlg=="],
|
||||
|
||||
"@rolldown/binding-linux-arm64-gnu": ["@rolldown/binding-linux-arm64-gnu@1.0.0-beta.27", "", { "os": "linux", "cpu": "arm64" }, "sha512-b9oKl/M5OIyAcosS73BmjOZOjvcONV97t2SnKpgwfDX/mjQO3dBgTYyvHMFA6hfhIDW1+2XVQR/k5uzBULFhoA=="],
|
||||
|
||||
"@rolldown/binding-linux-arm64-musl": ["@rolldown/binding-linux-arm64-musl@1.0.0-beta.27", "", { "os": "linux", "cpu": "arm64" }, "sha512-RmaNSkVmAH8u/r5Q+v4O0zL4HY8pLrvlM5wBoBrb/QHDQgksGKBqhecpg1ERER0Q7gMh/GJUz6JiiD55Q+9UOA=="],
|
||||
|
||||
"@rolldown/binding-linux-arm64-ohos": ["@rolldown/binding-linux-arm64-ohos@1.0.0-beta.27", "", { "os": "none", "cpu": "arm64" }, "sha512-gq78fI/g0cp1UKFMk53kP/oZAgYOXbaqdadVMuCJc0CoSkDJcpO2YIasRs/QYlE91QWfcHD5RZl9zbf4ksTS/w=="],
|
||||
|
||||
"@rolldown/binding-linux-x64-gnu": ["@rolldown/binding-linux-x64-gnu@1.0.0-beta.27", "", { "os": "linux", "cpu": "x64" }, "sha512-yS/GreJ6BT44dHu1WLigc50S8jZA+pDzzsf8tqRptUTwi5YW7dX3NqcDlc/lXsZqu57aKynLljgClYAm90LEKw=="],
|
||||
|
||||
"@rolldown/binding-linux-x64-musl": ["@rolldown/binding-linux-x64-musl@1.0.0-beta.27", "", { "os": "linux", "cpu": "x64" }, "sha512-6FV9To1sXewGHY4NaCPeOE5p5o1qfuAjj+m75WVIPw9HEJVsQoC5QiTL5wWVNqSMch4X0eWnQ6WsQolU6sGMIA=="],
|
||||
|
||||
"@rolldown/binding-wasm32-wasi": ["@rolldown/binding-wasm32-wasi@1.0.0-beta.27", "", { "dependencies": { "@napi-rs/wasm-runtime": "^0.2.12" }, "cpu": "none" }, "sha512-VcxdhF0PQda9krFJHw4DqUkdAsHWYs/Uz/Kr/zhU8zMFDzmK6OdUgl9emGj9wTzXAEHYkAMDhk+OJBRJvp424g=="],
|
||||
|
||||
"@rolldown/binding-win32-arm64-msvc": ["@rolldown/binding-win32-arm64-msvc@1.0.0-beta.27", "", { "os": "win32", "cpu": "arm64" }, "sha512-3bXSARqSf8jLHrQ1/tw9pX1GwIR9jA6OEsqTgdC0DdpoZ+34sbJXE9Nse3dQ0foGLKBkh4PqDv/rm2Thu9oVBw=="],
|
||||
|
||||
"@rolldown/binding-win32-ia32-msvc": ["@rolldown/binding-win32-ia32-msvc@1.0.0-beta.27", "", { "os": "win32", "cpu": "ia32" }, "sha512-xPGcKb+W8NIWAf5KApsUIrhiKH5NImTarICge5jQ2m0BBxD31crio4OXy/eYVq5CZkqkqszLQz2fWZcWNmbzlQ=="],
|
||||
|
||||
"@rolldown/binding-win32-x64-msvc": ["@rolldown/binding-win32-x64-msvc@1.0.0-beta.27", "", { "os": "win32", "cpu": "x64" }, "sha512-3y1G8ARpXBAcz4RJM5nzMU6isS/gXZl8SuX8lS2piFOnQMiOp6ajeelnciD+EgG4ej793zvNvr+WZtdnao2yrw=="],
|
||||
|
||||
"@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-beta.19", "", {}, "sha512-3FL3mnMbPu0muGOCaKAhhFEYmqv9eTfPSJRJmANrCwtgK8VuxpsZDGK+m0LYAGoyO8+0j5uRe4PeyPDK1yA/hA=="],
|
||||
|
||||
"@rollup/pluginutils": ["@rollup/pluginutils@5.2.0", "", { "dependencies": { "@types/estree": "^1.0.0", "estree-walker": "^2.0.2", "picomatch": "^4.0.2" }, "peerDependencies": { "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" }, "optionalPeers": ["rollup"] }, "sha512-qWJ2ZTbmumwiLFomfzTyt5Kng4hwPi9rwCYN4SHb6eaRU1KNO4ccxINHr/VhH4GgPlt1XfSTLX2LBTme8ne4Zw=="],
|
||||
|
||||
"@sec-ant/readable-stream": ["@sec-ant/readable-stream@0.4.1", "", {}, "sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg=="],
|
||||
|
||||
"@sindresorhus/merge-streams": ["@sindresorhus/merge-streams@4.0.0", "", {}, "sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ=="],
|
||||
|
||||
"@tailwindcss/node": ["@tailwindcss/node@4.1.11", "", { "dependencies": { "@ampproject/remapping": "^2.3.0", "enhanced-resolve": "^5.18.1", "jiti": "^2.4.2", "lightningcss": "1.30.1", "magic-string": "^0.30.17", "source-map-js": "^1.2.1", "tailwindcss": "4.1.11" } }, "sha512-yzhzuGRmv5QyU9qLNg4GTlYI6STedBWRE7NjxP45CsFYYq9taI0zJXZBMqIC/c8fViNLhmrbpSFS57EoxUmD6Q=="],
|
||||
|
||||
"@tailwindcss/oxide": ["@tailwindcss/oxide@4.1.11", "", { "dependencies": { "detect-libc": "^2.0.4", "tar": "^7.4.3" }, "optionalDependencies": { "@tailwindcss/oxide-android-arm64": "4.1.11", "@tailwindcss/oxide-darwin-arm64": "4.1.11", "@tailwindcss/oxide-darwin-x64": "4.1.11", "@tailwindcss/oxide-freebsd-x64": "4.1.11", "@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.11", "@tailwindcss/oxide-linux-arm64-gnu": "4.1.11", "@tailwindcss/oxide-linux-arm64-musl": "4.1.11", "@tailwindcss/oxide-linux-x64-gnu": "4.1.11", "@tailwindcss/oxide-linux-x64-musl": "4.1.11", "@tailwindcss/oxide-wasm32-wasi": "4.1.11", "@tailwindcss/oxide-win32-arm64-msvc": "4.1.11", "@tailwindcss/oxide-win32-x64-msvc": "4.1.11" } }, "sha512-Q69XzrtAhuyfHo+5/HMgr1lAiPP/G40OMFAnws7xcFEYqcypZmdW8eGXaOUIeOl1dzPJBPENXgbjsOyhg2nkrg=="],
|
||||
|
||||
"@tailwindcss/oxide-android-arm64": ["@tailwindcss/oxide-android-arm64@4.1.11", "", { "os": "android", "cpu": "arm64" }, "sha512-3IfFuATVRUMZZprEIx9OGDjG3Ou3jG4xQzNTvjDoKmU9JdmoCohQJ83MYd0GPnQIu89YoJqvMM0G3uqLRFtetg=="],
|
||||
|
||||
"@tailwindcss/oxide-darwin-arm64": ["@tailwindcss/oxide-darwin-arm64@4.1.11", "", { "os": "darwin", "cpu": "arm64" }, "sha512-ESgStEOEsyg8J5YcMb1xl8WFOXfeBmrhAwGsFxxB2CxY9evy63+AtpbDLAyRkJnxLy2WsD1qF13E97uQyP1lfQ=="],
|
||||
|
||||
"@tailwindcss/oxide-darwin-x64": ["@tailwindcss/oxide-darwin-x64@4.1.11", "", { "os": "darwin", "cpu": "x64" }, "sha512-EgnK8kRchgmgzG6jE10UQNaH9Mwi2n+yw1jWmof9Vyg2lpKNX2ioe7CJdf9M5f8V9uaQxInenZkOxnTVL3fhAw=="],
|
||||
|
||||
"@tailwindcss/oxide-freebsd-x64": ["@tailwindcss/oxide-freebsd-x64@4.1.11", "", { "os": "freebsd", "cpu": "x64" }, "sha512-xdqKtbpHs7pQhIKmqVpxStnY1skuNh4CtbcyOHeX1YBE0hArj2romsFGb6yUmzkq/6M24nkxDqU8GYrKrz+UcA=="],
|
||||
|
||||
"@tailwindcss/oxide-linux-arm-gnueabihf": ["@tailwindcss/oxide-linux-arm-gnueabihf@4.1.11", "", { "os": "linux", "cpu": "arm" }, "sha512-ryHQK2eyDYYMwB5wZL46uoxz2zzDZsFBwfjssgB7pzytAeCCa6glsiJGjhTEddq/4OsIjsLNMAiMlHNYnkEEeg=="],
|
||||
|
||||
"@tailwindcss/oxide-linux-arm64-gnu": ["@tailwindcss/oxide-linux-arm64-gnu@4.1.11", "", { "os": "linux", "cpu": "arm64" }, "sha512-mYwqheq4BXF83j/w75ewkPJmPZIqqP1nhoghS9D57CLjsh3Nfq0m4ftTotRYtGnZd3eCztgbSPJ9QhfC91gDZQ=="],
|
||||
|
||||
"@tailwindcss/oxide-linux-arm64-musl": ["@tailwindcss/oxide-linux-arm64-musl@4.1.11", "", { "os": "linux", "cpu": "arm64" }, "sha512-m/NVRFNGlEHJrNVk3O6I9ggVuNjXHIPoD6bqay/pubtYC9QIdAMpS+cswZQPBLvVvEF6GtSNONbDkZrjWZXYNQ=="],
|
||||
|
||||
"@tailwindcss/oxide-linux-x64-gnu": ["@tailwindcss/oxide-linux-x64-gnu@4.1.11", "", { "os": "linux", "cpu": "x64" }, "sha512-YW6sblI7xukSD2TdbbaeQVDysIm/UPJtObHJHKxDEcW2exAtY47j52f8jZXkqE1krdnkhCMGqP3dbniu1Te2Fg=="],
|
||||
|
||||
"@tailwindcss/oxide-linux-x64-musl": ["@tailwindcss/oxide-linux-x64-musl@4.1.11", "", { "os": "linux", "cpu": "x64" }, "sha512-e3C/RRhGunWYNC3aSF7exsQkdXzQ/M+aYuZHKnw4U7KQwTJotnWsGOIVih0s2qQzmEzOFIJ3+xt7iq67K/p56Q=="],
|
||||
|
||||
"@tailwindcss/oxide-wasm32-wasi": ["@tailwindcss/oxide-wasm32-wasi@4.1.11", "", { "dependencies": { "@emnapi/core": "^1.4.3", "@emnapi/runtime": "^1.4.3", "@emnapi/wasi-threads": "^1.0.2", "@napi-rs/wasm-runtime": "^0.2.11", "@tybys/wasm-util": "^0.9.0", "tslib": "^2.8.0" }, "cpu": "none" }, "sha512-Xo1+/GU0JEN/C/dvcammKHzeM6NqKovG+6921MR6oadee5XPBaKOumrJCXvopJ/Qb5TH7LX/UAywbqrP4lax0g=="],
|
||||
|
||||
"@tailwindcss/oxide-win32-arm64-msvc": ["@tailwindcss/oxide-win32-arm64-msvc@4.1.11", "", { "os": "win32", "cpu": "arm64" }, "sha512-UgKYx5PwEKrac3GPNPf6HVMNhUIGuUh4wlDFR2jYYdkX6pL/rn73zTq/4pzUm8fOjAn5L8zDeHp9iXmUGOXZ+w=="],
|
||||
|
||||
"@tailwindcss/oxide-win32-x64-msvc": ["@tailwindcss/oxide-win32-x64-msvc@4.1.11", "", { "os": "win32", "cpu": "x64" }, "sha512-YfHoggn1j0LK7wR82TOucWc5LDCguHnoS879idHekmmiR7g9HUtMw9MI0NHatS28u/Xlkfi9w5RJWgz2Dl+5Qg=="],
|
||||
|
||||
"@tailwindcss/vite": ["@tailwindcss/vite@4.1.11", "", { "dependencies": { "@tailwindcss/node": "4.1.11", "@tailwindcss/oxide": "4.1.11", "tailwindcss": "4.1.11" }, "peerDependencies": { "vite": "^5.2.0 || ^6 || ^7" } }, "sha512-RHYhrR3hku0MJFRV+fN2gNbDNEh3dwKvY8XJvTxCSXeMOsCRSr+uKvDWQcbizrHgjML6ZmTE5OwMrl5wKcujCw=="],
|
||||
|
||||
"@tsconfig/node22": ["@tsconfig/node22@22.0.2", "", {}, "sha512-Kmwj4u8sDRDrMYRoN9FDEcXD8UpBSaPQQ24Gz+Gamqfm7xxn+GBR7ge/Z7pK8OXNGyUzbSwJj+TH6B+DS/epyA=="],
|
||||
|
||||
"@tybys/wasm-util": ["@tybys/wasm-util@0.10.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-VyyPYFlOMNylG45GoAe0xDoLwWuowvf92F9kySqzYh8vmYm7D2u4iUJKa1tOUpS70Ku13ASrOkS4ScXFsTaCNQ=="],
|
||||
|
||||
"@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="],
|
||||
|
||||
"@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="],
|
||||
|
||||
"@types/katex": ["@types/katex@0.16.7", "", {}, "sha512-HMwFiRujE5PjrgwHQ25+bsLJgowjGjm5Z8FVSf0N6PwgJrwxH0QxzHYDcKsTfV3wva0vzrpqMTJS2jXPr5BMEQ=="],
|
||||
|
||||
"@types/lodash": ["@types/lodash@4.17.20", "", {}, "sha512-H3MHACvFUEiujabxhaI/ImO6gUrd8oOurg7LQtS7mbwIXA/cUqWrvBsaeJ23aZEPk1TAYkurjfMbSELfoCXlGA=="],
|
||||
|
||||
"@types/lodash-es": ["@types/lodash-es@4.17.12", "", { "dependencies": { "@types/lodash": "*" } }, "sha512-0NgftHUcV4v34VhXm8QBSftKVXtbkBG3ViCjs6+eJ5a6y6Mi/jiFGPc1sC7QK+9BFhWrURE3EOggmWaSxL9OzQ=="],
|
||||
|
||||
"@types/node": ["@types/node@22.16.4", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-PYRhNtZdm2wH/NT2k/oAJ6/f2VD2N2Dag0lGlx2vWgMSJXGNmlce5MiTQzoWAiIJtso30mjnfQCOKVH+kAQC/g=="],
|
||||
|
||||
"@types/web-bluetooth": ["@types/web-bluetooth@0.0.21", "", {}, "sha512-oIQLCGWtcFZy2JW77j9k8nHzAOpqMHLQejDA48XXMWH6tjCQHz5RCFz1bzsmROyL6PUm+LLnUiI4BCn221inxA=="],
|
||||
|
||||
"@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.37.0", "", { "dependencies": { "@eslint-community/regexpp": "^4.10.0", "@typescript-eslint/scope-manager": "8.37.0", "@typescript-eslint/type-utils": "8.37.0", "@typescript-eslint/utils": "8.37.0", "@typescript-eslint/visitor-keys": "8.37.0", "graphemer": "^1.4.0", "ignore": "^7.0.0", "natural-compare": "^1.4.0", "ts-api-utils": "^2.1.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.37.0", "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.9.0" } }, "sha512-jsuVWeIkb6ggzB+wPCsR4e6loj+rM72ohW6IBn2C+5NCvfUVY8s33iFPySSVXqtm5Hu29Ne/9bnA0JmyLmgenA=="],
|
||||
|
||||
"@typescript-eslint/parser": ["@typescript-eslint/parser@8.37.0", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.37.0", "@typescript-eslint/types": "8.37.0", "@typescript-eslint/typescript-estree": "8.37.0", "@typescript-eslint/visitor-keys": "8.37.0", "debug": "^4.3.4" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.9.0" } }, "sha512-kVIaQE9vrN9RLCQMQ3iyRlVJpTiDUY6woHGb30JDkfJErqrQEmtdWH3gV0PBAfGZgQXoqzXOO0T3K6ioApbbAA=="],
|
||||
|
||||
"@typescript-eslint/project-service": ["@typescript-eslint/project-service@8.37.0", "", { "dependencies": { "@typescript-eslint/tsconfig-utils": "^8.37.0", "@typescript-eslint/types": "^8.37.0", "debug": "^4.3.4" }, "peerDependencies": { "typescript": ">=4.8.4 <5.9.0" } }, "sha512-BIUXYsbkl5A1aJDdYJCBAo8rCEbAvdquQ8AnLb6z5Lp1u3x5PNgSSx9A/zqYc++Xnr/0DVpls8iQ2cJs/izTXA=="],
|
||||
|
||||
"@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.37.0", "", { "dependencies": { "@typescript-eslint/types": "8.37.0", "@typescript-eslint/visitor-keys": "8.37.0" } }, "sha512-0vGq0yiU1gbjKob2q691ybTg9JX6ShiVXAAfm2jGf3q0hdP6/BruaFjL/ManAR/lj05AvYCH+5bbVo0VtzmjOA=="],
|
||||
|
||||
"@typescript-eslint/tsconfig-utils": ["@typescript-eslint/tsconfig-utils@8.37.0", "", { "peerDependencies": { "typescript": ">=4.8.4 <5.9.0" } }, "sha512-1/YHvAVTimMM9mmlPvTec9NP4bobA1RkDbMydxG8omqwJJLEW/Iy2C4adsAESIXU3WGLXFHSZUU+C9EoFWl4Zg=="],
|
||||
|
||||
"@typescript-eslint/type-utils": ["@typescript-eslint/type-utils@8.37.0", "", { "dependencies": { "@typescript-eslint/types": "8.37.0", "@typescript-eslint/typescript-estree": "8.37.0", "@typescript-eslint/utils": "8.37.0", "debug": "^4.3.4", "ts-api-utils": "^2.1.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.9.0" } }, "sha512-SPkXWIkVZxhgwSwVq9rqj/4VFo7MnWwVaRNznfQDc/xPYHjXnPfLWn+4L6FF1cAz6e7dsqBeMawgl7QjUMj4Ow=="],
|
||||
|
||||
"@typescript-eslint/types": ["@typescript-eslint/types@8.37.0", "", {}, "sha512-ax0nv7PUF9NOVPs+lmQ7yIE7IQmAf8LGcXbMvHX5Gm+YJUYNAl340XkGnrimxZ0elXyoQJuN5sbg6C4evKA4SQ=="],
|
||||
|
||||
"@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.37.0", "", { "dependencies": { "@typescript-eslint/project-service": "8.37.0", "@typescript-eslint/tsconfig-utils": "8.37.0", "@typescript-eslint/types": "8.37.0", "@typescript-eslint/visitor-keys": "8.37.0", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", "minimatch": "^9.0.4", "semver": "^7.6.0", "ts-api-utils": "^2.1.0" }, "peerDependencies": { "typescript": ">=4.8.4 <5.9.0" } }, "sha512-zuWDMDuzMRbQOM+bHyU4/slw27bAUEcKSKKs3hcv2aNnc/tvE/h7w60dwVw8vnal2Pub6RT1T7BI8tFZ1fE+yg=="],
|
||||
|
||||
"@typescript-eslint/utils": ["@typescript-eslint/utils@8.37.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.7.0", "@typescript-eslint/scope-manager": "8.37.0", "@typescript-eslint/types": "8.37.0", "@typescript-eslint/typescript-estree": "8.37.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.9.0" } }, "sha512-TSFvkIW6gGjN2p6zbXo20FzCABbyUAuq6tBvNRGsKdsSQ6a7rnV6ADfZ7f4iI3lIiXc4F4WWvtUfDw9CJ9pO5A=="],
|
||||
|
||||
"@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.37.0", "", { "dependencies": { "@typescript-eslint/types": "8.37.0", "eslint-visitor-keys": "^4.2.1" } }, "sha512-YzfhzcTnZVPiLfP/oeKtDp2evwvHLMe0LOy7oe+hb9KKIumLNohYS9Hgp1ifwpu42YWxhZE8yieggz6JpqO/1w=="],
|
||||
|
||||
"@vicons/material": ["@vicons/material@0.13.0", "", {}, "sha512-lKVxFNprM+CaBkUH3gt6VjIeiMsKQl2zARQMwTCZruQl2vRHzyeZiKeCflWS99CEfv2JzX/6y697smxlzyxcVw=="],
|
||||
|
||||
"@vitejs/plugin-vue": ["@vitejs/plugin-vue@6.0.0", "", { "dependencies": { "@rolldown/pluginutils": "1.0.0-beta.19" }, "peerDependencies": { "vite": "^5.0.0 || ^6.0.0 || ^7.0.0", "vue": "^3.2.25" } }, "sha512-iAliE72WsdhjzTOp2DtvKThq1VBC4REhwRcaA+zPAAph6I+OQhUXv+Xu2KS7ElxYtb7Zc/3R30Hwv1DxEo7NXQ=="],
|
||||
|
||||
"@vitejs/plugin-vue-jsx": ["@vitejs/plugin-vue-jsx@5.0.1", "", { "dependencies": { "@babel/core": "^7.27.7", "@babel/plugin-transform-typescript": "^7.27.1", "@rolldown/pluginutils": "^1.0.0-beta.21", "@vue/babel-plugin-jsx": "^1.4.0" }, "peerDependencies": { "vite": "^5.0.0 || ^6.0.0 || ^7.0.0", "vue": "^3.0.0" } }, "sha512-X7qmQMXbdDh+sfHUttXokPD0cjPkMFoae7SgbkF9vi3idGUKmxLcnU2Ug49FHwiKXebfzQRIm5yK3sfCJzNBbg=="],
|
||||
|
||||
"@volar/language-core": ["@volar/language-core@2.4.15", "", { "dependencies": { "@volar/source-map": "2.4.15" } }, "sha512-3VHw+QZU0ZG9IuQmzT68IyN4hZNd9GchGPhbD9+pa8CVv7rnoOZwo7T8weIbrRmihqy3ATpdfXFnqRrfPVK6CA=="],
|
||||
|
||||
"@volar/source-map": ["@volar/source-map@2.4.15", "", {}, "sha512-CPbMWlUN6hVZJYGcU/GSoHu4EnCHiLaXI9n8c9la6RaI9W5JHX+NqG+GSQcB0JdC2FIBLdZJwGsfKyBB71VlTg=="],
|
||||
|
||||
"@volar/typescript": ["@volar/typescript@2.4.15", "", { "dependencies": { "@volar/language-core": "2.4.15", "path-browserify": "^1.0.1", "vscode-uri": "^3.0.8" } }, "sha512-2aZ8i0cqPGjXb4BhkMsPYDkkuc2ZQ6yOpqwAuNwUoncELqoy5fRgOQtLR9gB0g902iS0NAkvpIzs27geVyVdPg=="],
|
||||
|
||||
"@vue/babel-helper-vue-transform-on": ["@vue/babel-helper-vue-transform-on@1.4.0", "", {}, "sha512-mCokbouEQ/ocRce/FpKCRItGo+013tHg7tixg3DUNS+6bmIchPt66012kBMm476vyEIJPafrvOf4E5OYj3shSw=="],
|
||||
|
||||
"@vue/babel-plugin-jsx": ["@vue/babel-plugin-jsx@1.4.0", "", { "dependencies": { "@babel/helper-module-imports": "^7.25.9", "@babel/helper-plugin-utils": "^7.26.5", "@babel/plugin-syntax-jsx": "^7.25.9", "@babel/template": "^7.26.9", "@babel/traverse": "^7.26.9", "@babel/types": "^7.26.9", "@vue/babel-helper-vue-transform-on": "1.4.0", "@vue/babel-plugin-resolve-type": "1.4.0", "@vue/shared": "^3.5.13" }, "peerDependencies": { "@babel/core": "^7.0.0-0" }, "optionalPeers": ["@babel/core"] }, "sha512-9zAHmwgMWlaN6qRKdrg1uKsBKHvnUU+Py+MOCTuYZBoZsopa90Di10QRjB+YPnVss0BZbG/H5XFwJY1fTxJWhA=="],
|
||||
|
||||
"@vue/babel-plugin-resolve-type": ["@vue/babel-plugin-resolve-type@1.4.0", "", { "dependencies": { "@babel/code-frame": "^7.26.2", "@babel/helper-module-imports": "^7.25.9", "@babel/helper-plugin-utils": "^7.26.5", "@babel/parser": "^7.26.9", "@vue/compiler-sfc": "^3.5.13" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-4xqDRRbQQEWHQyjlYSgZsWj44KfiF6D+ktCuXyZ8EnVDYV3pztmXJDf1HveAjUAXxAnR8daCQT51RneWWxtTyQ=="],
|
||||
|
||||
"@vue/compiler-core": ["@vue/compiler-core@3.5.17", "", { "dependencies": { "@babel/parser": "^7.27.5", "@vue/shared": "3.5.17", "entities": "^4.5.0", "estree-walker": "^2.0.2", "source-map-js": "^1.2.1" } }, "sha512-Xe+AittLbAyV0pabcN7cP7/BenRBNcteM4aSDCtRvGw0d9OL+HG1u/XHLY/kt1q4fyMeZYXyIYrsHuPSiDPosA=="],
|
||||
|
||||
"@vue/compiler-dom": ["@vue/compiler-dom@3.5.17", "", { "dependencies": { "@vue/compiler-core": "3.5.17", "@vue/shared": "3.5.17" } }, "sha512-+2UgfLKoaNLhgfhV5Ihnk6wB4ljyW1/7wUIog2puUqajiC29Lp5R/IKDdkebh9jTbTogTbsgB+OY9cEWzG95JQ=="],
|
||||
|
||||
"@vue/compiler-sfc": ["@vue/compiler-sfc@3.5.17", "", { "dependencies": { "@babel/parser": "^7.27.5", "@vue/compiler-core": "3.5.17", "@vue/compiler-dom": "3.5.17", "@vue/compiler-ssr": "3.5.17", "@vue/shared": "3.5.17", "estree-walker": "^2.0.2", "magic-string": "^0.30.17", "postcss": "^8.5.6", "source-map-js": "^1.2.1" } }, "sha512-rQQxbRJMgTqwRugtjw0cnyQv9cP4/4BxWfTdRBkqsTfLOHWykLzbOc3C4GGzAmdMDxhzU/1Ija5bTjMVrddqww=="],
|
||||
|
||||
"@vue/compiler-ssr": ["@vue/compiler-ssr@3.5.17", "", { "dependencies": { "@vue/compiler-dom": "3.5.17", "@vue/shared": "3.5.17" } }, "sha512-hkDbA0Q20ZzGgpj5uZjb9rBzQtIHLS78mMilwrlpWk2Ep37DYntUz0PonQ6kr113vfOEdM+zTBuJDaceNIW0tQ=="],
|
||||
|
||||
"@vue/compiler-vue2": ["@vue/compiler-vue2@2.7.16", "", { "dependencies": { "de-indent": "^1.0.2", "he": "^1.2.0" } }, "sha512-qYC3Psj9S/mfu9uVi5WvNZIzq+xnXMhOwbTFKKDD7b1lhpnn71jXSFdTQ+WsIEk0ONCd7VV2IMm7ONl6tbQ86A=="],
|
||||
|
||||
"@vue/devtools-api": ["@vue/devtools-api@7.7.7", "", { "dependencies": { "@vue/devtools-kit": "^7.7.7" } }, "sha512-lwOnNBH2e7x1fIIbVT7yF5D+YWhqELm55/4ZKf45R9T8r9dE2AIOy8HKjfqzGsoTHFbWbr337O4E0A0QADnjBg=="],
|
||||
|
||||
"@vue/devtools-core": ["@vue/devtools-core@7.7.7", "", { "dependencies": { "@vue/devtools-kit": "^7.7.7", "@vue/devtools-shared": "^7.7.7", "mitt": "^3.0.1", "nanoid": "^5.1.0", "pathe": "^2.0.3", "vite-hot-client": "^2.0.4" }, "peerDependencies": { "vue": "^3.0.0" } }, "sha512-9z9TLbfC+AjAi1PQyWX+OErjIaJmdFlbDHcD+cAMYKY6Bh5VlsAtCeGyRMrXwIlMEQPukvnWt3gZBLwTAIMKzQ=="],
|
||||
|
||||
"@vue/devtools-kit": ["@vue/devtools-kit@7.7.7", "", { "dependencies": { "@vue/devtools-shared": "^7.7.7", "birpc": "^2.3.0", "hookable": "^5.5.3", "mitt": "^3.0.1", "perfect-debounce": "^1.0.0", "speakingurl": "^14.0.1", "superjson": "^2.2.2" } }, "sha512-wgoZtxcTta65cnZ1Q6MbAfePVFxfM+gq0saaeytoph7nEa7yMXoi6sCPy4ufO111B9msnw0VOWjPEFCXuAKRHA=="],
|
||||
|
||||
"@vue/devtools-shared": ["@vue/devtools-shared@7.7.7", "", { "dependencies": { "rfdc": "^1.4.1" } }, "sha512-+udSj47aRl5aKb0memBvcUG9koarqnxNM5yjuREvqwK6T3ap4mn3Zqqc17QrBFTqSMjr3HK1cvStEZpMDpfdyw=="],
|
||||
|
||||
"@vue/eslint-config-prettier": ["@vue/eslint-config-prettier@10.2.0", "", { "dependencies": { "eslint-config-prettier": "^10.0.1", "eslint-plugin-prettier": "^5.2.2" }, "peerDependencies": { "eslint": ">= 8.21.0", "prettier": ">= 3.0.0" } }, "sha512-GL3YBLwv/+b86yHcNNfPJxOTtVFJ4Mbc9UU3zR+KVoG7SwGTjPT+32fXamscNumElhcpXW3mT0DgzS9w32S7Bw=="],
|
||||
|
||||
"@vue/eslint-config-typescript": ["@vue/eslint-config-typescript@14.6.0", "", { "dependencies": { "@typescript-eslint/utils": "^8.35.1", "fast-glob": "^3.3.3", "typescript-eslint": "^8.35.1", "vue-eslint-parser": "^10.2.0" }, "peerDependencies": { "eslint": "^9.10.0", "eslint-plugin-vue": "^9.28.0 || ^10.0.0", "typescript": ">=4.8.4" }, "optionalPeers": ["typescript"] }, "sha512-UpiRY/7go4Yps4mYCjkvlIbVWmn9YvPGQDxTAlcKLphyaD77LjIu3plH4Y9zNT0GB4f3K5tMmhhtRhPOgrQ/bQ=="],
|
||||
|
||||
"@vue/language-core": ["@vue/language-core@2.2.12", "", { "dependencies": { "@volar/language-core": "2.4.15", "@vue/compiler-dom": "^3.5.0", "@vue/compiler-vue2": "^2.7.16", "@vue/shared": "^3.5.0", "alien-signals": "^1.0.3", "minimatch": "^9.0.3", "muggle-string": "^0.4.1", "path-browserify": "^1.0.1" }, "peerDependencies": { "typescript": "*" }, "optionalPeers": ["typescript"] }, "sha512-IsGljWbKGU1MZpBPN+BvPAdr55YPkj2nB/TBNGNC32Vy2qLG25DYu/NBN2vNtZqdRbTRjaoYrahLrToim2NanA=="],
|
||||
|
||||
"@vue/reactivity": ["@vue/reactivity@3.5.17", "", { "dependencies": { "@vue/shared": "3.5.17" } }, "sha512-l/rmw2STIscWi7SNJp708FK4Kofs97zc/5aEPQh4bOsReD/8ICuBcEmS7KGwDj5ODQLYWVN2lNibKJL1z5b+Lw=="],
|
||||
|
||||
"@vue/runtime-core": ["@vue/runtime-core@3.5.17", "", { "dependencies": { "@vue/reactivity": "3.5.17", "@vue/shared": "3.5.17" } }, "sha512-QQLXa20dHg1R0ri4bjKeGFKEkJA7MMBxrKo2G+gJikmumRS7PTD4BOU9FKrDQWMKowz7frJJGqBffYMgQYS96Q=="],
|
||||
|
||||
"@vue/runtime-dom": ["@vue/runtime-dom@3.5.17", "", { "dependencies": { "@vue/reactivity": "3.5.17", "@vue/runtime-core": "3.5.17", "@vue/shared": "3.5.17", "csstype": "^3.1.3" } }, "sha512-8El0M60TcwZ1QMz4/os2MdlQECgGoVHPuLnQBU3m9h3gdNRW9xRmI8iLS4t/22OQlOE6aJvNNlBiCzPHur4H9g=="],
|
||||
|
||||
"@vue/server-renderer": ["@vue/server-renderer@3.5.17", "", { "dependencies": { "@vue/compiler-ssr": "3.5.17", "@vue/shared": "3.5.17" }, "peerDependencies": { "vue": "3.5.17" } }, "sha512-BOHhm8HalujY6lmC3DbqF6uXN/K00uWiEeF22LfEsm9Q93XeJ/plHTepGwf6tqFcF7GA5oGSSAAUock3VvzaCA=="],
|
||||
|
||||
"@vue/shared": ["@vue/shared@3.5.17", "", {}, "sha512-CabR+UN630VnsJO/jHWYBC1YVXyMq94KKp6iF5MQgZJs5I8cmjw6oVMO1oDbtBkENSHSSn/UadWlW/OAgdmKrg=="],
|
||||
|
||||
"@vue/tsconfig": ["@vue/tsconfig@0.7.0", "", { "peerDependencies": { "typescript": "5.x", "vue": "^3.4.0" }, "optionalPeers": ["typescript", "vue"] }, "sha512-ku2uNz5MaZ9IerPPUyOHzyjhXoX2kVJaVf7hL315DC17vS6IiZRmmCPfggNbU16QTvM80+uYYy3eYJB59WCtvg=="],
|
||||
|
||||
"@vueuse/core": ["@vueuse/core@13.5.0", "", { "dependencies": { "@types/web-bluetooth": "^0.0.21", "@vueuse/metadata": "13.5.0", "@vueuse/shared": "13.5.0" }, "peerDependencies": { "vue": "^3.5.0" } }, "sha512-wV7z0eUpifKmvmN78UBZX8T7lMW53Nrk6JP5+6hbzrB9+cJ3jr//hUlhl9TZO/03bUkMK6gGkQpqOPWoabr72g=="],
|
||||
|
||||
"@vueuse/metadata": ["@vueuse/metadata@13.5.0", "", {}, "sha512-euhItU3b0SqXxSy8u1XHxUCdQ8M++bsRs+TYhOLDU/OykS7KvJnyIFfep0XM5WjIFry9uAPlVSjmVHiqeshmkw=="],
|
||||
|
||||
"@vueuse/shared": ["@vueuse/shared@13.5.0", "", { "peerDependencies": { "vue": "^3.5.0" } }, "sha512-K7GrQIxJ/ANtucxIXbQlUHdB0TPA8c+q5i+zbrjxuhJCnJ9GtBg75sBSnvmLSxHKPg2Yo8w62PWksl9kwH0Q8g=="],
|
||||
|
||||
"acorn": ["acorn@8.15.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg=="],
|
||||
|
||||
"acorn-jsx": ["acorn-jsx@5.3.2", "", { "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ=="],
|
||||
|
||||
"ajv": ["ajv@6.12.6", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g=="],
|
||||
|
||||
"alien-signals": ["alien-signals@1.0.13", "", {}, "sha512-OGj9yyTnJEttvzhTUWuscOvtqxq5vrhF7vL9oS0xJ2mK0ItPYP1/y+vCFebfxoEyAz0++1AIwJ5CMr+Fk3nDmg=="],
|
||||
|
||||
"ansi-styles": ["ansi-styles@6.2.1", "", {}, "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug=="],
|
||||
|
||||
"ansis": ["ansis@4.1.0", "", {}, "sha512-BGcItUBWSMRgOCe+SVZJ+S7yTRG0eGt9cXAHev72yuGcY23hnLA7Bky5L/xLyPINoSN95geovfBkqoTlNZYa7w=="],
|
||||
|
||||
"argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="],
|
||||
|
||||
"aspnet-prerendering": ["aspnet-prerendering@3.0.1", "", { "dependencies": { "domain-task": "^3.0.0" } }, "sha512-nfOQYVKW3sYQMZBXNM2KPrXU2MOBuLn/gszRZM0Y1Pj4EpzCw1KjXiO681eQo4ZR1TLLzJ8L2sQbq0qeC1zxVg=="],
|
||||
|
||||
"async-validator": ["async-validator@4.2.5", "", {}, "sha512-7HhHjtERjqlNbZtqNqy2rckN/SpOOlmDliet+lP7k+eKZEjPk3DgyeU9lIXLdeLz0uBbbVp+9Qdow9wJWgwwfg=="],
|
||||
|
||||
"balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
|
||||
|
||||
"birpc": ["birpc@2.5.0", "", {}, "sha512-VSWO/W6nNQdyP520F1mhf+Lc2f8pjGQOtoHHm7Ze8Go1kX7akpVIrtTa0fn+HB0QJEDVacl6aO08YE0PgXfdnQ=="],
|
||||
|
||||
"boolbase": ["boolbase@1.0.0", "", {}, "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww=="],
|
||||
|
||||
"brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="],
|
||||
|
||||
"braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="],
|
||||
|
||||
"browserslist": ["browserslist@4.25.1", "", { "dependencies": { "caniuse-lite": "^1.0.30001726", "electron-to-chromium": "^1.5.173", "node-releases": "^2.0.19", "update-browserslist-db": "^1.1.3" }, "bin": { "browserslist": "cli.js" } }, "sha512-KGj0KoOMXLpSNkkEI6Z6mShmQy0bc1I+T7K9N81k4WWMrfz+6fQ6es80B/YLAeRoKvjYE1YSHHOW1qe9xIVzHw=="],
|
||||
|
||||
"buffer-from": ["buffer-from@1.1.2", "", {}, "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ=="],
|
||||
|
||||
"bundle-name": ["bundle-name@4.1.0", "", { "dependencies": { "run-applescript": "^7.0.0" } }, "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q=="],
|
||||
|
||||
"callsites": ["callsites@3.1.0", "", {}, "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ=="],
|
||||
|
||||
"caniuse-lite": ["caniuse-lite@1.0.30001727", "", {}, "sha512-pB68nIHmbN6L/4C6MH1DokyR3bYqFwjaSs/sWDHGj4CTcFtQUQMuJftVwWkXq7mNWOybD3KhUv3oWHoGxgP14Q=="],
|
||||
|
||||
"cfturnstile-vue3": ["cfturnstile-vue3@2.0.0", "", { "dependencies": { "vue": "^3.2.38" } }, "sha512-wamRC8ZoUAjvfOVoPAbJM14qqxc0gfjqfV6ESZh4rMs7G0yp+R4dpHNjxa7YAjdFTutaviMEZYCuK9tM4ZaGJQ=="],
|
||||
|
||||
"chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="],
|
||||
|
||||
"chart.js": ["chart.js@4.5.0", "", { "dependencies": { "@kurkle/color": "^0.3.0" } }, "sha512-aYeC/jDgSEx8SHWZvANYMioYMZ2KX02W6f6uVfyteuCGcadDLcYVHdfdygsTQkQ4TKn5lghoojAsPj5pu0SnvQ=="],
|
||||
|
||||
"chownr": ["chownr@3.0.0", "", {}, "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g=="],
|
||||
|
||||
"color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="],
|
||||
|
||||
"color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="],
|
||||
|
||||
"combine-errors": ["combine-errors@3.0.3", "", { "dependencies": { "custom-error-instance": "2.1.1", "lodash.uniqby": "4.5.0" } }, "sha512-C8ikRNRMygCwaTx+Ek3Yr+OuZzgZjduCOfSQBjbM8V3MfgcjSTeto/GXP6PAwKvJz/v15b7GHZvx5rOlczFw/Q=="],
|
||||
|
||||
"concat-map": ["concat-map@0.0.1", "", {}, "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="],
|
||||
|
||||
"convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="],
|
||||
|
||||
"copy-anything": ["copy-anything@3.0.5", "", { "dependencies": { "is-what": "^4.1.8" } }, "sha512-yCEafptTtb4bk7GLEQoM8KVJpxAfdBJYaXyzQEgQQQgYrZiDp8SJmGKlYza6CYjEDNstAdNdKA3UuoULlEbS6w=="],
|
||||
|
||||
"cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="],
|
||||
|
||||
"css-render": ["css-render@0.15.14", "", { "dependencies": { "@emotion/hash": "~0.8.0", "csstype": "~3.0.5" } }, "sha512-9nF4PdUle+5ta4W5SyZdLCCmFd37uVimSjg1evcTqKJCyvCEEj12WKzOSBNak6r4im4J4iYXKH1OWpUV5LBYFg=="],
|
||||
|
||||
"cssesc": ["cssesc@3.0.0", "", { "bin": { "cssesc": "bin/cssesc" } }, "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg=="],
|
||||
|
||||
"csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="],
|
||||
|
||||
"custom-error-instance": ["custom-error-instance@2.1.1", "", {}, "sha512-p6JFxJc3M4OTD2li2qaHkDCw9SfMw82Ldr6OC9Je1aXiGfhx2W8p3GaoeaGrPJTUN9NirTM/KTxHWMUdR1rsUg=="],
|
||||
|
||||
"date-fns": ["date-fns@3.6.0", "", {}, "sha512-fRHTG8g/Gif+kSh50gaGEdToemgfj74aRX3swtiouboip5JDLAyDE9F11nHMIcvOaXeOC6D7SpNhi7uFyB7Uww=="],
|
||||
|
||||
"date-fns-tz": ["date-fns-tz@3.2.0", "", { "peerDependencies": { "date-fns": "^3.0.0 || ^4.0.0" } }, "sha512-sg8HqoTEulcbbbVXeg84u5UnlsQa8GS5QXMqjjYIhS4abEVVKIUwe0/l/UhrZdKaL/W5eWZNlbTeEIiOXTcsBQ=="],
|
||||
|
||||
"de-indent": ["de-indent@1.0.2", "", {}, "sha512-e/1zu3xH5MQryN2zdVaF0OrdNLUbvWxzMbi+iNA6Bky7l1RoP8a2fIbRocyHclXt/arDrrR6lL3TqFD9pMQTsg=="],
|
||||
|
||||
"debug": ["debug@4.4.1", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ=="],
|
||||
|
||||
"deep-is": ["deep-is@0.1.4", "", {}, "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ=="],
|
||||
|
||||
"default-browser": ["default-browser@5.2.1", "", { "dependencies": { "bundle-name": "^4.1.0", "default-browser-id": "^5.0.0" } }, "sha512-WY/3TUME0x3KPYdRRxEJJvXRHV4PyPoUsxtZa78lwItwRQRHhd2U9xOscaT/YTf8uCXIAjeJOFBVEh/7FtD8Xg=="],
|
||||
|
||||
"default-browser-id": ["default-browser-id@5.0.0", "", {}, "sha512-A6p/pu/6fyBcA1TRz/GqWYPViplrftcW2gZC9q79ngNCKAeR/X3gcEdXQHl4KNXV+3wgIJ1CPkJQ3IHM6lcsyA=="],
|
||||
|
||||
"define-lazy-prop": ["define-lazy-prop@3.0.0", "", {}, "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg=="],
|
||||
|
||||
"detect-libc": ["detect-libc@2.0.4", "", {}, "sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA=="],
|
||||
|
||||
"domain-context": ["domain-context@0.5.1", "", {}, "sha512-WyTWkXciNvYYaQzdnKJtjlVSXHivtt0E/vCv36Bkwh+Sk4NXkrQpHxZT5BHYmKRVgxWMol1wcdurZCzyTT6Euw=="],
|
||||
|
||||
"domain-task": ["domain-task@3.0.3", "", { "dependencies": { "domain-context": "^0.5.1", "is-absolute-url": "^2.1.0", "isomorphic-fetch": "^2.2.1" } }, "sha512-7oAiY1AvjhVNVJbOwSHbrm6lEHczOSSCSqDkHp2ZO7vb/iOCGl7YNk/1cv4yKwSGhBMpBZ5mu+7cMorbWxWvOg=="],
|
||||
|
||||
"electron-to-chromium": ["electron-to-chromium@1.5.183", "", {}, "sha512-vCrDBYjQCAEefWGjlK3EpoSKfKbT10pR4XXPdn65q7snuNOZnthoVpBfZPykmDapOKfoD+MMIPG8ZjKyyc9oHA=="],
|
||||
|
||||
"encoding": ["encoding@0.1.13", "", { "dependencies": { "iconv-lite": "^0.6.2" } }, "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A=="],
|
||||
|
||||
"enhanced-resolve": ["enhanced-resolve@5.18.2", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.2.0" } }, "sha512-6Jw4sE1maoRJo3q8MsSIn2onJFbLTOjY9hlx4DZXmOKvLRd1Ok2kXmAGXaafL2+ijsJZ1ClYbl/pmqr9+k4iUQ=="],
|
||||
|
||||
"entities": ["entities@4.5.0", "", {}, "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="],
|
||||
|
||||
"error-stack-parser-es": ["error-stack-parser-es@0.1.5", "", {}, "sha512-xHku1X40RO+fO8yJ8Wh2f2rZWVjqyhb1zgq1yZ8aZRQkv6OOKhKWRUaht3eSCUbAOBaKIgM+ykwFLE+QUxgGeg=="],
|
||||
|
||||
"escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="],
|
||||
|
||||
"escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="],
|
||||
|
||||
"eslint": ["eslint@9.31.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.12.1", "@eslint/config-array": "^0.21.0", "@eslint/config-helpers": "^0.3.0", "@eslint/core": "^0.15.0", "@eslint/eslintrc": "^3.3.1", "@eslint/js": "9.31.0", "@eslint/plugin-kit": "^0.3.1", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", "@types/json-schema": "^7.0.15", "ajv": "^6.12.4", "chalk": "^4.0.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", "eslint-scope": "^8.4.0", "eslint-visitor-keys": "^4.2.1", "espree": "^10.4.0", "esquery": "^1.5.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", "lodash.merge": "^4.6.2", "minimatch": "^3.1.2", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, "peerDependencies": { "jiti": "*" }, "optionalPeers": ["jiti"], "bin": { "eslint": "bin/eslint.js" } }, "sha512-QldCVh/ztyKJJZLr4jXNUByx3gR+TDYZCRXEktiZoUR3PGy4qCmSbkxcIle8GEwGpb5JBZazlaJ/CxLidXdEbQ=="],
|
||||
|
||||
"eslint-config-prettier": ["eslint-config-prettier@10.1.5", "", { "peerDependencies": { "eslint": ">=7.0.0" }, "bin": { "eslint-config-prettier": "bin/cli.js" } }, "sha512-zc1UmCpNltmVY34vuLRV61r1K27sWuX39E+uyUnY8xS2Bex88VV9cugG+UZbRSRGtGyFboj+D8JODyme1plMpw=="],
|
||||
|
||||
"eslint-plugin-oxlint": ["eslint-plugin-oxlint@1.1.0", "", { "dependencies": { "jsonc-parser": "^3.3.1" } }, "sha512-spDWxcsAfoUDjSwxPrP2gfuOJ2Hrv8faqQ5Vkm90lURp4no5aWJQ09xRKmZroIPTuQCKYgG9nvnakdIbXGlijg=="],
|
||||
|
||||
"eslint-plugin-prettier": ["eslint-plugin-prettier@5.5.1", "", { "dependencies": { "prettier-linter-helpers": "^1.0.0", "synckit": "^0.11.7" }, "peerDependencies": { "@types/eslint": ">=8.0.0", "eslint": ">=8.0.0", "eslint-config-prettier": ">= 7.0.0 <10.0.0 || >=10.1.0", "prettier": ">=3.0.0" }, "optionalPeers": ["@types/eslint", "eslint-config-prettier"] }, "sha512-dobTkHT6XaEVOo8IO90Q4DOSxnm3Y151QxPJlM/vKC0bVy+d6cVWQZLlFiuZPP0wS6vZwSKeJgKkcS+KfMBlRw=="],
|
||||
|
||||
"eslint-plugin-vue": ["eslint-plugin-vue@10.2.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.4.0", "natural-compare": "^1.4.0", "nth-check": "^2.1.1", "postcss-selector-parser": "^6.0.15", "semver": "^7.6.3", "xml-name-validator": "^4.0.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "vue-eslint-parser": "^10.0.0" } }, "sha512-tl9s+KN3z0hN2b8fV2xSs5ytGl7Esk1oSCxULLwFcdaElhZ8btYYZFrWxvh4En+czrSDtuLCeCOGa8HhEZuBdQ=="],
|
||||
|
||||
"eslint-scope": ["eslint-scope@8.4.0", "", { "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" } }, "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg=="],
|
||||
|
||||
"eslint-visitor-keys": ["eslint-visitor-keys@4.2.1", "", {}, "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ=="],
|
||||
|
||||
"espree": ["espree@10.4.0", "", { "dependencies": { "acorn": "^8.15.0", "acorn-jsx": "^5.3.2", "eslint-visitor-keys": "^4.2.1" } }, "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ=="],
|
||||
|
||||
"esquery": ["esquery@1.6.0", "", { "dependencies": { "estraverse": "^5.1.0" } }, "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg=="],
|
||||
|
||||
"esrecurse": ["esrecurse@4.3.0", "", { "dependencies": { "estraverse": "^5.2.0" } }, "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag=="],
|
||||
|
||||
"estraverse": ["estraverse@5.3.0", "", {}, "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA=="],
|
||||
|
||||
"estree-walker": ["estree-walker@2.0.2", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="],
|
||||
|
||||
"esutils": ["esutils@2.0.3", "", {}, "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g=="],
|
||||
|
||||
"evtd": ["evtd@0.2.4", "", {}, "sha512-qaeGN5bx63s/AXgQo8gj6fBkxge+OoLddLniox5qtLAEY5HSnuSlISXVPxnSae1dWblvTh4/HoMIB+mbMsvZzw=="],
|
||||
|
||||
"execa": ["execa@9.6.0", "", { "dependencies": { "@sindresorhus/merge-streams": "^4.0.0", "cross-spawn": "^7.0.6", "figures": "^6.1.0", "get-stream": "^9.0.0", "human-signals": "^8.0.1", "is-plain-obj": "^4.1.0", "is-stream": "^4.0.1", "npm-run-path": "^6.0.0", "pretty-ms": "^9.2.0", "signal-exit": "^4.1.0", "strip-final-newline": "^4.0.0", "yoctocolors": "^2.1.1" } }, "sha512-jpWzZ1ZhwUmeWRhS7Qv3mhpOhLfwI+uAX4e5fOcXqwMR7EcJ0pj2kV1CVzHVMX/LphnKWD3LObjZCoJ71lKpHw=="],
|
||||
|
||||
"fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="],
|
||||
|
||||
"fast-diff": ["fast-diff@1.3.0", "", {}, "sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw=="],
|
||||
|
||||
"fast-glob": ["fast-glob@3.3.3", "", { "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", "micromatch": "^4.0.8" } }, "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg=="],
|
||||
|
||||
"fast-json-stable-stringify": ["fast-json-stable-stringify@2.1.0", "", {}, "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw=="],
|
||||
|
||||
"fast-levenshtein": ["fast-levenshtein@2.0.6", "", {}, "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw=="],
|
||||
|
||||
"fastq": ["fastq@1.19.1", "", { "dependencies": { "reusify": "^1.0.4" } }, "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ=="],
|
||||
|
||||
"fdir": ["fdir@6.4.6", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-hiFoqpyZcfNm1yc4u8oWCf9A2c4D3QjCrks3zmoVKVxpQRzmPNar1hUJcBG2RQHvEVGDN+Jm81ZheVLAQMK6+w=="],
|
||||
|
||||
"figures": ["figures@6.1.0", "", { "dependencies": { "is-unicode-supported": "^2.0.0" } }, "sha512-d+l3qxjSesT4V7v2fh+QnmFnUWv9lSpjarhShNTgBOfA0ttejbQUAlHLitbjkoRiDulW0OPoQPYIGhIC8ohejg=="],
|
||||
|
||||
"file-entry-cache": ["file-entry-cache@8.0.0", "", { "dependencies": { "flat-cache": "^4.0.0" } }, "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ=="],
|
||||
|
||||
"fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="],
|
||||
|
||||
"find-up": ["find-up@5.0.0", "", { "dependencies": { "locate-path": "^6.0.0", "path-exists": "^4.0.0" } }, "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng=="],
|
||||
|
||||
"flat-cache": ["flat-cache@4.0.1", "", { "dependencies": { "flatted": "^3.2.9", "keyv": "^4.5.4" } }, "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw=="],
|
||||
|
||||
"flatted": ["flatted@3.3.3", "", {}, "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg=="],
|
||||
|
||||
"fs-extra": ["fs-extra@11.3.0", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-Z4XaCL6dUDHfP/jT25jJKMmtxvuwbkrD1vNSMFlo9lNLY2c5FHYSQgHPRZUjAB26TpDEoW9HCOgplrdbaPV/ew=="],
|
||||
|
||||
"fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],
|
||||
|
||||
"gensync": ["gensync@1.0.0-beta.2", "", {}, "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg=="],
|
||||
|
||||
"get-stream": ["get-stream@9.0.1", "", { "dependencies": { "@sec-ant/readable-stream": "^0.4.1", "is-stream": "^4.0.1" } }, "sha512-kVCxPF3vQM/N0B1PmoqVUqgHP+EeVjmZSQn+1oCRPxd2P21P2F19lIgbR3HBosbB1PUhOAoctJnfEn2GbN2eZA=="],
|
||||
|
||||
"glob-parent": ["glob-parent@6.0.2", "", { "dependencies": { "is-glob": "^4.0.3" } }, "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A=="],
|
||||
|
||||
"globals": ["globals@14.0.0", "", {}, "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ=="],
|
||||
|
||||
"graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="],
|
||||
|
||||
"graphemer": ["graphemer@1.4.0", "", {}, "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag=="],
|
||||
|
||||
"has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="],
|
||||
|
||||
"he": ["he@1.2.0", "", { "bin": { "he": "bin/he" } }, "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw=="],
|
||||
|
||||
"highlight.js": ["highlight.js@11.11.1", "", {}, "sha512-Xwwo44whKBVCYoliBQwaPvtd/2tYFkRQtXDWj1nackaV2JPXx3L0+Jvd8/qCJ2p+ML0/XVkJ2q+Mr+UVdpJK5w=="],
|
||||
|
||||
"hookable": ["hookable@5.5.3", "", {}, "sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ=="],
|
||||
|
||||
"human-signals": ["human-signals@8.0.1", "", {}, "sha512-eKCa6bwnJhvxj14kZk5NCPc6Hb6BdsU9DZcOnmQKSnO1VKrfV0zCvtttPZUsBvjmNDn8rpcJfpwSYnHBjc95MQ=="],
|
||||
|
||||
"iconv-lite": ["iconv-lite@0.6.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="],
|
||||
|
||||
"ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="],
|
||||
|
||||
"import-fresh": ["import-fresh@3.3.1", "", { "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" } }, "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ=="],
|
||||
|
||||
"imurmurhash": ["imurmurhash@0.1.4", "", {}, "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA=="],
|
||||
|
||||
"is-absolute-url": ["is-absolute-url@2.1.0", "", {}, "sha512-vOx7VprsKyllwjSkLV79NIhpyLfr3jAp7VaTCMXOJHu4m0Ew1CZ2fcjASwmV1jI3BWuWHB013M48eyeldk9gYg=="],
|
||||
|
||||
"is-docker": ["is-docker@3.0.0", "", { "bin": { "is-docker": "cli.js" } }, "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ=="],
|
||||
|
||||
"is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="],
|
||||
|
||||
"is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="],
|
||||
|
||||
"is-inside-container": ["is-inside-container@1.0.0", "", { "dependencies": { "is-docker": "^3.0.0" }, "bin": { "is-inside-container": "cli.js" } }, "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA=="],
|
||||
|
||||
"is-number": ["is-number@7.0.0", "", {}, "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="],
|
||||
|
||||
"is-plain-obj": ["is-plain-obj@4.1.0", "", {}, "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg=="],
|
||||
|
||||
"is-stream": ["is-stream@2.0.1", "", {}, "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg=="],
|
||||
|
||||
"is-unicode-supported": ["is-unicode-supported@2.1.0", "", {}, "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ=="],
|
||||
|
||||
"is-what": ["is-what@4.1.16", "", {}, "sha512-ZhMwEosbFJkA0YhFnNDgTM4ZxDRsS6HqTo7qsZM08fehyRYIYa0yHu5R6mgo1n/8MgaPBXiPimPD77baVFYg+A=="],
|
||||
|
||||
"is-wsl": ["is-wsl@3.1.0", "", { "dependencies": { "is-inside-container": "^1.0.0" } }, "sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw=="],
|
||||
|
||||
"isexe": ["isexe@3.1.1", "", {}, "sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ=="],
|
||||
|
||||
"isomorphic-fetch": ["isomorphic-fetch@2.2.1", "", { "dependencies": { "node-fetch": "^1.0.1", "whatwg-fetch": ">=0.10.0" } }, "sha512-9c4TNAKYXM5PRyVcwUZrF3W09nQ+sO7+jydgs4ZGW9dhsLG2VOlISJABombdQqQRXCwuYG3sYV/puGf5rp0qmA=="],
|
||||
|
||||
"jiti": ["jiti@2.4.2", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A=="],
|
||||
|
||||
"js-base64": ["js-base64@3.7.7", "", {}, "sha512-7rCnleh0z2CkXhH67J8K1Ytz0b2Y+yxTPL+/KOJoa20hfnVQ/3/T6W/KflYI4bRHRagNeXeU2bkNGI3v1oS/lw=="],
|
||||
|
||||
"js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="],
|
||||
|
||||
"js-yaml": ["js-yaml@4.1.0", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA=="],
|
||||
|
||||
"jsesc": ["jsesc@3.1.0", "", { "bin": { "jsesc": "bin/jsesc" } }, "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA=="],
|
||||
|
||||
"json-buffer": ["json-buffer@3.0.1", "", {}, "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ=="],
|
||||
|
||||
"json-parse-even-better-errors": ["json-parse-even-better-errors@4.0.0", "", {}, "sha512-lR4MXjGNgkJc7tkQ97kb2nuEMnNCyU//XYVH0MKTGcXEiSudQ5MKGKen3C5QubYy0vmq+JGitUg92uuywGEwIA=="],
|
||||
|
||||
"json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="],
|
||||
|
||||
"json-stable-stringify-without-jsonify": ["json-stable-stringify-without-jsonify@1.0.1", "", {}, "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw=="],
|
||||
|
||||
"json5": ["json5@2.2.3", "", { "bin": { "json5": "lib/cli.js" } }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="],
|
||||
|
||||
"jsonc-parser": ["jsonc-parser@3.3.1", "", {}, "sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ=="],
|
||||
|
||||
"jsonfile": ["jsonfile@6.1.0", "", { "dependencies": { "universalify": "^2.0.0" }, "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ=="],
|
||||
|
||||
"keyv": ["keyv@4.5.4", "", { "dependencies": { "json-buffer": "3.0.1" } }, "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw=="],
|
||||
|
||||
"kolorist": ["kolorist@1.8.0", "", {}, "sha512-Y+60/zizpJ3HRH8DCss+q95yr6145JXZo46OTpFvDZWLfRCE4qChOyk1b26nMaNpfHHgxagk9dXT5OP0Tfe+dQ=="],
|
||||
|
||||
"levn": ["levn@0.4.1", "", { "dependencies": { "prelude-ls": "^1.2.1", "type-check": "~0.4.0" } }, "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ=="],
|
||||
|
||||
"lightningcss": ["lightningcss@1.30.1", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-darwin-arm64": "1.30.1", "lightningcss-darwin-x64": "1.30.1", "lightningcss-freebsd-x64": "1.30.1", "lightningcss-linux-arm-gnueabihf": "1.30.1", "lightningcss-linux-arm64-gnu": "1.30.1", "lightningcss-linux-arm64-musl": "1.30.1", "lightningcss-linux-x64-gnu": "1.30.1", "lightningcss-linux-x64-musl": "1.30.1", "lightningcss-win32-arm64-msvc": "1.30.1", "lightningcss-win32-x64-msvc": "1.30.1" } }, "sha512-xi6IyHML+c9+Q3W0S4fCQJOym42pyurFiJUHEcEyHS0CeKzia4yZDEsLlqOFykxOdHpNy0NmvVO31vcSqAxJCg=="],
|
||||
|
||||
"lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.30.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-c8JK7hyE65X1MHMN+Viq9n11RRC7hgin3HhYKhrMyaXflk5GVplZ60IxyoVtzILeKr+xAJwg6zK6sjTBJ0FKYQ=="],
|
||||
|
||||
"lightningcss-darwin-x64": ["lightningcss-darwin-x64@1.30.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-k1EvjakfumAQoTfcXUcHQZhSpLlkAuEkdMBsI/ivWw9hL+7FtilQc0Cy3hrx0AAQrVtQAbMI7YjCgYgvn37PzA=="],
|
||||
|
||||
"lightningcss-freebsd-x64": ["lightningcss-freebsd-x64@1.30.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-kmW6UGCGg2PcyUE59K5r0kWfKPAVy4SltVeut+umLCFoJ53RdCUWxcRDzO1eTaxf/7Q2H7LTquFHPL5R+Gjyig=="],
|
||||
|
||||
"lightningcss-linux-arm-gnueabihf": ["lightningcss-linux-arm-gnueabihf@1.30.1", "", { "os": "linux", "cpu": "arm" }, "sha512-MjxUShl1v8pit+6D/zSPq9S9dQ2NPFSQwGvxBCYaBYLPlCWuPh9/t1MRS8iUaR8i+a6w7aps+B4N0S1TYP/R+Q=="],
|
||||
|
||||
"lightningcss-linux-arm64-gnu": ["lightningcss-linux-arm64-gnu@1.30.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-gB72maP8rmrKsnKYy8XUuXi/4OctJiuQjcuqWNlJQ6jZiWqtPvqFziskH3hnajfvKB27ynbVCucKSm2rkQp4Bw=="],
|
||||
|
||||
"lightningcss-linux-arm64-musl": ["lightningcss-linux-arm64-musl@1.30.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-jmUQVx4331m6LIX+0wUhBbmMX7TCfjF5FoOH6SD1CttzuYlGNVpA7QnrmLxrsub43ClTINfGSYyHe2HWeLl5CQ=="],
|
||||
|
||||
"lightningcss-linux-x64-gnu": ["lightningcss-linux-x64-gnu@1.30.1", "", { "os": "linux", "cpu": "x64" }, "sha512-piWx3z4wN8J8z3+O5kO74+yr6ze/dKmPnI7vLqfSqI8bccaTGY5xiSGVIJBDd5K5BHlvVLpUB3S2YCfelyJ1bw=="],
|
||||
|
||||
"lightningcss-linux-x64-musl": ["lightningcss-linux-x64-musl@1.30.1", "", { "os": "linux", "cpu": "x64" }, "sha512-rRomAK7eIkL+tHY0YPxbc5Dra2gXlI63HL+v1Pdi1a3sC+tJTcFrHX+E86sulgAXeI7rSzDYhPSeHHjqFhqfeQ=="],
|
||||
|
||||
"lightningcss-win32-arm64-msvc": ["lightningcss-win32-arm64-msvc@1.30.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-mSL4rqPi4iXq5YVqzSsJgMVFENoa4nGTT/GjO2c0Yl9OuQfPsIfncvLrEW6RbbB24WtZ3xP/2CCmI3tNkNV4oA=="],
|
||||
|
||||
"lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.30.1", "", { "os": "win32", "cpu": "x64" }, "sha512-PVqXh48wh4T53F/1CCu8PIPCxLzWyCnn/9T5W1Jpmdy5h9Cwd+0YQS6/LwhHXSafuc61/xg9Lv5OrCby6a++jg=="],
|
||||
|
||||
"locate-path": ["locate-path@6.0.0", "", { "dependencies": { "p-locate": "^5.0.0" } }, "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw=="],
|
||||
|
||||
"lodash": ["lodash@4.17.21", "", {}, "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="],
|
||||
|
||||
"lodash-es": ["lodash-es@4.17.21", "", {}, "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw=="],
|
||||
|
||||
"lodash._baseiteratee": ["lodash._baseiteratee@4.7.0", "", { "dependencies": { "lodash._stringtopath": "~4.8.0" } }, "sha512-nqB9M+wITz0BX/Q2xg6fQ8mLkyfF7MU7eE+MNBNjTHFKeKaZAPEzEg+E8LWxKWf1DQVflNEn9N49yAuqKh2mWQ=="],
|
||||
|
||||
"lodash._basetostring": ["lodash._basetostring@4.12.0", "", {}, "sha512-SwcRIbyxnN6CFEEK4K1y+zuApvWdpQdBHM/swxP962s8HIxPO3alBH5t3m/dl+f4CMUug6sJb7Pww8d13/9WSw=="],
|
||||
|
||||
"lodash._baseuniq": ["lodash._baseuniq@4.6.0", "", { "dependencies": { "lodash._createset": "~4.0.0", "lodash._root": "~3.0.0" } }, "sha512-Ja1YevpHZctlI5beLA7oc5KNDhGcPixFhcqSiORHNsp/1QTv7amAXzw+gu4YOvErqVlMVyIJGgtzeepCnnur0A=="],
|
||||
|
||||
"lodash._createset": ["lodash._createset@4.0.3", "", {}, "sha512-GTkC6YMprrJZCYU3zcqZj+jkXkrXzq3IPBcF/fIPpNEAB4hZEtXU8zp/RwKOvZl43NUmwDbyRk3+ZTbeRdEBXA=="],
|
||||
|
||||
"lodash._root": ["lodash._root@3.0.1", "", {}, "sha512-O0pWuFSK6x4EXhM1dhZ8gchNtG7JMqBtrHdoUFUWXD7dJnNSUze1GuyQr5sOs0aCvgGeI3o/OJW8f4ca7FDxmQ=="],
|
||||
|
||||
"lodash._stringtopath": ["lodash._stringtopath@4.8.0", "", { "dependencies": { "lodash._basetostring": "~4.12.0" } }, "sha512-SXL66C731p0xPDC5LZg4wI5H+dJo/EO4KTqOMwLYCH3+FmmfAKJEZCm6ohGpI+T1xwsDsJCfL4OnhorllvlTPQ=="],
|
||||
|
||||
"lodash.merge": ["lodash.merge@4.6.2", "", {}, "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ=="],
|
||||
|
||||
"lodash.throttle": ["lodash.throttle@4.1.1", "", {}, "sha512-wIkUCfVKpVsWo3JSZlc+8MB5it+2AN5W8J7YVMST30UrvcQNZ1Okbj+rbVniijTWE6FGYy4XJq/rHkas8qJMLQ=="],
|
||||
|
||||
"lodash.uniqby": ["lodash.uniqby@4.5.0", "", { "dependencies": { "lodash._baseiteratee": "~4.7.0", "lodash._baseuniq": "~4.6.0" } }, "sha512-IRt7cfTtHy6f1aRVA5n7kT8rgN3N1nH6MOWLcHfpWG2SH19E3JksLK38MktLxZDhlAjCP9jpIXkOnRXlu6oByQ=="],
|
||||
|
||||
"lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="],
|
||||
|
||||
"magic-string": ["magic-string@0.30.17", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0" } }, "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA=="],
|
||||
|
||||
"memorystream": ["memorystream@0.3.1", "", {}, "sha512-S3UwM3yj5mtUSEfP41UZmt/0SCoVYUcU1rkXv+BQ5Ig8ndL4sPoJNBUJERafdPb5jjHJGuMgytgKvKIf58XNBw=="],
|
||||
|
||||
"merge2": ["merge2@1.4.1", "", {}, "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg=="],
|
||||
|
||||
"micromatch": ["micromatch@4.0.8", "", { "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" } }, "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA=="],
|
||||
|
||||
"minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="],
|
||||
|
||||
"minipass": ["minipass@7.1.2", "", {}, "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw=="],
|
||||
|
||||
"minizlib": ["minizlib@3.0.2", "", { "dependencies": { "minipass": "^7.1.2" } }, "sha512-oG62iEk+CYt5Xj2YqI5Xi9xWUeZhDI8jjQmC5oThVH5JGCTgIjr7ciJDzC7MBzYd//WvR1OTmP5Q38Q8ShQtVA=="],
|
||||
|
||||
"mitt": ["mitt@3.0.1", "", {}, "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw=="],
|
||||
|
||||
"mkdirp": ["mkdirp@3.0.1", "", { "bin": { "mkdirp": "dist/cjs/src/bin.js" } }, "sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg=="],
|
||||
|
||||
"mrmime": ["mrmime@2.0.1", "", {}, "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ=="],
|
||||
|
||||
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
|
||||
|
||||
"muggle-string": ["muggle-string@0.4.1", "", {}, "sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ=="],
|
||||
|
||||
"naive-ui": ["naive-ui@2.42.0", "", { "dependencies": { "@css-render/plugin-bem": "^0.15.14", "@css-render/vue3-ssr": "^0.15.14", "@types/katex": "^0.16.2", "@types/lodash": "^4.14.198", "@types/lodash-es": "^4.17.9", "async-validator": "^4.2.5", "css-render": "^0.15.14", "csstype": "^3.1.3", "date-fns": "^3.6.0", "date-fns-tz": "^3.1.3", "evtd": "^0.2.4", "highlight.js": "^11.8.0", "lodash": "^4.17.21", "lodash-es": "^4.17.21", "seemly": "^0.3.8", "treemate": "^0.3.11", "vdirs": "^0.1.8", "vooks": "^0.2.12", "vueuc": "^0.4.63" }, "peerDependencies": { "vue": "^3.0.0" } }, "sha512-c7cXR2YgOjgtBadXHwiWL4Y0tpGLAI5W5QzzHksOi22iuHXoSGMAzdkVTGVPE/PM0MSGQ/JtUIzCx2Y0hU0vTQ=="],
|
||||
|
||||
"nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="],
|
||||
|
||||
"natural-compare": ["natural-compare@1.4.0", "", {}, "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw=="],
|
||||
|
||||
"node-fetch": ["node-fetch@1.7.3", "", { "dependencies": { "encoding": "^0.1.11", "is-stream": "^1.0.1" } }, "sha512-NhZ4CsKx7cYm2vSrBAr2PvFOe6sWDf0UYLRqA6svUYg7+/TSfVAu49jYC4BvQ4Sms9SZgdqGBgroqfDhJdTyKQ=="],
|
||||
|
||||
"node-releases": ["node-releases@2.0.19", "", {}, "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw=="],
|
||||
|
||||
"npm-normalize-package-bin": ["npm-normalize-package-bin@4.0.0", "", {}, "sha512-TZKxPvItzai9kN9H/TkmCtx/ZN/hvr3vUycjlfmH0ootY9yFBzNOpiXAdIn1Iteqsvk4lQn6B5PTrt+n6h8k/w=="],
|
||||
|
||||
"npm-run-all2": ["npm-run-all2@8.0.4", "", { "dependencies": { "ansi-styles": "^6.2.1", "cross-spawn": "^7.0.6", "memorystream": "^0.3.1", "picomatch": "^4.0.2", "pidtree": "^0.6.0", "read-package-json-fast": "^4.0.0", "shell-quote": "^1.7.3", "which": "^5.0.0" }, "bin": { "run-p": "bin/run-p/index.js", "run-s": "bin/run-s/index.js", "npm-run-all": "bin/npm-run-all/index.js", "npm-run-all2": "bin/npm-run-all/index.js" } }, "sha512-wdbB5My48XKp2ZfJUlhnLVihzeuA1hgBnqB2J9ahV77wLS+/YAJAlN8I+X3DIFIPZ3m5L7nplmlbhNiFDmXRDA=="],
|
||||
|
||||
"npm-run-path": ["npm-run-path@6.0.0", "", { "dependencies": { "path-key": "^4.0.0", "unicorn-magic": "^0.3.0" } }, "sha512-9qny7Z9DsQU8Ou39ERsPU4OZQlSTP47ShQzuKZ6PRXpYLtIFgl/DEBYEXKlvcEa+9tHVcK8CF81Y2V72qaZhWA=="],
|
||||
|
||||
"nth-check": ["nth-check@2.1.1", "", { "dependencies": { "boolbase": "^1.0.0" } }, "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w=="],
|
||||
|
||||
"open": ["open@10.2.0", "", { "dependencies": { "default-browser": "^5.2.1", "define-lazy-prop": "^3.0.0", "is-inside-container": "^1.0.0", "wsl-utils": "^0.1.0" } }, "sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA=="],
|
||||
|
||||
"optionator": ["optionator@0.9.4", "", { "dependencies": { "deep-is": "^0.1.3", "fast-levenshtein": "^2.0.6", "levn": "^0.4.1", "prelude-ls": "^1.2.1", "type-check": "^0.4.0", "word-wrap": "^1.2.5" } }, "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g=="],
|
||||
|
||||
"oxlint": ["oxlint@1.1.0", "", { "optionalDependencies": { "@oxlint/darwin-arm64": "1.1.0", "@oxlint/darwin-x64": "1.1.0", "@oxlint/linux-arm64-gnu": "1.1.0", "@oxlint/linux-arm64-musl": "1.1.0", "@oxlint/linux-x64-gnu": "1.1.0", "@oxlint/linux-x64-musl": "1.1.0", "@oxlint/win32-arm64": "1.1.0", "@oxlint/win32-x64": "1.1.0" }, "bin": { "oxlint": "bin/oxlint", "oxc_language_server": "bin/oxc_language_server" } }, "sha512-OVNpaoaQCUHHhCv5sYMPJ7Ts5k7ziw0QteH1gBSwF3elf/8GAew2Uh/0S7HsU1iGtjhlFy80+A8nwIb3Tq6m1w=="],
|
||||
|
||||
"p-limit": ["p-limit@3.1.0", "", { "dependencies": { "yocto-queue": "^0.1.0" } }, "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ=="],
|
||||
|
||||
"p-locate": ["p-locate@5.0.0", "", { "dependencies": { "p-limit": "^3.0.2" } }, "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw=="],
|
||||
|
||||
"parent-module": ["parent-module@1.0.1", "", { "dependencies": { "callsites": "^3.0.0" } }, "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g=="],
|
||||
|
||||
"parse-ms": ["parse-ms@4.0.0", "", {}, "sha512-TXfryirbmq34y8QBwgqCVLi+8oA3oWx2eAnSn62ITyEhEYaWRlVZ2DvMM9eZbMs/RfxPu/PK/aBLyGj4IrqMHw=="],
|
||||
|
||||
"path-browserify": ["path-browserify@1.0.1", "", {}, "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g=="],
|
||||
|
||||
"path-exists": ["path-exists@4.0.0", "", {}, "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w=="],
|
||||
|
||||
"path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="],
|
||||
|
||||
"pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="],
|
||||
|
||||
"perfect-debounce": ["perfect-debounce@1.0.0", "", {}, "sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA=="],
|
||||
|
||||
"picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],
|
||||
|
||||
"picomatch": ["picomatch@4.0.2", "", {}, "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg=="],
|
||||
|
||||
"pidtree": ["pidtree@0.6.0", "", { "bin": { "pidtree": "bin/pidtree.js" } }, "sha512-eG2dWTVw5bzqGRztnHExczNxt5VGsE6OwTeCG3fdUf9KBsZzO3R5OIIIzWR+iZA0NtZ+RDVdaoE2dK1cn6jH4g=="],
|
||||
|
||||
"pinia": ["pinia@3.0.3", "", { "dependencies": { "@vue/devtools-api": "^7.7.2" }, "peerDependencies": { "typescript": ">=4.4.4", "vue": "^2.7.0 || ^3.5.11" }, "optionalPeers": ["typescript"] }, "sha512-ttXO/InUULUXkMHpTdp9Fj4hLpD/2AoJdmAbAeW2yu1iy1k+pkFekQXw5VpC0/5p51IOR/jDaDRfRWRnMMsGOA=="],
|
||||
|
||||
"postcss": ["postcss@8.5.6", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg=="],
|
||||
|
||||
"postcss-selector-parser": ["postcss-selector-parser@6.1.2", "", { "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" } }, "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg=="],
|
||||
|
||||
"prelude-ls": ["prelude-ls@1.2.1", "", {}, "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g=="],
|
||||
|
||||
"prettier": ["prettier@3.5.3", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw=="],
|
||||
|
||||
"prettier-linter-helpers": ["prettier-linter-helpers@1.0.0", "", { "dependencies": { "fast-diff": "^1.1.2" } }, "sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w=="],
|
||||
|
||||
"pretty-ms": ["pretty-ms@9.2.0", "", { "dependencies": { "parse-ms": "^4.0.0" } }, "sha512-4yf0QO/sllf/1zbZWYnvWw3NxCQwLXKzIj0G849LSufP15BXKM0rbD2Z3wVnkMfjdn/CB0Dpp444gYAACdsplg=="],
|
||||
|
||||
"proper-lockfile": ["proper-lockfile@4.1.2", "", { "dependencies": { "graceful-fs": "^4.2.4", "retry": "^0.12.0", "signal-exit": "^3.0.2" } }, "sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA=="],
|
||||
|
||||
"punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="],
|
||||
|
||||
"querystringify": ["querystringify@2.2.0", "", {}, "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ=="],
|
||||
|
||||
"queue-microtask": ["queue-microtask@1.2.3", "", {}, "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="],
|
||||
|
||||
"read-package-json-fast": ["read-package-json-fast@4.0.0", "", { "dependencies": { "json-parse-even-better-errors": "^4.0.0", "npm-normalize-package-bin": "^4.0.0" } }, "sha512-qpt8EwugBWDw2cgE2W+/3oxC+KTez2uSVR8JU9Q36TXPAGCaozfQUs59v4j4GFpWTaw0i6hAZSvOmu1J0uOEUg=="],
|
||||
|
||||
"requires-port": ["requires-port@1.0.0", "", {}, "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ=="],
|
||||
|
||||
"resolve-from": ["resolve-from@4.0.0", "", {}, "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g=="],
|
||||
|
||||
"retry": ["retry@0.12.0", "", {}, "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow=="],
|
||||
|
||||
"reusify": ["reusify@1.1.0", "", {}, "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw=="],
|
||||
|
||||
"rfdc": ["rfdc@1.4.1", "", {}, "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA=="],
|
||||
|
||||
"rolldown": ["rolldown@1.0.0-beta.27", "", { "dependencies": { "@oxc-project/runtime": "=0.77.0", "@oxc-project/types": "=0.77.0", "@rolldown/pluginutils": "1.0.0-beta.27", "ansis": "^4.0.0" }, "optionalDependencies": { "@rolldown/binding-android-arm64": "1.0.0-beta.27", "@rolldown/binding-darwin-arm64": "1.0.0-beta.27", "@rolldown/binding-darwin-x64": "1.0.0-beta.27", "@rolldown/binding-freebsd-x64": "1.0.0-beta.27", "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-beta.27", "@rolldown/binding-linux-arm64-gnu": "1.0.0-beta.27", "@rolldown/binding-linux-arm64-musl": "1.0.0-beta.27", "@rolldown/binding-linux-arm64-ohos": "1.0.0-beta.27", "@rolldown/binding-linux-x64-gnu": "1.0.0-beta.27", "@rolldown/binding-linux-x64-musl": "1.0.0-beta.27", "@rolldown/binding-wasm32-wasi": "1.0.0-beta.27", "@rolldown/binding-win32-arm64-msvc": "1.0.0-beta.27", "@rolldown/binding-win32-ia32-msvc": "1.0.0-beta.27", "@rolldown/binding-win32-x64-msvc": "1.0.0-beta.27" }, "bin": { "rolldown": "bin/cli.mjs" } }, "sha512-aYiJmzKoUHoaaEZLRegYVfZkXW7gzdgSbq+u5cXQ6iXc/y8tnQ3zGffQo44Pr1lTKeLluw3bDIDUCx/NAzqKeA=="],
|
||||
|
||||
"run-applescript": ["run-applescript@7.0.0", "", {}, "sha512-9by4Ij99JUr/MCFBUkDKLWK3G9HVXmabKz9U5MlIAIuvuzkiOicRYs8XJLxX+xahD+mLiiCYDqF9dKAgtzKP1A=="],
|
||||
|
||||
"run-parallel": ["run-parallel@1.2.0", "", { "dependencies": { "queue-microtask": "^1.2.2" } }, "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA=="],
|
||||
|
||||
"safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="],
|
||||
|
||||
"seemly": ["seemly@0.3.10", "", {}, "sha512-2+SMxtG1PcsL0uyhkumlOU6Qo9TAQ/WyH7tthnPIOQB05/12jz9naq6GZ6iZ6ApVsO3rr2gsnTf3++OV63kE1Q=="],
|
||||
|
||||
"semver": ["semver@7.7.2", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="],
|
||||
|
||||
"shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="],
|
||||
|
||||
"shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="],
|
||||
|
||||
"shell-quote": ["shell-quote@1.8.3", "", {}, "sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw=="],
|
||||
|
||||
"signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="],
|
||||
|
||||
"sirv": ["sirv@3.0.1", "", { "dependencies": { "@polka/url": "^1.0.0-next.24", "mrmime": "^2.0.0", "totalist": "^3.0.0" } }, "sha512-FoqMu0NCGBLCcAkS1qA+XJIQTR6/JHfQXl+uGteNCQ76T91DMUjPa9xfmeqMY3z80nLSg9yQmNjK0Px6RWsH/A=="],
|
||||
|
||||
"source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
|
||||
|
||||
"speakingurl": ["speakingurl@14.0.1", "", {}, "sha512-1POYv7uv2gXoyGFpBCmpDVSNV74IfsWlDW216UPjbWufNf+bSU6GdbDsxdcxtfwb4xlI3yxzOTKClUosxARYrQ=="],
|
||||
|
||||
"strip-final-newline": ["strip-final-newline@4.0.0", "", {}, "sha512-aulFJcD6YK8V1G7iRB5tigAP4TsHBZZrOV8pjV++zdUwmeV8uzbY7yn6h9MswN62adStNZFuCIx4haBnRuMDaw=="],
|
||||
|
||||
"strip-json-comments": ["strip-json-comments@3.1.1", "", {}, "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig=="],
|
||||
|
||||
"superjson": ["superjson@2.2.2", "", { "dependencies": { "copy-anything": "^3.0.2" } }, "sha512-5JRxVqC8I8NuOUjzBbvVJAKNM8qoVuH0O77h4WInc/qC2q5IreqKxYwgkga3PfA22OayK2ikceb/B26dztPl+Q=="],
|
||||
|
||||
"supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="],
|
||||
|
||||
"synckit": ["synckit@0.11.8", "", { "dependencies": { "@pkgr/core": "^0.2.4" } }, "sha512-+XZ+r1XGIJGeQk3VvXhT6xx/VpbHsRzsTkGgF6E5RX9TTXD0118l87puaEBZ566FhqblC6U0d4XnubznJDm30A=="],
|
||||
|
||||
"tailwindcss": ["tailwindcss@4.1.11", "", {}, "sha512-2E9TBm6MDD/xKYe+dvJZAmg3yxIEDNRc0jwlNyDg/4Fil2QcSLjFKGVff0lAf1jjeaArlG/M75Ey/EYr/OJtBA=="],
|
||||
|
||||
"tapable": ["tapable@2.2.2", "", {}, "sha512-Re10+NauLTMCudc7T5WLFLAwDhQ0JWdrMK+9B2M8zR5hRExKmsRDCBA7/aV/pNJFltmBFO5BAMlQFi/vq3nKOg=="],
|
||||
|
||||
"tar": ["tar@7.4.3", "", { "dependencies": { "@isaacs/fs-minipass": "^4.0.0", "chownr": "^3.0.0", "minipass": "^7.1.2", "minizlib": "^3.0.1", "mkdirp": "^3.0.1", "yallist": "^5.0.0" } }, "sha512-5S7Va8hKfV7W5U6g3aYxXmlPoZVAwUMy9AOKyF2fVuZa2UD3qZjg578OrLRt8PcNN1PleVaL/5/yYATNL0ICUw=="],
|
||||
|
||||
"tinyglobby": ["tinyglobby@0.2.14", "", { "dependencies": { "fdir": "^6.4.4", "picomatch": "^4.0.2" } }, "sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ=="],
|
||||
|
||||
"to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="],
|
||||
|
||||
"totalist": ["totalist@3.0.1", "", {}, "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ=="],
|
||||
|
||||
"treemate": ["treemate@0.3.11", "", {}, "sha512-M8RGFoKtZ8dF+iwJfAJTOH/SM4KluKOKRJpjCMhI8bG3qB74zrFoArKZ62ll0Fr3mqkMJiQOmWYkdYgDeITYQg=="],
|
||||
|
||||
"ts-api-utils": ["ts-api-utils@2.1.0", "", { "peerDependencies": { "typescript": ">=4.8.4" } }, "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ=="],
|
||||
|
||||
"tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
|
||||
|
||||
"tus-js-client": ["tus-js-client@4.3.1", "", { "dependencies": { "buffer-from": "^1.1.2", "combine-errors": "^3.0.3", "is-stream": "^2.0.0", "js-base64": "^3.7.2", "lodash.throttle": "^4.1.1", "proper-lockfile": "^4.1.2", "url-parse": "^1.5.7" } }, "sha512-ZLeYmjrkaU1fUsKbIi8JML52uAocjEZtBx4DKjRrqzrZa0O4MYwT6db+oqePlspV+FxXJAyFBc/L5gwUi2OFsg=="],
|
||||
|
||||
"type-check": ["type-check@0.4.0", "", { "dependencies": { "prelude-ls": "^1.2.1" } }, "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew=="],
|
||||
|
||||
"typescript": ["typescript@5.8.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ=="],
|
||||
|
||||
"typescript-eslint": ["typescript-eslint@8.37.0", "", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.37.0", "@typescript-eslint/parser": "8.37.0", "@typescript-eslint/typescript-estree": "8.37.0", "@typescript-eslint/utils": "8.37.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.9.0" } }, "sha512-TnbEjzkE9EmcO0Q2zM+GE8NQLItNAJpMmED1BdgoBMYNdqMhzlbqfdSwiRlAzEK2pA9UzVW0gzaaIzXWg2BjfA=="],
|
||||
|
||||
"undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="],
|
||||
|
||||
"unicorn-magic": ["unicorn-magic@0.3.0", "", {}, "sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA=="],
|
||||
|
||||
"universalify": ["universalify@2.0.1", "", {}, "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw=="],
|
||||
|
||||
"update-browserslist-db": ["update-browserslist-db@1.1.3", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw=="],
|
||||
|
||||
"uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="],
|
||||
|
||||
"url-parse": ["url-parse@1.5.10", "", { "dependencies": { "querystringify": "^2.1.1", "requires-port": "^1.0.0" } }, "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ=="],
|
||||
|
||||
"util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="],
|
||||
|
||||
"vdirs": ["vdirs@0.1.8", "", { "dependencies": { "evtd": "^0.2.2" }, "peerDependencies": { "vue": "^3.0.11" } }, "sha512-H9V1zGRLQZg9b+GdMk8MXDN2Lva0zx72MPahDKc30v+DtwKjfyOSXWRIX4t2mhDubM1H09gPhWeth/BJWPHGUw=="],
|
||||
|
||||
"vite": ["rolldown-vite@7.0.9", "", { "dependencies": { "fdir": "^6.4.6", "lightningcss": "^1.30.1", "picomatch": "^4.0.2", "postcss": "^8.5.6", "rolldown": "1.0.0-beta.27", "tinyglobby": "^0.2.14" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "esbuild": "^0.25.0", "jiti": ">=1.21.0", "less": "^4.0.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "esbuild", "jiti", "less", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-RxVP6CY9CNCEM9UecdytqeADxOGSjgkfSE/eI986sM7I3/F09lQ9UfQo3y6W10ICBppKsEHe71NbCX/tirYDFg=="],
|
||||
|
||||
"vite-hot-client": ["vite-hot-client@2.1.0", "", { "peerDependencies": { "vite": "^2.6.0 || ^3.0.0 || ^4.0.0 || ^5.0.0-0 || ^6.0.0-0 || ^7.0.0-0" } }, "sha512-7SpgZmU7R+dDnSmvXE1mfDtnHLHQSisdySVR7lO8ceAXvM0otZeuQQ6C8LrS5d/aYyP/QZ0hI0L+dIPrm4YlFQ=="],
|
||||
|
||||
"vite-plugin-inspect": ["vite-plugin-inspect@0.8.9", "", { "dependencies": { "@antfu/utils": "^0.7.10", "@rollup/pluginutils": "^5.1.3", "debug": "^4.3.7", "error-stack-parser-es": "^0.1.5", "fs-extra": "^11.2.0", "open": "^10.1.0", "perfect-debounce": "^1.0.0", "picocolors": "^1.1.1", "sirv": "^3.0.0" }, "peerDependencies": { "vite": "^3.1.0 || ^4.0.0 || ^5.0.0-0 || ^6.0.1" } }, "sha512-22/8qn+LYonzibb1VeFZmISdVao5kC22jmEKm24vfFE8siEn47EpVcCLYMv6iKOYMJfjSvSJfueOwcFCkUnV3A=="],
|
||||
|
||||
"vite-plugin-vue-devtools": ["vite-plugin-vue-devtools@7.7.7", "", { "dependencies": { "@vue/devtools-core": "^7.7.7", "@vue/devtools-kit": "^7.7.7", "@vue/devtools-shared": "^7.7.7", "execa": "^9.5.2", "sirv": "^3.0.1", "vite-plugin-inspect": "0.8.9", "vite-plugin-vue-inspector": "^5.3.1" }, "peerDependencies": { "vite": "^3.1.0 || ^4.0.0-0 || ^5.0.0-0 || ^6.0.0-0 || ^7.0.0-0" } }, "sha512-d0fIh3wRcgSlr4Vz7bAk4va1MkdqhQgj9ANE/rBhsAjOnRfTLs2ocjFMvSUOsv6SRRXU9G+VM7yMgqDb6yI4iQ=="],
|
||||
|
||||
"vite-plugin-vue-inspector": ["vite-plugin-vue-inspector@5.3.2", "", { "dependencies": { "@babel/core": "^7.23.0", "@babel/plugin-proposal-decorators": "^7.23.0", "@babel/plugin-syntax-import-attributes": "^7.22.5", "@babel/plugin-syntax-import-meta": "^7.10.4", "@babel/plugin-transform-typescript": "^7.22.15", "@vue/babel-plugin-jsx": "^1.1.5", "@vue/compiler-dom": "^3.3.4", "kolorist": "^1.8.0", "magic-string": "^0.30.4" }, "peerDependencies": { "vite": "^3.0.0-0 || ^4.0.0-0 || ^5.0.0-0 || ^6.0.0-0 || ^7.0.0-0" } }, "sha512-YvEKooQcSiBTAs0DoYLfefNja9bLgkFM7NI2b07bE2SruuvX0MEa9cMaxjKVMkeCp5Nz9FRIdcN1rOdFVBeL6Q=="],
|
||||
|
||||
"vooks": ["vooks@0.2.12", "", { "dependencies": { "evtd": "^0.2.2" }, "peerDependencies": { "vue": "^3.0.0" } }, "sha512-iox0I3RZzxtKlcgYaStQYKEzWWGAduMmq+jS7OrNdQo1FgGfPMubGL3uGHOU9n97NIvfFDBGnpSvkWyb/NSn/Q=="],
|
||||
|
||||
"vscode-uri": ["vscode-uri@3.1.0", "", {}, "sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ=="],
|
||||
|
||||
"vue": ["vue@3.5.17", "", { "dependencies": { "@vue/compiler-dom": "3.5.17", "@vue/compiler-sfc": "3.5.17", "@vue/runtime-dom": "3.5.17", "@vue/server-renderer": "3.5.17", "@vue/shared": "3.5.17" }, "peerDependencies": { "typescript": "*" }, "optionalPeers": ["typescript"] }, "sha512-LbHV3xPN9BeljML+Xctq4lbz2lVHCR6DtbpTf5XIO6gugpXUN49j2QQPcMj086r9+AkJ0FfUT8xjulKKBkkr9g=="],
|
||||
|
||||
"vue-chartjs": ["vue-chartjs@5.3.2", "", { "peerDependencies": { "chart.js": "^4.1.1", "vue": "^3.0.0-0 || ^2.7.0" } }, "sha512-NrkbRRoYshbXbWqJkTN6InoDVwVb90C0R7eAVgMWcB9dPikbruaOoTFjFYHE/+tNPdIe6qdLCDjfjPHQ0fw4jw=="],
|
||||
|
||||
"vue-eslint-parser": ["vue-eslint-parser@10.2.0", "", { "dependencies": { "debug": "^4.4.0", "eslint-scope": "^8.2.0", "eslint-visitor-keys": "^4.2.0", "espree": "^10.3.0", "esquery": "^1.6.0", "semver": "^7.6.3" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0" } }, "sha512-CydUvFOQKD928UzZhTp4pr2vWz1L+H99t7Pkln2QSPdvmURT0MoC4wUccfCnuEaihNsu9aYYyk+bep8rlfkUXw=="],
|
||||
|
||||
"vue-router": ["vue-router@4.5.1", "", { "dependencies": { "@vue/devtools-api": "^6.6.4" }, "peerDependencies": { "vue": "^3.2.0" } }, "sha512-ogAF3P97NPm8fJsE4by9dwSYtDwXIY1nFY9T6DyQnGHd1E2Da94w9JIolpe42LJGIl0DwOHBi8TcRPlPGwbTtw=="],
|
||||
|
||||
"vue-tsc": ["vue-tsc@2.2.12", "", { "dependencies": { "@volar/typescript": "2.4.15", "@vue/language-core": "2.2.12" }, "peerDependencies": { "typescript": ">=5.0.0" }, "bin": { "vue-tsc": "./bin/vue-tsc.js" } }, "sha512-P7OP77b2h/Pmk+lZdJ0YWs+5tJ6J2+uOQPo7tlBnY44QqQSPYvS0qVT4wqDJgwrZaLe47etJLLQRFia71GYITw=="],
|
||||
|
||||
"vueuc": ["vueuc@0.4.64", "", { "dependencies": { "@css-render/vue3-ssr": "^0.15.10", "@juggle/resize-observer": "^3.3.1", "css-render": "^0.15.10", "evtd": "^0.2.4", "seemly": "^0.3.6", "vdirs": "^0.1.4", "vooks": "^0.2.4" }, "peerDependencies": { "vue": "^3.0.11" } }, "sha512-wlJQj7fIwKK2pOEoOq4Aro8JdPOGpX8aWQhV8YkTW9OgWD2uj2O8ANzvSsIGjx7LTOc7QbS7sXdxHi6XvRnHPA=="],
|
||||
|
||||
"whatwg-fetch": ["whatwg-fetch@3.6.20", "", {}, "sha512-EqhiFU6daOA8kpjOWTL0olhVOF3i7OrFzSYiGsEMB8GcXS+RrzauAERX65xMeNWVqxA6HXH2m69Z9LaKKdisfg=="],
|
||||
|
||||
"which": ["which@5.0.0", "", { "dependencies": { "isexe": "^3.1.1" }, "bin": { "node-which": "bin/which.js" } }, "sha512-JEdGzHwwkrbWoGOlIHqQ5gtprKGOenpDHpxE9zVR1bWbOtYRyPPHMe9FaP6x61CmNaTThSkb0DAJte5jD+DmzQ=="],
|
||||
|
||||
"word-wrap": ["word-wrap@1.2.5", "", {}, "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA=="],
|
||||
|
||||
"wsl-utils": ["wsl-utils@0.1.0", "", { "dependencies": { "is-wsl": "^3.1.0" } }, "sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw=="],
|
||||
|
||||
"xml-name-validator": ["xml-name-validator@4.0.0", "", {}, "sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw=="],
|
||||
|
||||
"yallist": ["yallist@5.0.0", "", {}, "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw=="],
|
||||
|
||||
"yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="],
|
||||
|
||||
"yoctocolors": ["yoctocolors@2.1.1", "", {}, "sha512-GQHQqAopRhwU8Kt1DDM8NjibDXHC8eoh1erhGAJPEyveY9qqVeXvVikNKrDz69sHowPMorbPUrH/mx8c50eiBQ=="],
|
||||
|
||||
"@babel/core/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
|
||||
|
||||
"@babel/helper-compilation-targets/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
|
||||
|
||||
"@babel/helper-create-class-features-plugin/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
|
||||
|
||||
"@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="],
|
||||
|
||||
"@humanfs/node/@humanwhocodes/retry": ["@humanwhocodes/retry@0.3.1", "", {}, "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA=="],
|
||||
|
||||
"@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.4.4", "", { "dependencies": { "@emnapi/wasi-threads": "1.0.3", "tslib": "^2.4.0" }, "bundled": true }, "sha512-A9CnAbC6ARNMKcIcrQwq6HeHCjpcBZ5wSx4U01WXCqEKlrzB9F9315WDNHkrs2xbx7YjjSxbUYxuN6EQzpcY2g=="],
|
||||
|
||||
"@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.4.4", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-hHyapA4A3gPaDCNfiqyZUStTMqIkKRshqPIuDOXv1hcBnD4U3l8cP0T1HMCfGRxQ6V64TGCcoswChANyOAwbQg=="],
|
||||
|
||||
"@tailwindcss/oxide-wasm32-wasi/@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.0.3", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-8K5IFFsQqF9wQNJptGbS6FNKgUTsSRYnTqNCG1vPP8jFdjSv18n2mQfJpkt2Oibo9iBEzcDnDxNwKTzC7svlJw=="],
|
||||
|
||||
"@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@0.2.12", "", { "dependencies": { "@emnapi/core": "^1.4.3", "@emnapi/runtime": "^1.4.3", "@tybys/wasm-util": "^0.10.0" }, "bundled": true }, "sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ=="],
|
||||
|
||||
"@tailwindcss/oxide-wasm32-wasi/@tybys/wasm-util": ["@tybys/wasm-util@0.9.0", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-6+7nlbMVX/PVDCwaIQ8nTOPveOcFLSt8GcXdx8hD0bt39uWxYT88uXzqTd4fTvqta7oeUJqudepapKNt2DYJFw=="],
|
||||
|
||||
"@tailwindcss/oxide-wasm32-wasi/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
|
||||
|
||||
"@typescript-eslint/eslint-plugin/ignore": ["ignore@7.0.5", "", {}, "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg=="],
|
||||
|
||||
"@typescript-eslint/typescript-estree/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="],
|
||||
|
||||
"@vitejs/plugin-vue-jsx/@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-beta.9-commit.d91dfb5", "", {}, "sha512-8sExkWRK+zVybw3+2/kBkYBFeLnEUWz1fT7BLHplpzmtqkOfTbAQ9gkt4pzwGIIZmg4Qn5US5ACjUBenrhezwQ=="],
|
||||
|
||||
"@vue/devtools-core/nanoid": ["nanoid@5.1.5", "", { "bin": { "nanoid": "bin/nanoid.js" } }, "sha512-Ir/+ZpE9fDsNH0hQ3C68uyThDXzYcim2EqcZ8zn8Chtt1iylPT9xXJB0kPCnqzgcEGikO9RxSrh63MsmVCU7Fw=="],
|
||||
|
||||
"@vue/language-core/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="],
|
||||
|
||||
"chalk/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
|
||||
|
||||
"cross-spawn/which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="],
|
||||
|
||||
"css-render/csstype": ["csstype@3.0.11", "", {}, "sha512-sa6P2wJ+CAbgyy4KFssIb/JNMLxFvKF1pCYCSXS8ZMuqZnMsrxqI2E5sPyoTpxoPU/gVZMzr2zjOfg8GIZOMsw=="],
|
||||
|
||||
"execa/is-stream": ["is-stream@4.0.1", "", {}, "sha512-Dnz92NInDqYckGEUJv689RbRiTSEHCQ7wOVeALbkOz999YpqT46yMRIGtSNl2iCL1waAZSx40+h59NV/EwzV/A=="],
|
||||
|
||||
"fast-glob/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="],
|
||||
|
||||
"get-stream/is-stream": ["is-stream@4.0.1", "", {}, "sha512-Dnz92NInDqYckGEUJv689RbRiTSEHCQ7wOVeALbkOz999YpqT46yMRIGtSNl2iCL1waAZSx40+h59NV/EwzV/A=="],
|
||||
|
||||
"lru-cache/yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="],
|
||||
|
||||
"micromatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],
|
||||
|
||||
"node-fetch/is-stream": ["is-stream@1.1.0", "", {}, "sha512-uQPm8kcs47jx38atAcWTVxyltQYoPT68y9aWYdV6yWXSyW8mzSat0TL6CiWdZeCdF3KrAvpVtnHbTv4RN+rqdQ=="],
|
||||
|
||||
"npm-run-path/path-key": ["path-key@4.0.0", "", {}, "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ=="],
|
||||
|
||||
"proper-lockfile/signal-exit": ["signal-exit@3.0.7", "", {}, "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="],
|
||||
|
||||
"rolldown/@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-beta.27", "", {}, "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA=="],
|
||||
|
||||
"vue-router/@vue/devtools-api": ["@vue/devtools-api@6.6.4", "", {}, "sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g=="],
|
||||
|
||||
"@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime/@tybys/wasm-util": ["@tybys/wasm-util@0.10.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-VyyPYFlOMNylG45GoAe0xDoLwWuowvf92F9kySqzYh8vmYm7D2u4iUJKa1tOUpS70Ku13ASrOkS4ScXFsTaCNQ=="],
|
||||
|
||||
"@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="],
|
||||
|
||||
"@vue/language-core/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="],
|
||||
|
||||
"cross-spawn/which/isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="],
|
||||
}
|
||||
}
|
||||
1
DysonNetwork.Drive/Client/env.d.ts
vendored
1
DysonNetwork.Drive/Client/env.d.ts
vendored
@@ -1 +0,0 @@
|
||||
/// <reference types="vite/client" />
|
||||
@@ -1,31 +0,0 @@
|
||||
import { globalIgnores } from 'eslint/config'
|
||||
import { defineConfigWithVueTs, vueTsConfigs } from '@vue/eslint-config-typescript'
|
||||
import pluginVue from 'eslint-plugin-vue'
|
||||
import pluginOxlint from 'eslint-plugin-oxlint'
|
||||
import skipFormatting from '@vue/eslint-config-prettier/skip-formatting'
|
||||
|
||||
// To allow more languages other than `ts` in `.vue` files, uncomment the following lines:
|
||||
// import { configureVueProject } from '@vue/eslint-config-typescript'
|
||||
// configureVueProject({ scriptLangs: ['ts', 'tsx'] })
|
||||
// More info at https://github.com/vuejs/eslint-config-typescript/#advanced-setup
|
||||
|
||||
export default defineConfigWithVueTs(
|
||||
{
|
||||
name: 'app/files-to-lint',
|
||||
files: ['**/*.{ts,mts,tsx,vue}'],
|
||||
},
|
||||
|
||||
globalIgnores(['**/dist/**', '**/dist-ssr/**', '**/coverage/**']),
|
||||
|
||||
pluginVue.configs['flat/essential'],
|
||||
vueTsConfigs.recommended,
|
||||
...pluginOxlint.configs['flat/recommended'],
|
||||
{
|
||||
rules: {
|
||||
'vue/multi-word-component-names': 'off',
|
||||
'@typescript-eslint/no-explicit-any': 'off',
|
||||
'@typescript-eslint/ban-ts-comment': 'off',
|
||||
},
|
||||
},
|
||||
skipFormatting,
|
||||
)
|
||||
@@ -1,14 +0,0 @@
|
||||
<!doctype html>
|
||||
<html lang="">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" href="/favicon.png" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Solar Network Drive</title>
|
||||
<app-data />
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,55 +0,0 @@
|
||||
{
|
||||
"name": "@solar-network/drive",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "run-p type-check \"build-only {@}\" --",
|
||||
"preview": "vite preview",
|
||||
"build-only": "vite build",
|
||||
"type-check": "vue-tsc --build",
|
||||
"lint:oxlint": "oxlint . --fix -D correctness --ignore-path .gitignore",
|
||||
"lint:eslint": "eslint . --fix",
|
||||
"lint": "run-s lint:*",
|
||||
"format": "prettier --write src/"
|
||||
},
|
||||
"dependencies": {
|
||||
"@fingerprintjs/fingerprintjs": "^4.6.2",
|
||||
"@fontsource-variable/nunito": "^5.2.6",
|
||||
"@hcaptcha/vue3-hcaptcha": "^1.3.0",
|
||||
"@tailwindcss/vite": "^4.1.11",
|
||||
"@vueuse/core": "^13.5.0",
|
||||
"aspnet-prerendering": "^3.0.1",
|
||||
"cfturnstile-vue3": "^2.0.0",
|
||||
"chart.js": "^4.5.0",
|
||||
"pinia": "^3.0.3",
|
||||
"tailwindcss": "^4.1.11",
|
||||
"tus-js-client": "^4.3.1",
|
||||
"vue": "^3.5.17",
|
||||
"vue-chartjs": "^5.3.2",
|
||||
"vue-router": "^4.5.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tsconfig/node22": "^22.0.2",
|
||||
"@types/node": "^22.16.4",
|
||||
"@vicons/material": "^0.13.0",
|
||||
"@vitejs/plugin-vue": "^6.0.0",
|
||||
"@vitejs/plugin-vue-jsx": "^5.0.1",
|
||||
"@vue/eslint-config-prettier": "^10.2.0",
|
||||
"@vue/eslint-config-typescript": "^14.6.0",
|
||||
"@vue/tsconfig": "^0.7.0",
|
||||
"eslint": "^9.31.0",
|
||||
"eslint-plugin-oxlint": "~1.1.0",
|
||||
"eslint-plugin-vue": "~10.2.0",
|
||||
"jiti": "^2.4.2",
|
||||
"naive-ui": "^2.42.0",
|
||||
"npm-run-all2": "^8.0.4",
|
||||
"oxlint": "~1.1.0",
|
||||
"prettier": "3.5.3",
|
||||
"typescript": "~5.8.3",
|
||||
"vite": "npm:rolldown-vite@latest",
|
||||
"vite-plugin-vue-devtools": "^7.7.7",
|
||||
"vue-tsc": "^2.2.12"
|
||||
}
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
@import "tailwindcss";
|
||||
|
||||
@layer theme, base, components, utilities;
|
||||
|
||||
@layer base {
|
||||
body {
|
||||
font-family: 'Nunito Variable', sans-serif;
|
||||
}
|
||||
}
|
||||
@@ -1,50 +0,0 @@
|
||||
<template>
|
||||
<n-select
|
||||
v-model:value="selectedBundle"
|
||||
:options="options"
|
||||
placeholder="Select a bundle"
|
||||
@update:value="handleBundleChange"
|
||||
filterable
|
||||
remote
|
||||
:loading="loading"
|
||||
@search="handleSearch"
|
||||
clearable
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { NSelect } from 'naive-ui'
|
||||
import { ref, onMounted } from 'vue'
|
||||
|
||||
const emit = defineEmits(['update:bundle'])
|
||||
|
||||
const selectedBundle = ref<string | null>(null)
|
||||
const loading = ref(false)
|
||||
const options = ref<any[]>([])
|
||||
|
||||
async function fetchBundles(term: string | null = null) {
|
||||
loading.value = true
|
||||
try {
|
||||
const resp = await fetch(`/api/bundles/me?${term ? `term=${term}` : ''}`)
|
||||
const data = await resp.json()
|
||||
options.value = data.map((bundle: any) => ({
|
||||
label: bundle.name,
|
||||
value: bundle.id,
|
||||
}))
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch bundles:', error)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function handleSearch(query: string) {
|
||||
fetchBundles(query)
|
||||
}
|
||||
|
||||
function handleBundleChange(value: string) {
|
||||
emit('update:bundle', value)
|
||||
}
|
||||
|
||||
onMounted(() => fetchBundles())
|
||||
</script>
|
||||
@@ -1,199 +0,0 @@
|
||||
<template>
|
||||
<n-select
|
||||
:value="modelValue"
|
||||
@update:value="onUpdate"
|
||||
:options="pools ?? []"
|
||||
:render-label="renderPoolSelectLabel"
|
||||
:render-tag="renderSingleSelectTag"
|
||||
value-field="id"
|
||||
label-field="name"
|
||||
:placeholder="props.placeholder || 'Select a file pool to upload'"
|
||||
:size="props.size || 'large'"
|
||||
clearable
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
NSelect,
|
||||
NTag,
|
||||
NDivider,
|
||||
NTooltip,
|
||||
type SelectOption,
|
||||
type SelectRenderTag,
|
||||
} from 'naive-ui'
|
||||
import { h, onMounted, ref, watch } from 'vue'
|
||||
import type { SnFilePool } from '@/types/pool'
|
||||
import { formatBytes } from '@/views/format'
|
||||
|
||||
const props = defineProps<{
|
||||
modelValue: string | null
|
||||
placeholder?: string | undefined
|
||||
size?: 'tiny' | 'small' | 'medium' | 'large' | undefined
|
||||
}>()
|
||||
|
||||
const emit = defineEmits(['update:modelValue', 'update:pool'])
|
||||
|
||||
type SnFilePoolOption = SnFilePool & any
|
||||
|
||||
const pools = ref<SnFilePoolOption[] | undefined>()
|
||||
async function fetchPools() {
|
||||
const resp = await fetch('/api/pools')
|
||||
pools.value = await resp.json()
|
||||
}
|
||||
onMounted(() => fetchPools())
|
||||
|
||||
function onUpdate(value: string | null) {
|
||||
emit('update:modelValue', value)
|
||||
if (value === null) {
|
||||
emit('update:pool', null)
|
||||
return
|
||||
}
|
||||
if (pools.value) {
|
||||
const pool = pools.value.find((p) => p.id === value) ?? null
|
||||
emit('update:pool', pool)
|
||||
}
|
||||
}
|
||||
|
||||
watch(pools, (newPools) => {
|
||||
if (props.modelValue && newPools) {
|
||||
const pool = newPools.find((p) => p.id === props.modelValue) ?? null
|
||||
emit('update:pool', pool)
|
||||
}
|
||||
})
|
||||
|
||||
const renderSingleSelectTag: SelectRenderTag = ({ option }) => {
|
||||
return h(
|
||||
'div',
|
||||
{
|
||||
style: {
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
},
|
||||
},
|
||||
[option.name as string],
|
||||
)
|
||||
}
|
||||
|
||||
const perkPrivilegeList = ['Stellar', 'Nova', 'Supernova']
|
||||
|
||||
function renderPoolSelectLabel(option: SelectOption & SnFilePool) {
|
||||
const policy: any = option.policy_config
|
||||
return h(
|
||||
'div',
|
||||
{
|
||||
style: {
|
||||
padding: '8px 2px',
|
||||
},
|
||||
},
|
||||
[
|
||||
h('div', null, [option.name as string]),
|
||||
option.description &&
|
||||
h(
|
||||
'div',
|
||||
{
|
||||
style: {
|
||||
fontSize: '0.875rem',
|
||||
opacity: '0.75',
|
||||
},
|
||||
},
|
||||
option.description,
|
||||
),
|
||||
h(
|
||||
'div',
|
||||
{
|
||||
style: {
|
||||
display: 'flex',
|
||||
marginBottom: '4px',
|
||||
fontSize: '0.75rem',
|
||||
opacity: '0.75',
|
||||
},
|
||||
},
|
||||
[
|
||||
policy.max_file_size && h('span', `Max ${formatBytes(policy.max_file_size)}`),
|
||||
policy.accept_types &&
|
||||
h(
|
||||
NTooltip,
|
||||
{},
|
||||
{
|
||||
trigger: () => h('span', `Accept limited types`),
|
||||
default: () => h('span', policy.accept_types.join(', ')),
|
||||
},
|
||||
),
|
||||
policy.require_privilege &&
|
||||
h('span', `Require ${perkPrivilegeList[policy.require_privilege - 1]} Program`),
|
||||
h('span', `Cost x${option.billing_config.cost_multiplier.toFixed(1)}`),
|
||||
]
|
||||
.filter((el) => el)
|
||||
.flatMap((el, idx, arr) =>
|
||||
idx < arr.length - 1 ? [el, h(NDivider, { vertical: true })] : [el],
|
||||
),
|
||||
),
|
||||
h(
|
||||
'div',
|
||||
{
|
||||
style: {
|
||||
display: 'flex',
|
||||
gap: '0.25rem',
|
||||
marginTop: '2px',
|
||||
marginLeft: '-2px',
|
||||
marginRight: '-2px',
|
||||
},
|
||||
},
|
||||
[
|
||||
policy.public_usable &&
|
||||
h(
|
||||
NTag,
|
||||
{
|
||||
type: 'info',
|
||||
size: 'small',
|
||||
round: true,
|
||||
},
|
||||
{ default: () => 'Public Shared' },
|
||||
),
|
||||
policy.public_indexable &&
|
||||
h(
|
||||
NTag,
|
||||
{
|
||||
type: 'success',
|
||||
size: 'small',
|
||||
round: true,
|
||||
},
|
||||
{ default: () => 'Public Indexable' },
|
||||
),
|
||||
policy.allow_encryption &&
|
||||
h(
|
||||
NTag,
|
||||
{
|
||||
type: 'warning',
|
||||
size: 'small',
|
||||
round: true,
|
||||
},
|
||||
{ default: () => 'Allow Encryption' },
|
||||
),
|
||||
policy.allow_anonymous &&
|
||||
h(
|
||||
NTag,
|
||||
{
|
||||
type: 'info',
|
||||
size: 'small',
|
||||
round: true,
|
||||
},
|
||||
{ default: () => 'Allow Anonymous' },
|
||||
),
|
||||
policy.enable_recycle &&
|
||||
h(
|
||||
NTag,
|
||||
{
|
||||
type: 'info',
|
||||
size: 'small',
|
||||
round: true,
|
||||
},
|
||||
{ default: () => 'Recycle Enabled' },
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
)
|
||||
}
|
||||
</script>
|
||||
@@ -1,271 +0,0 @@
|
||||
<template>
|
||||
<div>
|
||||
<n-collapse-transition :show="showRecycleHint">
|
||||
<n-alert size="small" type="warning" title="Recycle Enabled" class="mb-3">
|
||||
You're uploading to a pool which enabled recycle. If the file you uploaded didn't referenced
|
||||
from the Solar Network. It will be marked and will be deleted some while later.
|
||||
</n-alert>
|
||||
</n-collapse-transition>
|
||||
|
||||
<n-collapse-transition :show="modeAdvanced">
|
||||
<n-card title="Advance Options" size="small" class="mb-3">
|
||||
<div class="flex flex-col gap-3">
|
||||
<div>
|
||||
<p class="pl-1 mb-0.5">File Password</p>
|
||||
<n-input
|
||||
v-model:value="filePass"
|
||||
:disabled="!currentFilePool?.allow_encryption"
|
||||
placeholder="Enter password to protect the file"
|
||||
show-password-toggle
|
||||
size="large"
|
||||
type="password"
|
||||
class="mb-2"
|
||||
/>
|
||||
<p class="pl-1 text-xs opacity-75 mt-[-4px]">
|
||||
Only available for Stellar Program and certian file pool.
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="pl-1 mb-0.5">File Expiration Date</p>
|
||||
<n-date-picker
|
||||
v-model:value="fileExpire"
|
||||
type="datetime"
|
||||
clearable
|
||||
:is-date-disabled="disablePreviousDate"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
v-if="currentFilePool?.policy_config?.enable_fast_upload || route.query.pool"
|
||||
class="flex items-center gap-2"
|
||||
>
|
||||
<p class="pl-1 mb-0.5">Fast Upload</p>
|
||||
<n-switch v-model:value="fastUpload" />
|
||||
</div>
|
||||
</div>
|
||||
</n-card>
|
||||
</n-collapse-transition>
|
||||
|
||||
<n-upload
|
||||
multiple
|
||||
directory-dnd
|
||||
with-credentials
|
||||
show-preview-button
|
||||
list-type="image"
|
||||
show-download-button
|
||||
:custom-request="customRequest"
|
||||
:custom-download="customDownload"
|
||||
:create-thumbnail-url="createThumbnailUrl"
|
||||
@preview="customPreview"
|
||||
>
|
||||
<n-upload-dragger>
|
||||
<div style="margin-bottom: 12px">
|
||||
<n-icon size="48" :depth="3">
|
||||
<cloud-upload-round />
|
||||
</n-icon>
|
||||
</div>
|
||||
<n-text style="font-size: 16px"> Click or drag a file to this area to upload </n-text>
|
||||
<n-p depth="3" style="margin: 8px 0 0 0">
|
||||
Strictly prohibit from uploading sensitive information. For example, your bank card PIN or
|
||||
your credit card expiry date.
|
||||
</n-p>
|
||||
</n-upload-dragger>
|
||||
</n-upload>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
NUpload,
|
||||
NUploadDragger,
|
||||
NIcon,
|
||||
NText,
|
||||
NP,
|
||||
NInput,
|
||||
NCollapseTransition,
|
||||
NDatePicker,
|
||||
NAlert,
|
||||
NCard,
|
||||
NSwitch,
|
||||
type UploadCustomRequestOptions,
|
||||
type UploadSettledFileInfo,
|
||||
type UploadFileInfo,
|
||||
useMessage,
|
||||
} from 'naive-ui'
|
||||
import { computed, ref } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { CloudUploadRound } from '@vicons/material'
|
||||
import type { SnFilePool } from '@/types/pool'
|
||||
|
||||
import * as tus from 'tus-js-client'
|
||||
|
||||
const props = defineProps<{
|
||||
filePool: string | null
|
||||
modeAdvanced: boolean
|
||||
pools: SnFilePool[]
|
||||
bundleId?: string
|
||||
}>()
|
||||
|
||||
const route = useRoute()
|
||||
|
||||
const filePass = ref<string>('')
|
||||
const fileExpire = ref<number | null>(null)
|
||||
const fastUpload = ref<boolean>(false)
|
||||
|
||||
const effectiveFilePool = computed(() => (route.query.pool as string) || props.filePool)
|
||||
|
||||
const currentFilePool = computed(() => {
|
||||
if (!effectiveFilePool.value) return null
|
||||
return props.pools?.find((pool) => pool.id === effectiveFilePool.value) ?? null
|
||||
})
|
||||
const showRecycleHint = computed(() => {
|
||||
if (!effectiveFilePool.value) return true
|
||||
return currentFilePool.value?.policy_config?.enable_recycle || false
|
||||
})
|
||||
|
||||
const messageDisplay = useMessage()
|
||||
|
||||
async function customRequest({
|
||||
file,
|
||||
headers,
|
||||
withCredentials,
|
||||
onFinish,
|
||||
onError,
|
||||
onProgress,
|
||||
}: UploadCustomRequestOptions) {
|
||||
if (fastUpload.value) {
|
||||
const hash = await crypto.subtle.digest('SHA-256', await file.file!.arrayBuffer())
|
||||
const hashString = Array.from(new Uint8Array(hash))
|
||||
.map((b) => b.toString(16).padStart(2, '0'))
|
||||
.join('')
|
||||
|
||||
const resp = await fetch('/api/files/fast', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
name: file.name,
|
||||
size: file.file?.size,
|
||||
hash: hashString,
|
||||
mime_type: file.file?.type,
|
||||
pool_id: effectiveFilePool.value,
|
||||
}),
|
||||
})
|
||||
|
||||
if (!resp.ok) {
|
||||
messageDisplay.error(`Failed to get presigned URL: ${await resp.text()}`)
|
||||
onError()
|
||||
return
|
||||
}
|
||||
|
||||
const respData = await resp.json()
|
||||
const url = respData.fast_upload_link
|
||||
|
||||
try {
|
||||
const xhr = new XMLHttpRequest()
|
||||
xhr.open('PUT', url, true)
|
||||
xhr.upload.onprogress = (event) => {
|
||||
if (event.lengthComputable) {
|
||||
onProgress({ percent: (event.loaded / event.total) * 100 })
|
||||
}
|
||||
}
|
||||
xhr.onload = () => {
|
||||
if (xhr.status >= 200 && xhr.status < 300) {
|
||||
onFinish()
|
||||
} else {
|
||||
messageDisplay.error(`Upload failed: ${xhr.responseText}`)
|
||||
onError()
|
||||
}
|
||||
}
|
||||
xhr.onerror = () => {
|
||||
messageDisplay.error('Upload failed due to a network error.')
|
||||
onError()
|
||||
}
|
||||
xhr.send(file.file)
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
messageDisplay.error(`Upload failed: ${e}`)
|
||||
onError()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
const requestHeaders: Record<string, string> = {}
|
||||
if (effectiveFilePool.value) requestHeaders['X-FilePool'] = effectiveFilePool.value
|
||||
if (filePass.value) requestHeaders['X-FilePass'] = filePass.value
|
||||
if (fileExpire.value) requestHeaders['X-FileExpire'] = fileExpire.value.toString()
|
||||
if (props.bundleId) requestHeaders['X-FileBundle'] = props.bundleId
|
||||
const upload = new tus.Upload(file.file as any, {
|
||||
endpoint: '/api/tus',
|
||||
retryDelays: [0, 3000, 5000, 10000, 20000],
|
||||
removeFingerprintOnSuccess: false,
|
||||
uploadDataDuringCreation: false,
|
||||
metadata: {
|
||||
filename: file.name,
|
||||
'content-type': file.type ?? 'application/octet-stream',
|
||||
},
|
||||
headers: {
|
||||
'X-DirectUpload': 'true',
|
||||
...requestHeaders,
|
||||
...headers,
|
||||
},
|
||||
onShouldRetry: () => false,
|
||||
onError: function (error) {
|
||||
if (error instanceof tus.DetailedError) {
|
||||
const failedBody = error.originalResponse?.getBody()
|
||||
if (failedBody != null)
|
||||
messageDisplay.error(`Upload failed: ${failedBody}`, {
|
||||
duration: 10000,
|
||||
closable: true,
|
||||
})
|
||||
}
|
||||
console.error('[DRIVE] Upload failed:', error)
|
||||
onError()
|
||||
},
|
||||
onProgress: function (bytesUploaded, bytesTotal) {
|
||||
onProgress({ percent: (bytesUploaded / bytesTotal) * 100 })
|
||||
},
|
||||
onSuccess: function (payload) {
|
||||
const rawInfo = payload.lastResponse.getHeader('x-fileinfo')
|
||||
const jsonInfo = JSON.parse(rawInfo as string)
|
||||
console.log('[DRIVE] Upload successful: ', jsonInfo)
|
||||
file.url = `/api/files/${jsonInfo.id}`
|
||||
file.type = jsonInfo.mime_type
|
||||
onFinish()
|
||||
},
|
||||
onBeforeRequest: function (req) {
|
||||
const xhr = req.getUnderlyingObject()
|
||||
xhr.withCredentials = withCredentials
|
||||
},
|
||||
})
|
||||
upload.findPreviousUploads().then(function (previousUploads) {
|
||||
if (previousUploads.length) {
|
||||
upload.resumeFromPreviousUpload(previousUploads[0])
|
||||
}
|
||||
upload.start()
|
||||
})
|
||||
}
|
||||
|
||||
function createThumbnailUrl(
|
||||
_file: File | null,
|
||||
fileInfo: UploadSettledFileInfo,
|
||||
): string | undefined {
|
||||
if (!fileInfo) return undefined
|
||||
return fileInfo.url ?? undefined
|
||||
}
|
||||
|
||||
function customDownload(file: UploadFileInfo) {
|
||||
const { url } = file
|
||||
if (!url) return
|
||||
window.open(url.replace('/api', ''), '_blank')
|
||||
}
|
||||
|
||||
function customPreview(file: UploadFileInfo, detail: { event: MouseEvent }) {
|
||||
detail.event.preventDefault()
|
||||
const { url } = file
|
||||
if (!url) return
|
||||
window.open(url.replace('/api', ''), '_blank')
|
||||
}
|
||||
|
||||
function disablePreviousDate(ts: number) {
|
||||
return ts <= Date.now()
|
||||
}
|
||||
</script>
|
||||
@@ -1,75 +0,0 @@
|
||||
<template>
|
||||
<n-form :model="formValue" :rules="rules" ref="formRef">
|
||||
<n-form-item label="Slug" path="slug">
|
||||
<n-input v-model:value="formValue.slug" placeholder="Input Slug" />
|
||||
</n-form-item>
|
||||
<n-form-item label="Name" path="name">
|
||||
<n-input v-model:value="formValue.name" placeholder="Input Name" />
|
||||
</n-form-item>
|
||||
<n-form-item label="Description" path="description">
|
||||
<n-input
|
||||
v-model:value="formValue.description"
|
||||
placeholder="Input Description"
|
||||
type="textarea"
|
||||
/>
|
||||
</n-form-item>
|
||||
<n-form-item label="Passcode" path="passcode">
|
||||
<n-input
|
||||
v-model:value="formValue.passcode"
|
||||
placeholder="Input Passcode"
|
||||
type="password"
|
||||
/>
|
||||
</n-form-item>
|
||||
<n-form-item label="Expired At" path="expiredAt">
|
||||
<n-date-picker v-model:value="formValue.expiredAt" type="datetime" />
|
||||
</n-form-item>
|
||||
</n-form>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import {
|
||||
NForm,
|
||||
NFormItem,
|
||||
NInput,
|
||||
NDatePicker,
|
||||
type FormInst,
|
||||
type FormRules,
|
||||
} from 'naive-ui'
|
||||
import { ref } from 'vue'
|
||||
|
||||
const formRef = ref<FormInst | null>(null)
|
||||
|
||||
const props = defineProps<{ value: any }>()
|
||||
const formValue = ref(props.value)
|
||||
|
||||
const rules: FormRules = {
|
||||
slug: [
|
||||
{
|
||||
max: 1024,
|
||||
message: 'Slug can be at most 1024 characters long',
|
||||
},
|
||||
],
|
||||
name: [
|
||||
{
|
||||
max: 1024,
|
||||
message: 'Name can be at most 1024 characters long',
|
||||
},
|
||||
],
|
||||
description: [
|
||||
{
|
||||
max: 8192,
|
||||
message: 'Description can be at most 8192 characters long',
|
||||
},
|
||||
],
|
||||
passcode: [
|
||||
{
|
||||
max: 256,
|
||||
message: 'Passcode can be at most 256 characters long',
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
formRef,
|
||||
})
|
||||
</script>
|
||||
@@ -1,7 +0,0 @@
|
||||
export {}
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
DyPrefetch?: any
|
||||
}
|
||||
}
|
||||
@@ -1,62 +0,0 @@
|
||||
<template>
|
||||
<n-layout has-sider class="h-full">
|
||||
<n-layout-sider bordered collapse-mode="width" :collapsed-width="64" :width="240" show-trigger>
|
||||
<n-menu
|
||||
:collapsed-width="64"
|
||||
:collapsed-icon-size="22"
|
||||
:options="menuOptions"
|
||||
:value="route.name as string"
|
||||
@update:value="updateMenuSelect"
|
||||
/>
|
||||
</n-layout-sider>
|
||||
<n-layout>
|
||||
<router-view />
|
||||
</n-layout>
|
||||
</n-layout>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
DataUsageRound,
|
||||
AllInboxFilled,
|
||||
PermDataSettingRound,
|
||||
ShoppingBagRound,
|
||||
} from '@vicons/material'
|
||||
import { NIcon, NLayout, NLayoutSider, NMenu, type MenuOption } from 'naive-ui'
|
||||
import { h, type Component } from 'vue'
|
||||
import { RouterView, useRoute, useRouter } from 'vue-router'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
|
||||
function renderIcon(icon: Component) {
|
||||
return () => h(NIcon, null, { default: () => h(icon) })
|
||||
}
|
||||
|
||||
const menuOptions: MenuOption[] = [
|
||||
{
|
||||
label: 'Usage',
|
||||
key: 'dashboardUsage',
|
||||
icon: renderIcon(DataUsageRound),
|
||||
},
|
||||
{
|
||||
label: 'Files',
|
||||
key: 'dashboardFiles',
|
||||
icon: renderIcon(AllInboxFilled),
|
||||
},
|
||||
{
|
||||
label: 'Bundles',
|
||||
key: 'dashboardBundles',
|
||||
icon: renderIcon(ShoppingBagRound),
|
||||
},
|
||||
{
|
||||
label: 'Quota',
|
||||
key: 'dashboardQuota',
|
||||
icon: renderIcon(PermDataSettingRound),
|
||||
},
|
||||
]
|
||||
|
||||
function updateMenuSelect(key: string) {
|
||||
router.push({ name: key })
|
||||
}
|
||||
</script>
|
||||
@@ -1,115 +0,0 @@
|
||||
<template>
|
||||
<n-layout>
|
||||
<n-layout-header class="border-b-1 flex justify-between items-center">
|
||||
<router-link to="/" class="text-lg font-bold">Solar Network Drive</router-link>
|
||||
<div v-if="!hideUserMenu">
|
||||
<n-dropdown
|
||||
v-if="!userStore.isAuthenticated"
|
||||
:options="guestOptions"
|
||||
@select="handleGuestMenuSelect"
|
||||
>
|
||||
<n-button>Account</n-button>
|
||||
</n-dropdown>
|
||||
<n-dropdown v-else :options="userOptions" @select="handleUserMenuSelect" type="primary">
|
||||
<n-button>{{ userStore.user.nick }}</n-button>
|
||||
</n-dropdown>
|
||||
</div>
|
||||
</n-layout-header>
|
||||
<n-layout-content embedded>
|
||||
<router-view />
|
||||
</n-layout-content>
|
||||
</n-layout>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, h } from 'vue'
|
||||
import { NLayout, NLayoutHeader, NLayoutContent, NButton, NDropdown, NIcon } from 'naive-ui'
|
||||
import {
|
||||
LogInOutlined,
|
||||
PersonAddAlt1Outlined,
|
||||
PersonOutlineRound,
|
||||
DataUsageRound,
|
||||
} from '@vicons/material'
|
||||
import { useUserStore } from '@/stores/user'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { useServicesStore } from '@/stores/services'
|
||||
|
||||
const userStore = useUserStore()
|
||||
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
|
||||
const hideUserMenu = computed(() => {
|
||||
return ['captcha', 'spells', 'login', 'create-account'].includes(route.name as string)
|
||||
})
|
||||
|
||||
const guestOptions = [
|
||||
{
|
||||
label: 'Login',
|
||||
key: 'login',
|
||||
icon: () =>
|
||||
h(NIcon, null, {
|
||||
default: () => h(LogInOutlined),
|
||||
}),
|
||||
},
|
||||
{
|
||||
label: 'Create Account',
|
||||
key: 'create-account',
|
||||
icon: () =>
|
||||
h(NIcon, null, {
|
||||
default: () => h(PersonAddAlt1Outlined),
|
||||
}),
|
||||
},
|
||||
]
|
||||
|
||||
const userOptions = computed(() => [
|
||||
{
|
||||
label: 'Dashboard',
|
||||
key: 'dashboardUsage',
|
||||
icon: () =>
|
||||
h(NIcon, null, {
|
||||
default: () => h(DataUsageRound),
|
||||
}),
|
||||
},
|
||||
{
|
||||
label: 'Profile',
|
||||
key: 'profile',
|
||||
icon: () =>
|
||||
h(NIcon, null, {
|
||||
default: () => h(PersonOutlineRound),
|
||||
}),
|
||||
},
|
||||
])
|
||||
|
||||
const servicesStore = useServicesStore()
|
||||
|
||||
function handleGuestMenuSelect(key: string) {
|
||||
if (key === 'login') {
|
||||
window.open(servicesStore.getSerivceUrl('DysonNetwork.Pass', 'login')!, '_blank')
|
||||
} else if (key === 'create-account') {
|
||||
window.open(servicesStore.getSerivceUrl('DysonNetwork.Pass', 'create-account')!, '_blank')
|
||||
}
|
||||
}
|
||||
|
||||
function handleUserMenuSelect(key: string) {
|
||||
if (key === 'profile') {
|
||||
window.open(servicesStore.getSerivceUrl('DysonNetwork.Pass', 'accounts/me')!, '_blank')
|
||||
} else {
|
||||
router.push({ name: key })
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.n-layout-header {
|
||||
padding: 8px 24px;
|
||||
border-color: var(--n-border-color);
|
||||
height: 57px; /* Fixed height */
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.n-layout-content {
|
||||
height: calc(100vh - 57px); /* Adjust based on header height */
|
||||
}
|
||||
</style>
|
||||
@@ -1,16 +0,0 @@
|
||||
import '@fontsource-variable/nunito';
|
||||
|
||||
import './assets/main.css'
|
||||
|
||||
import { createApp } from 'vue'
|
||||
import { createPinia } from 'pinia'
|
||||
|
||||
import Root from './root.vue'
|
||||
import router from './router'
|
||||
|
||||
const app = createApp(Root)
|
||||
|
||||
app.use(createPinia())
|
||||
app.use(router)
|
||||
|
||||
app.mount('#app')
|
||||
@@ -1,55 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import LayoutDefault from './layouts/default.vue'
|
||||
|
||||
import { RouterView } from 'vue-router'
|
||||
import {
|
||||
NGlobalStyle,
|
||||
NConfigProvider,
|
||||
NMessageProvider,
|
||||
NDialogProvider,
|
||||
NLoadingBarProvider,
|
||||
lightTheme,
|
||||
darkTheme,
|
||||
} from 'naive-ui'
|
||||
import { usePreferredDark } from '@vueuse/core'
|
||||
import { useUserStore } from './stores/user'
|
||||
import { onMounted } from 'vue'
|
||||
import { useServicesStore } from './stores/services'
|
||||
|
||||
const themeOverrides = {
|
||||
common: {
|
||||
fontFamily: 'Nunito Variable, v-sans, ui-system, -apple-system, sans-serif',
|
||||
primaryColor: '#7D80BAFF',
|
||||
primaryColorHover: '#9294C5FF',
|
||||
primaryColorPressed: '#575B9DFF',
|
||||
primaryColorSuppl: '#6B6FC1FF',
|
||||
},
|
||||
}
|
||||
|
||||
const isDark = usePreferredDark()
|
||||
|
||||
const userStore = useUserStore()
|
||||
const servicesStore = useServicesStore()
|
||||
|
||||
onMounted(() => {
|
||||
userStore.initialize()
|
||||
|
||||
userStore.fetchUser()
|
||||
servicesStore.fetchServices()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<n-config-provider :theme-overrides="themeOverrides" :theme="isDark ? darkTheme : lightTheme">
|
||||
<n-global-style />
|
||||
<n-loading-bar-provider>
|
||||
<n-dialog-provider>
|
||||
<n-message-provider placement="bottom">
|
||||
<layout-default>
|
||||
<router-view />
|
||||
</layout-default>
|
||||
</n-message-provider>
|
||||
</n-dialog-provider>
|
||||
</n-loading-bar-provider>
|
||||
</n-config-provider>
|
||||
</template>
|
||||
@@ -1,86 +0,0 @@
|
||||
import { createRouter, createWebHistory } from 'vue-router'
|
||||
import { useUserStore } from '@/stores/user'
|
||||
import { useServicesStore } from '@/stores/services'
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHistory(import.meta.env.BASE_URL),
|
||||
routes: [
|
||||
{
|
||||
path: '/',
|
||||
name: 'index',
|
||||
component: () => import('../views/index.vue'),
|
||||
},
|
||||
{
|
||||
path: '/files/:fileId',
|
||||
name: 'files',
|
||||
component: () => import('../views/files.vue'),
|
||||
},
|
||||
{
|
||||
path: '/bundles/:bundleId',
|
||||
name: 'bundleDetails',
|
||||
component: () => import('../views/bundles.vue'),
|
||||
},
|
||||
{
|
||||
path: '/dashboard',
|
||||
name: 'dashboard',
|
||||
component: () => import('../layouts/dashboard.vue'),
|
||||
meta: { requiresAuth: true },
|
||||
children: [
|
||||
{
|
||||
path: 'usage',
|
||||
name: 'dashboardUsage',
|
||||
component: () => import('../views/dashboard/usage.vue'),
|
||||
meta: { requiresAuth: true },
|
||||
},
|
||||
{
|
||||
path: 'files',
|
||||
name: 'dashboardFiles',
|
||||
component: () => import('../views/dashboard/files.vue'),
|
||||
meta: { requiresAuth: true },
|
||||
},
|
||||
{
|
||||
path: 'bundles',
|
||||
name: 'dashboardBundles',
|
||||
component: () => import('../views/dashboard/bundles.vue'),
|
||||
meta: { requiresAuth: true },
|
||||
},
|
||||
{
|
||||
path: 'quotas',
|
||||
name: 'dashboardQuota',
|
||||
component: () => import('../views/dashboard/quotas.vue'),
|
||||
meta: { requiresAuth: true },
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: '/:notFound(.*)',
|
||||
name: 'errorNotFound',
|
||||
component: () => import('../views/not-found.vue'),
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
router.beforeEach(async (to, from, next) => {
|
||||
const userStore = useUserStore()
|
||||
const servicesStore = useServicesStore()
|
||||
|
||||
// Initialize user state if not already initialized
|
||||
if (!userStore.user) {
|
||||
await userStore.fetchUser()
|
||||
}
|
||||
|
||||
if (to.matched.some((record) => record.meta.requiresAuth) && !userStore.isAuthenticated) {
|
||||
window.open(
|
||||
servicesStore.getSerivceUrl(
|
||||
'DysonNetwork.Pass',
|
||||
'login?redirect=' + encodeURIComponent(window.location.href),
|
||||
)!,
|
||||
'_blank',
|
||||
)
|
||||
next('/')
|
||||
} else {
|
||||
next()
|
||||
}
|
||||
})
|
||||
|
||||
export default router
|
||||
@@ -1,27 +0,0 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref } from 'vue'
|
||||
|
||||
export const useServicesStore = defineStore('services', () => {
|
||||
const services = ref<Record<string, string>>({})
|
||||
|
||||
async function fetchServices() {
|
||||
try {
|
||||
const response = await fetch('/cgi/.well-known/services')
|
||||
if (!response.ok) {
|
||||
throw new Error('Network response was not ok')
|
||||
}
|
||||
const data = await response.json()
|
||||
services.value = data
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch services:', error)
|
||||
services.value = {}
|
||||
}
|
||||
}
|
||||
|
||||
function getSerivceUrl(serviceName: string, ...parts: string[]): string | null {
|
||||
const baseUrl = services.value[serviceName] || null
|
||||
return baseUrl ? `${baseUrl}/${parts.join('/')}` : null
|
||||
}
|
||||
|
||||
return { services, fetchServices, getSerivceUrl }
|
||||
})
|
||||
@@ -1,65 +0,0 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref, computed } from 'vue'
|
||||
|
||||
export const useUserStore = defineStore('user', () => {
|
||||
// State
|
||||
const user = ref<any>(null)
|
||||
const isLoading = ref(false)
|
||||
const error = ref<string | null>(null)
|
||||
|
||||
// Getters
|
||||
const isAuthenticated = computed(() => !!user.value)
|
||||
|
||||
// Actions
|
||||
async function fetchUser(reload = true) {
|
||||
if (!reload && user.value) return
|
||||
isLoading.value = true
|
||||
error.value = null
|
||||
try {
|
||||
const response = await fetch('/cgi/id/accounts/me', {
|
||||
credentials: 'include',
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
// If the token is invalid, clear it and the user state
|
||||
throw new Error('Failed to fetch user information.')
|
||||
}
|
||||
|
||||
user.value = await response.json()
|
||||
} catch (e: any) {
|
||||
error.value = e.message
|
||||
user.value = null // Clear user data on error
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function initialize() {
|
||||
const allowedOrigin = import.meta.env.DEV ? window.location.origin : 'https://id.solian.app'
|
||||
window.addEventListener('message', (event) => {
|
||||
// IMPORTANT: Always check the origin of the message for security!
|
||||
// This prevents malicious scripts from sending fake login status updates.
|
||||
// Ensure event.origin exactly matches your identity service's origin.
|
||||
if (event.origin !== allowedOrigin) {
|
||||
console.warn(`[SYNC] Message received from unexpected origin: ${event.origin}. Ignoring.`)
|
||||
return // Ignore messages from unknown origins
|
||||
}
|
||||
|
||||
// Check if the message is the type we're expecting
|
||||
if (event.data && event.data.type === 'DY:LOGIN_STATUS_CHANGE') {
|
||||
const { loggedIn } = event.data
|
||||
console.log(`[SYNC] Received login status change: ${loggedIn}`)
|
||||
fetchUser() // Re-fetch user data on login status change
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
user,
|
||||
isLoading,
|
||||
error,
|
||||
isAuthenticated,
|
||||
fetchUser,
|
||||
initialize,
|
||||
}
|
||||
})
|
||||
@@ -1,37 +0,0 @@
|
||||
export interface SnFilePool {
|
||||
id: string
|
||||
name: string
|
||||
description: string
|
||||
storage_config: StorageConfig
|
||||
billing_config: BillingConfig
|
||||
policy_config: any
|
||||
public_indexable: boolean
|
||||
public_usable: boolean
|
||||
no_optimization: boolean
|
||||
no_metadata: boolean
|
||||
allow_encryption: boolean
|
||||
allow_anonymous: boolean
|
||||
require_privilege: number
|
||||
account_id: null
|
||||
resource_identifier: string
|
||||
created_at: Date
|
||||
updated_at: Date
|
||||
deleted_at: null
|
||||
}
|
||||
|
||||
export interface BillingConfig {
|
||||
cost_multiplier: number
|
||||
}
|
||||
|
||||
export interface StorageConfig {
|
||||
region: string
|
||||
bucket: string
|
||||
endpoint: string
|
||||
secret_id: string
|
||||
secret_key: string
|
||||
enable_signed: boolean
|
||||
enable_ssl: boolean
|
||||
image_proxy: null
|
||||
access_proxy: null
|
||||
expiration: null
|
||||
}
|
||||
@@ -1,255 +0,0 @@
|
||||
<template>
|
||||
<section class="min-h-full relative flex items-center justify-center">
|
||||
<n-spin v-if="!bundleInfo && !error" />
|
||||
<n-result
|
||||
status="404"
|
||||
title="No bundle was found"
|
||||
:description="error"
|
||||
v-else-if="error === '404'"
|
||||
/>
|
||||
|
||||
<n-card class="max-w-md my-4 mx-8" v-else-if="error === '403'">
|
||||
<n-result
|
||||
status="403"
|
||||
title="Access Denied"
|
||||
description="This bundle is protected by a passcode"
|
||||
class="mt-5 mb-2"
|
||||
>
|
||||
<template #footer>
|
||||
<n-alert v-if="passcodeError" type="error" class="mb-3">
|
||||
{{ passcodeError }}
|
||||
</n-alert>
|
||||
<n-input
|
||||
v-model:value="passcode"
|
||||
type="password"
|
||||
show-password-on="mousedown"
|
||||
placeholder="Passcode"
|
||||
@keyup.enter="fetchBundleInfo"
|
||||
class="mb-3"
|
||||
/>
|
||||
<n-button type="primary" block @click="fetchBundleInfo">Access Bundle</n-button>
|
||||
</template>
|
||||
</n-result>
|
||||
</n-card>
|
||||
|
||||
<n-card class="max-w-4xl my-4 mx-8" v-else>
|
||||
<n-grid cols="1 m:2" x-gap="16" y-gap="16" responsive="screen">
|
||||
<n-gi>
|
||||
<n-card title="Content" size="small">
|
||||
<n-list
|
||||
size="small"
|
||||
v-if="bundleInfo.files && bundleInfo.files.length > 0"
|
||||
style="padding: 0"
|
||||
>
|
||||
<n-list-item v-for="file in bundleInfo.files" :key="file.id">
|
||||
<n-thing :title="file.name" :description="formatBytes(file.size)">
|
||||
<template #header-extra>
|
||||
<n-button text type="primary" @click="goToFileDetails(file.id)">View</n-button>
|
||||
</template>
|
||||
</n-thing>
|
||||
</n-list-item>
|
||||
</n-list>
|
||||
<n-empty v-else description="No files in this bundle" />
|
||||
<template #footer>
|
||||
<n-collapse-transition :show="!!downloadProgress">
|
||||
<n-progress
|
||||
type="line"
|
||||
:percentage="downloadProgress"
|
||||
indicator-placement="inside"
|
||||
:status="downloadStatus"
|
||||
processing
|
||||
class="mb-4"
|
||||
/>
|
||||
</n-collapse-transition>
|
||||
<n-button
|
||||
type="primary"
|
||||
block
|
||||
:disabled="!bundleInfo.files || bundleInfo.files.length === 0 || downloading"
|
||||
@click="downloadAllFiles"
|
||||
>
|
||||
Download All
|
||||
</n-button>
|
||||
</template>
|
||||
</n-card>
|
||||
</n-gi>
|
||||
|
||||
<n-gi>
|
||||
<n-card size="small">
|
||||
<h3 class="text-lg">{{ bundleInfo.name }}</h3>
|
||||
<p class="mb-3" v-if="bundleInfo.description">{{ bundleInfo.description }}</p>
|
||||
<div class="flex gap-2">
|
||||
<span class="flex-grow-1 flex items-center gap-2">
|
||||
<n-icon>
|
||||
<calendar-today-round />
|
||||
</n-icon>
|
||||
Expires At
|
||||
</span>
|
||||
<span>{{
|
||||
bundleInfo.expiredAt ? new Date(bundleInfo.expiredAt).toLocaleString() : 'Never'
|
||||
}}</span>
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<span class="flex-grow-1 flex items-center gap-2">
|
||||
<n-icon>
|
||||
<lock-round />
|
||||
</n-icon>
|
||||
Passcode Protected
|
||||
</span>
|
||||
<span>{{ bundleInfo.passcode ? 'Yes' : 'No' }}</span>
|
||||
</div>
|
||||
</n-card>
|
||||
<n-input
|
||||
v-model:value="filePass"
|
||||
type="password"
|
||||
size="large"
|
||||
placeholder="File password file decrypt"
|
||||
class="mt-3"
|
||||
/>
|
||||
</n-gi>
|
||||
</n-grid>
|
||||
</n-card>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
NCard,
|
||||
NResult,
|
||||
NSpin,
|
||||
NIcon,
|
||||
NGrid,
|
||||
NGi,
|
||||
NList,
|
||||
NListItem,
|
||||
NThing,
|
||||
NButton,
|
||||
NEmpty,
|
||||
NInput,
|
||||
NAlert,
|
||||
NProgress,
|
||||
NCollapseTransition,
|
||||
useMessage,
|
||||
} from 'naive-ui'
|
||||
import { CalendarTodayRound, LockRound } from '@vicons/material'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { onMounted, ref, watch } from 'vue'
|
||||
|
||||
import { formatBytes } from './format' // Assuming format.ts is in the same directory
|
||||
import { downloadAndDecryptFile } from './secure'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
|
||||
const error = ref<string | null>(null)
|
||||
const bundleId = route.params.bundleId
|
||||
const passcode = ref<string>('')
|
||||
const passcodeError = ref<string | null>(null)
|
||||
|
||||
const filePass = ref<string>('')
|
||||
|
||||
const downloading = ref(false)
|
||||
const downloadProgress = ref<number | undefined>()
|
||||
const downloadStatus = ref<'success' | 'error' | 'info'>('info')
|
||||
|
||||
watch(
|
||||
route,
|
||||
(value) => {
|
||||
if (value.query.passcode) passcode.value = value.query.passcode.toString()
|
||||
},
|
||||
{ immediate: true, deep: true },
|
||||
)
|
||||
|
||||
const bundleInfo = ref<any>(null)
|
||||
async function fetchBundleInfo() {
|
||||
try {
|
||||
let url = '/api/bundles/' + bundleId
|
||||
if (passcode.value) {
|
||||
url += `?passcode=${passcode.value}`
|
||||
}
|
||||
const resp = await fetch(url)
|
||||
if (resp.status === 403) {
|
||||
error.value = '403'
|
||||
bundleInfo.value = null
|
||||
if (passcode.value) {
|
||||
passcodeError.value = 'Incorrect passcode.'
|
||||
}
|
||||
return
|
||||
}
|
||||
if (!resp.ok) {
|
||||
throw new Error('Failed to fetch bundle info: ' + resp.statusText)
|
||||
}
|
||||
bundleInfo.value = await resp.json()
|
||||
error.value = null
|
||||
passcodeError.value = null
|
||||
} catch (err) {
|
||||
error.value = (err as Error).message
|
||||
}
|
||||
}
|
||||
onMounted(() => fetchBundleInfo())
|
||||
|
||||
function goToFileDetails(fileId: string) {
|
||||
router.push({ path: `/files/${fileId}`, query: { passcode: passcode.value } })
|
||||
}
|
||||
|
||||
const messageDisplay = useMessage()
|
||||
|
||||
async function downloadAllFiles() {
|
||||
if (!bundleInfo.value || !bundleInfo.value.files || bundleInfo.value.files.length === 0) {
|
||||
return
|
||||
}
|
||||
|
||||
downloading.value = true
|
||||
downloadProgress.value = 0
|
||||
downloadStatus.value = 'info'
|
||||
|
||||
const totalFiles = bundleInfo.value.files.length
|
||||
let completedDownloads = 0
|
||||
|
||||
for (const file of bundleInfo.value.files) {
|
||||
let url = `/api/files/${file.id}`
|
||||
if (passcode.value) {
|
||||
url += `?passcode=${passcode.value}`
|
||||
}
|
||||
|
||||
if (file.is_encrypted) {
|
||||
downloadAndDecryptFile(file, filePass.value, file.name, () => {})
|
||||
.catch((err) => {
|
||||
messageDisplay.error('Download failed: ' + err.message, {
|
||||
closable: true,
|
||||
duration: 10000,
|
||||
})
|
||||
})
|
||||
.finally(() => {
|
||||
completedDownloads++
|
||||
downloadProgress.value = (completedDownloads / totalFiles) * 100
|
||||
})
|
||||
} else {
|
||||
try {
|
||||
const res = await fetch(url)
|
||||
if (!res.ok) {
|
||||
throw new Error(`Failed to download ${file.name}: ${res.statusText}`)
|
||||
}
|
||||
const blob = await res.blob()
|
||||
const blobUrl = window.URL.createObjectURL(blob)
|
||||
const a = document.createElement('a')
|
||||
a.href = blobUrl
|
||||
a.download = file.name || 'download' // fallback name
|
||||
document.body.appendChild(a)
|
||||
a.click()
|
||||
a.remove()
|
||||
window.URL.revokeObjectURL(blobUrl)
|
||||
|
||||
if (completedDownloads === totalFiles) {
|
||||
downloadStatus.value = 'success'
|
||||
}
|
||||
} catch (err) {
|
||||
messageDisplay.error(`Download failed for ${file.name}: ${err}`)
|
||||
downloadStatus.value = 'error'
|
||||
} finally {
|
||||
completedDownloads++
|
||||
downloadProgress.value = (completedDownloads / totalFiles) * 100
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -1,180 +0,0 @@
|
||||
<template>
|
||||
<section class="h-full px-5 py-4">
|
||||
<n-data-table
|
||||
remote
|
||||
:row-key="(row) => row.id"
|
||||
:columns="tableColumns"
|
||||
:data="bundles"
|
||||
:loading="loading"
|
||||
:pagination="tablePagination"
|
||||
@page-change="handlePageChange"
|
||||
/>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import {
|
||||
NDataTable,
|
||||
type DataTableColumns,
|
||||
type PaginationProps,
|
||||
useMessage,
|
||||
useLoadingBar,
|
||||
NButton,
|
||||
NIcon,
|
||||
NSpace,
|
||||
useDialog,
|
||||
} from 'naive-ui'
|
||||
import { h, onMounted, ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { DeleteRound } from '@vicons/material'
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
const bundles = ref<any[]>([])
|
||||
|
||||
const tableColumns: DataTableColumns<any> = [
|
||||
{
|
||||
title: 'Name',
|
||||
key: 'name',
|
||||
render(row: any) {
|
||||
return h(
|
||||
NButton,
|
||||
{
|
||||
text: true,
|
||||
onClick: () => {
|
||||
router.push(`/bundles/${row.id}`)
|
||||
},
|
||||
},
|
||||
{
|
||||
default: () => row.name,
|
||||
},
|
||||
)
|
||||
},
|
||||
maxWidth: 80,
|
||||
ellipsis: true,
|
||||
},
|
||||
{
|
||||
title: 'Description',
|
||||
key: 'description',
|
||||
maxWidth: 180,
|
||||
ellipsis: true,
|
||||
},
|
||||
{
|
||||
title: 'Expired At',
|
||||
key: 'expired_at',
|
||||
render(row: any) {
|
||||
if (!row.expired_at) return 'Never'
|
||||
return new Date(row.expired_at).toLocaleString()
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'Created At',
|
||||
key: 'created_at',
|
||||
render(row: any) {
|
||||
return new Date(row.created_at).toLocaleString()
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'Updated At',
|
||||
key: 'updated_at',
|
||||
render(row: any) {
|
||||
return new Date(row.updated_at).toLocaleString()
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'Action',
|
||||
key: 'action',
|
||||
render(row: any) {
|
||||
return h(NSpace, {}, [
|
||||
h(
|
||||
NButton,
|
||||
{
|
||||
circle: true,
|
||||
text: true,
|
||||
type: 'error',
|
||||
onClick: () => {
|
||||
askDeleteBundle(row)
|
||||
},
|
||||
},
|
||||
{
|
||||
icon: () => h(NIcon, {}, { default: () => h(DeleteRound) }),
|
||||
},
|
||||
),
|
||||
])
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
const tablePagination = ref<PaginationProps>({
|
||||
page: 1,
|
||||
itemCount: 0,
|
||||
pageSize: 10,
|
||||
showSizePicker: true,
|
||||
pageSizes: [10, 20, 30, 40, 50],
|
||||
})
|
||||
|
||||
async function fetchBundles() {
|
||||
if (loading.value) return
|
||||
try {
|
||||
loading.value = true
|
||||
const pag = tablePagination.value
|
||||
const response = await fetch(
|
||||
`/api/bundles/me?take=${pag.pageSize}&offset=${(pag.page! - 1) * pag.pageSize!}`,
|
||||
)
|
||||
if (!response.ok) {
|
||||
throw new Error('Network response was not ok')
|
||||
}
|
||||
const data = await response.json()
|
||||
bundles.value = data
|
||||
tablePagination.value.itemCount = parseInt(response.headers.get('x-total') ?? '0')
|
||||
} catch (error) {
|
||||
messageDialog.error('Failed to fetch bundles: ' + (error as Error).message)
|
||||
console.error('Failed to fetch bundles:', error)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
onMounted(() => fetchBundles())
|
||||
|
||||
function handlePageChange(page: number) {
|
||||
tablePagination.value.page = page
|
||||
fetchBundles()
|
||||
}
|
||||
|
||||
const loading = ref(false)
|
||||
|
||||
const messageDialog = useMessage()
|
||||
const loadingBar = useLoadingBar()
|
||||
const dialog = useDialog()
|
||||
|
||||
function askDeleteBundle(bundle: any) {
|
||||
dialog.warning({
|
||||
title: 'Confirm',
|
||||
content: `Are you sure you want to delete the bundle ${bundle.name}?`,
|
||||
positiveText: 'Sure',
|
||||
negativeText: 'Not Sure',
|
||||
onPositiveClick: () => {
|
||||
deleteBundle(bundle)
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
async function deleteBundle(bundle: any) {
|
||||
try {
|
||||
loadingBar.start()
|
||||
const response = await fetch(`/api/bundles/${bundle.id}`, {
|
||||
method: 'DELETE',
|
||||
})
|
||||
if (!response.ok) {
|
||||
throw new Error('Network response was not ok')
|
||||
}
|
||||
tablePagination.value.page = 1
|
||||
await fetchBundles()
|
||||
loadingBar.finish()
|
||||
messageDialog.success('Bundle deleted successfully')
|
||||
} catch (error) {
|
||||
loadingBar.error()
|
||||
messageDialog.error('Failed to delete bundle: ' + (error as Error).message)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -1,304 +0,0 @@
|
||||
<template>
|
||||
<section class="h-full px-5 py-4">
|
||||
<div class="flex items-center gap-4 mb-3">
|
||||
<file-pool-select
|
||||
v-model="filePool"
|
||||
placeholder="Filter by file pool"
|
||||
size="medium"
|
||||
class="max-w-[480px]"
|
||||
@update:pool="fetchFiles"
|
||||
/>
|
||||
<div class="flex items-center gap-2.5">
|
||||
<n-switch size="large" v-model:value="showRecycled">
|
||||
<template #checked>Recycled</template>
|
||||
<template #unchecked>Unrecycled</template>
|
||||
</n-switch>
|
||||
<n-button
|
||||
@click="askDeleteRecycledFiles"
|
||||
v-if="showRecycled"
|
||||
type="error"
|
||||
circle
|
||||
size="small"
|
||||
>
|
||||
<n-icon>
|
||||
<delete-sweep-round />
|
||||
</n-icon>
|
||||
</n-button>
|
||||
</div>
|
||||
</div>
|
||||
<n-data-table
|
||||
remote
|
||||
:row-key="(row) => row.id"
|
||||
:columns="tableColumns"
|
||||
:data="files"
|
||||
:loading="loading"
|
||||
:pagination="tablePagination"
|
||||
@page-change="handlePageChange"
|
||||
/>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import {
|
||||
NDataTable,
|
||||
NIcon,
|
||||
NImage,
|
||||
NButton,
|
||||
NSpace,
|
||||
type DataTableColumns,
|
||||
type PaginationProps,
|
||||
useDialog,
|
||||
useMessage,
|
||||
useLoadingBar,
|
||||
NSwitch,
|
||||
NTooltip,
|
||||
} from 'naive-ui'
|
||||
import {
|
||||
AudioFileRound,
|
||||
InsertDriveFileRound,
|
||||
VideoFileRound,
|
||||
FileDownloadOutlined,
|
||||
DeleteRound,
|
||||
DeleteSweepRound,
|
||||
} from '@vicons/material'
|
||||
import { h, onMounted, ref, watch } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { formatBytes } from '../format'
|
||||
import FilePoolSelect from '@/components/FilePoolSelect.vue'
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
const files = ref<any[]>([])
|
||||
|
||||
const filePool = ref<string | null>(null)
|
||||
const showRecycled = ref(false)
|
||||
|
||||
const tableColumns: DataTableColumns<any> = [
|
||||
{
|
||||
title: 'Preview',
|
||||
key: 'preview',
|
||||
render(row: any) {
|
||||
switch (row.mime_type.split('/')[0]) {
|
||||
case 'image':
|
||||
return h(NImage, {
|
||||
src: '/api/files/' + row.id,
|
||||
width: 32,
|
||||
height: 32,
|
||||
objectFit: 'contain',
|
||||
style: { aspectRatio: 1 },
|
||||
})
|
||||
case 'video':
|
||||
return h(NIcon, { size: 32 }, { default: () => h(VideoFileRound) })
|
||||
case 'audio':
|
||||
return h(NIcon, { size: 32 }, { default: () => h(AudioFileRound) })
|
||||
default:
|
||||
return h(NIcon, { size: 32 }, { default: () => h(InsertDriveFileRound) })
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'Name',
|
||||
key: 'name',
|
||||
maxWidth: 180,
|
||||
ellipsis: true,
|
||||
render(row: any) {
|
||||
return h(
|
||||
NButton,
|
||||
{
|
||||
text: true,
|
||||
onClick: () => {
|
||||
router.push(`/files/${row.id}`)
|
||||
},
|
||||
},
|
||||
{
|
||||
default: () => row.name,
|
||||
},
|
||||
)
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'Size',
|
||||
key: 'size',
|
||||
render(row: any) {
|
||||
return formatBytes(row.size)
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'Pool',
|
||||
key: 'pool',
|
||||
render(row: any) {
|
||||
if (!row.pool) return 'Unstored'
|
||||
return h(
|
||||
NTooltip,
|
||||
{},
|
||||
{
|
||||
default: () => h('span', row.pool.id),
|
||||
trigger: () => h('span', row.pool.name),
|
||||
},
|
||||
)
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'Expired At',
|
||||
key: 'expired_at',
|
||||
render(row: any) {
|
||||
if (!row.expired_at) return 'Never'
|
||||
return new Date(row.expired_at).toLocaleString()
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'Uploaded At',
|
||||
key: 'created_at',
|
||||
render(row: any) {
|
||||
return new Date(row.created_at).toLocaleString()
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'Action',
|
||||
key: 'action',
|
||||
render(row: any) {
|
||||
return h(NSpace, {}, [
|
||||
h(
|
||||
NButton,
|
||||
{
|
||||
circle: true,
|
||||
text: true,
|
||||
onClick: () => {
|
||||
window.open(`/api/files/${row.id}`, '_blank')
|
||||
},
|
||||
},
|
||||
{
|
||||
icon: () => h(NIcon, {}, { default: () => h(FileDownloadOutlined) }),
|
||||
},
|
||||
),
|
||||
h(
|
||||
NButton,
|
||||
{
|
||||
circle: true,
|
||||
text: true,
|
||||
type: 'error',
|
||||
onClick: () => {
|
||||
askDeleteFile(row)
|
||||
},
|
||||
},
|
||||
{
|
||||
icon: () => h(NIcon, {}, { default: () => h(DeleteRound) }),
|
||||
},
|
||||
),
|
||||
])
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
const tablePagination = ref<PaginationProps>({
|
||||
page: 1,
|
||||
itemCount: 0,
|
||||
pageSize: 10,
|
||||
showSizePicker: true,
|
||||
pageSizes: [10, 20, 30, 40, 50],
|
||||
})
|
||||
|
||||
async function fetchFiles() {
|
||||
if (loading.value) return
|
||||
try {
|
||||
loading.value = true
|
||||
const pag = tablePagination.value
|
||||
const response = await fetch(
|
||||
`/api/files/me?take=${pag.pageSize}&offset=${(pag.page! - 1) * pag.pageSize!}&recycled=${showRecycled.value}${filePool.value ? '&pool=' + filePool.value : ''}`,
|
||||
)
|
||||
if (!response.ok) {
|
||||
throw new Error('Network response was not ok')
|
||||
}
|
||||
const data = await response.json()
|
||||
files.value = data
|
||||
tablePagination.value.itemCount = parseInt(response.headers.get('x-total') ?? '0')
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch files:', error)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
onMounted(() => fetchFiles())
|
||||
|
||||
watch(showRecycled, () => {
|
||||
tablePagination.value.itemCount = 0
|
||||
tablePagination.value.page = 1
|
||||
fetchFiles()
|
||||
})
|
||||
|
||||
function handlePageChange(page: number) {
|
||||
tablePagination.value.page = page
|
||||
fetchFiles()
|
||||
}
|
||||
|
||||
const loading = ref(false)
|
||||
|
||||
const dialog = useDialog()
|
||||
const messageDialog = useMessage()
|
||||
const loadingBar = useLoadingBar()
|
||||
|
||||
function askDeleteFile(file: any) {
|
||||
dialog.warning({
|
||||
title: 'Confirm',
|
||||
content: `Are you sure you want delete ${file.name}? This will delete the stored file data immediately, there is no return.`,
|
||||
positiveText: 'Sure',
|
||||
negativeText: 'Not Sure',
|
||||
draggable: true,
|
||||
onPositiveClick: () => {
|
||||
deleteFile(file)
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
async function deleteFile(file: any) {
|
||||
try {
|
||||
loadingBar.start()
|
||||
const response = await fetch(`/api/files/${file.id}`, {
|
||||
method: 'DELETE',
|
||||
})
|
||||
if (!response.ok) {
|
||||
throw new Error('Network response was not ok')
|
||||
}
|
||||
tablePagination.value.page = 1
|
||||
await fetchFiles()
|
||||
loadingBar.finish()
|
||||
messageDialog.success('File deleted successfully')
|
||||
} catch (error) {
|
||||
loadingBar.error()
|
||||
messageDialog.error('Failed to delete file: ' + (error as Error).message)
|
||||
}
|
||||
}
|
||||
|
||||
function askDeleteRecycledFiles() {
|
||||
dialog.warning({
|
||||
title: 'Confirm',
|
||||
content: `Are you sure you want to delete all ${tablePagination.value.itemCount} marked recycled file(s) by system?`,
|
||||
positiveText: 'Sure',
|
||||
negativeText: 'Not Sure',
|
||||
draggable: true,
|
||||
onPositiveClick: () => {
|
||||
deleteRecycledFiles()
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
async function deleteRecycledFiles() {
|
||||
try {
|
||||
loadingBar.start()
|
||||
const response = await fetch('/api/files/me/recycle', {
|
||||
method: 'DELETE',
|
||||
})
|
||||
if (!response.ok) {
|
||||
throw new Error('Network response was not ok')
|
||||
}
|
||||
const resp = await response.json()
|
||||
tablePagination.value.page = 1
|
||||
await fetchFiles()
|
||||
loadingBar.finish()
|
||||
messageDialog.success(`Recycled files deleted successfully, deleted count: ${resp.count}`)
|
||||
} catch (error) {
|
||||
loadingBar.error()
|
||||
messageDialog.error('Failed to delete recycled files: ' + (error as Error).message)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -1,101 +0,0 @@
|
||||
<template>
|
||||
<section class="h-full px-5 py-4">
|
||||
<n-data-table
|
||||
remote
|
||||
:row-key="(row) => row.id"
|
||||
:columns="tableColumns"
|
||||
:data="quotas"
|
||||
:loading="loading"
|
||||
:pagination="tablePagination"
|
||||
@page-change="handlePageChange"
|
||||
/>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { NDataTable, type DataTableColumns, type PaginationProps, useMessage } from 'naive-ui'
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { formatBytes } from '../format'
|
||||
|
||||
const quotas = ref<any[]>([])
|
||||
|
||||
const tableColumns: DataTableColumns<any> = [
|
||||
{
|
||||
title: 'Name',
|
||||
key: 'name',
|
||||
},
|
||||
{
|
||||
title: 'Description',
|
||||
key: 'description',
|
||||
},
|
||||
{
|
||||
title: 'Quota',
|
||||
key: 'quota',
|
||||
render(row: any) {
|
||||
return formatBytes(row.quota * 1024 * 1024)
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'Expired At',
|
||||
key: 'expired_at',
|
||||
render(row: any) {
|
||||
if (!row.expired_at) return 'Never'
|
||||
return new Date(row.expired_at).toLocaleString()
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'Created At',
|
||||
key: 'created_at',
|
||||
render(row: any) {
|
||||
return new Date(row.created_at).toLocaleString()
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'Updated At',
|
||||
key: 'updated_at',
|
||||
render(row: any) {
|
||||
return new Date(row.updated_at).toLocaleString()
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
const tablePagination = ref<PaginationProps>({
|
||||
page: 1,
|
||||
itemCount: 0,
|
||||
pageSize: 10,
|
||||
showSizePicker: true,
|
||||
pageSizes: [10, 20, 30, 40, 50],
|
||||
})
|
||||
|
||||
async function fetchQuotas() {
|
||||
if (loading.value) return
|
||||
try {
|
||||
loading.value = true
|
||||
const pag = tablePagination.value
|
||||
const response = await fetch(
|
||||
`/api/billing/quota/records?take=${pag.pageSize}&offset=${(pag.page! - 1) * pag.pageSize!}`,
|
||||
)
|
||||
if (!response.ok) {
|
||||
throw new Error('Network response was not ok')
|
||||
}
|
||||
const data = await response.json()
|
||||
quotas.value = data
|
||||
tablePagination.value.itemCount = parseInt(response.headers.get('x-total') ?? '0')
|
||||
} catch (error) {
|
||||
messageDialog.error('Failed to fetch quotas: ' + (error as Error).message)
|
||||
console.error('Failed to fetch quotas:', error)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
onMounted(() => fetchQuotas())
|
||||
|
||||
function handlePageChange(page: number) {
|
||||
tablePagination.value.page = page
|
||||
fetchQuotas()
|
||||
}
|
||||
|
||||
const loading = ref(false)
|
||||
|
||||
const messageDialog = useMessage()
|
||||
</script>
|
||||
@@ -1,164 +0,0 @@
|
||||
<template>
|
||||
<section class="h-full container-fluid mx-auto py-4 px-5">
|
||||
<div class="h-full flex justify-center items-center" v-if="!usage">
|
||||
<n-spin />
|
||||
</div>
|
||||
<n-grid cols="1 s:2 l:4" responsive="screen" :x-gap="16" :y-gap="16" v-else>
|
||||
<n-gi span="4">
|
||||
<n-alert title="Billing Tips" size="small" type="info" closable>
|
||||
<p>
|
||||
The minimal billable unit is MiB, if your file is not enough 1 MiB it will be counted as
|
||||
1 MiB.
|
||||
</p>
|
||||
<p>The <b>1 MiB = 1024 KiB = 1,048,576 B</b></p>
|
||||
</n-alert>
|
||||
</n-gi>
|
||||
<n-gi>
|
||||
<n-card class="h-stats">
|
||||
<n-statistic label="All Uploads" tabular-nums>
|
||||
<n-number-animation
|
||||
:from="0"
|
||||
:to="toGigabytes(usage.total_usage_bytes)"
|
||||
:precision="3"
|
||||
/>
|
||||
<template #suffix>GiB</template>
|
||||
</n-statistic>
|
||||
</n-card>
|
||||
</n-gi>
|
||||
<n-gi>
|
||||
<n-card class="h-stats">
|
||||
<n-statistic label="All Files" tabular-nums>
|
||||
<n-number-animation :from="0" :to="usage.total_file_count" />
|
||||
</n-statistic>
|
||||
</n-card>
|
||||
</n-gi>
|
||||
<n-gi>
|
||||
<n-card class="h-stats">
|
||||
<n-statistic label="Quota" tabular-nums>
|
||||
<n-number-animation :from="0" :to="usage.total_quota" />
|
||||
<template #suffix>MiB</template>
|
||||
</n-statistic>
|
||||
</n-card>
|
||||
</n-gi>
|
||||
<n-gi>
|
||||
<n-card class="h-stats">
|
||||
<div class="flex gap-2 justify-between items-end">
|
||||
<n-statistic label="Used Quota" tabular-nums>
|
||||
<n-number-animation :from="0" :to="quotaUsagePercentage" :precision="2" />
|
||||
<template #suffix>%</template>
|
||||
</n-statistic>
|
||||
<n-progress
|
||||
type="circle"
|
||||
:percentage="quotaUsagePercentage"
|
||||
:show-indicator="false"
|
||||
:stroke-width="16"
|
||||
style="width: 40px"
|
||||
/>
|
||||
</div>
|
||||
</n-card>
|
||||
</n-gi>
|
||||
<n-gi span="2">
|
||||
<n-card class="aspect-video" title="Pool Usage">
|
||||
<pie
|
||||
:data="poolChartData"
|
||||
:options="{
|
||||
maintainAspectRatio: false,
|
||||
responsive: true,
|
||||
plugins: { legend: { position: isDesktop ? 'right' : 'bottom' } },
|
||||
}"
|
||||
/>
|
||||
</n-card>
|
||||
</n-gi>
|
||||
<n-gi span="2">
|
||||
<n-card class="aspect-video h-full" title="Verbose Quota">
|
||||
<pie
|
||||
:data="quotaChartData"
|
||||
:options="{
|
||||
maintainAspectRatio: false,
|
||||
responsive: true,
|
||||
plugins: { legend: { position: isDesktop ? 'right' : 'bottom' } },
|
||||
}"
|
||||
/>
|
||||
</n-card>
|
||||
</n-gi>
|
||||
</n-grid>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { NSpin, NCard, NStatistic, NGrid, NGi, NNumberAnimation, NAlert, NProgress } from 'naive-ui'
|
||||
import { Chart as ChartJS, Title, Tooltip, Legend, ArcElement } from 'chart.js'
|
||||
import { Pie } from 'vue-chartjs'
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import { breakpointsTailwind, useBreakpoints } from '@vueuse/core'
|
||||
|
||||
ChartJS.register(Title, Tooltip, Legend, ArcElement)
|
||||
|
||||
const breakpoints = useBreakpoints(breakpointsTailwind)
|
||||
const isDesktop = breakpoints.greaterOrEqual('md')
|
||||
|
||||
const poolChartData = computed(() => ({
|
||||
labels: usage.value.pool_usages.map((pool: any) => pool.pool_name),
|
||||
datasets: [
|
||||
{
|
||||
label: 'Pool Usage',
|
||||
backgroundColor: '#7D80BAFF',
|
||||
data: usage.value.pool_usages.map((pool: any) => pool.usage_bytes),
|
||||
},
|
||||
],
|
||||
}))
|
||||
|
||||
const usage = ref<any>()
|
||||
async function fetchUsage() {
|
||||
try {
|
||||
const response = await fetch('/api/billing/usage')
|
||||
if (!response.ok) {
|
||||
throw new Error('Network response was not ok')
|
||||
}
|
||||
usage.value = await response.json()
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch usage data:', error)
|
||||
}
|
||||
}
|
||||
onMounted(() => fetchUsage())
|
||||
|
||||
const verboseQuota = ref<
|
||||
{ based_quota: number; extra_quota: number; total_quota: number } | undefined
|
||||
>()
|
||||
async function fetchVerboseQuota() {
|
||||
try {
|
||||
const response = await fetch('/api/billing/quota')
|
||||
if (!response.ok) {
|
||||
throw new Error('Network response was not ok')
|
||||
}
|
||||
verboseQuota.value = await response.json()
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch verbose data:', error)
|
||||
}
|
||||
}
|
||||
onMounted(() => fetchVerboseQuota())
|
||||
|
||||
const quotaChartData = computed(() => ({
|
||||
labels: ['Base Quota', 'Extra Quota'],
|
||||
datasets: [
|
||||
{
|
||||
label: 'Verbose Quota',
|
||||
backgroundColor: '#7D80BAFF',
|
||||
data: [verboseQuota.value?.based_quota ?? 0, verboseQuota.value?.extra_quota ?? 0],
|
||||
},
|
||||
],
|
||||
}))
|
||||
const quotaUsagePercentage = computed(
|
||||
() => (usage.value.used_quota / usage.value.total_quota) * 100,
|
||||
)
|
||||
|
||||
function toGigabytes(bytes: number): number {
|
||||
return bytes / (1024 * 1024 * 1024)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.h-stats {
|
||||
height: 105px;
|
||||
}
|
||||
</style>
|
||||
@@ -1,262 +0,0 @@
|
||||
<template>
|
||||
<section class="min-h-full relative flex items-center justify-center">
|
||||
<n-spin v-if="!fileInfo && !error" />
|
||||
<n-result status="404" title="No file was found" :description="error" v-else-if="error" />
|
||||
<n-card class="max-w-4xl my-4 mx-8" v-else>
|
||||
<n-grid cols="1 m:2" x-gap="16" y-gap="16" responsive="screen">
|
||||
<n-gi>
|
||||
<div v-if="fileInfo.is_encrypted">
|
||||
<n-alert type="info" size="small" title="Encrypted file">
|
||||
The file has been encrypted. Preview not available. Please enter the password to
|
||||
download it.
|
||||
</n-alert>
|
||||
</div>
|
||||
<div v-else>
|
||||
<n-image v-if="fileType === 'image'" :src="fileSource" class="w-full" />
|
||||
<video v-else-if="fileType === 'video'" :src="fileSource" controls class="w-full" />
|
||||
<audio v-else-if="fileType === 'audio'" :src="fileSource" controls class="w-full" />
|
||||
<n-result
|
||||
status="418"
|
||||
title="Preview Unavailable"
|
||||
description="How can you preview this file?"
|
||||
size="small"
|
||||
class="py-6"
|
||||
v-else
|
||||
/>
|
||||
</div>
|
||||
</n-gi>
|
||||
|
||||
<n-gi>
|
||||
<div class="mb-3">
|
||||
<n-card title="File Infomation" size="small">
|
||||
<div class="flex gap-2">
|
||||
<span class="flex-grow-1 flex items-center gap-2">
|
||||
<n-icon>
|
||||
<info-round />
|
||||
</n-icon>
|
||||
File Type
|
||||
</span>
|
||||
<span>{{ fileInfo.mime_type }} ({{ fileType }})</span>
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<span class="flex-grow-1 flex items-center gap-2">
|
||||
<n-icon>
|
||||
<data-usage-round />
|
||||
</n-icon>
|
||||
File Size
|
||||
</span>
|
||||
<span>{{ formatBytes(fileInfo.size) }}</span>
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<span class="flex-grow-1 flex items-center gap-2">
|
||||
<n-icon>
|
||||
<file-upload-outlined />
|
||||
</n-icon>
|
||||
Uploaded At
|
||||
</span>
|
||||
<span>{{ new Date(fileInfo.created_at).toLocaleString() }}</span>
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<span class="flex-grow-1 flex items-center gap-2">
|
||||
<n-icon>
|
||||
<details-round />
|
||||
</n-icon>
|
||||
Techical Info
|
||||
</span>
|
||||
<n-button text size="small" @click="showTechDetails = !showTechDetails">
|
||||
{{ showTechDetails ? 'Hide' : 'Show' }}
|
||||
</n-button>
|
||||
</div>
|
||||
|
||||
<n-collapse-transition :show="showTechDetails">
|
||||
<div v-if="showTechDetails" class="mt-2 flex flex-col gap-1">
|
||||
<p class="text-xs opacity-75">#{{ fileInfo.id }}</p>
|
||||
|
||||
<n-card size="small" content-style="padding: 0" embedded>
|
||||
<div class="overflow-x-auto px-4 py-2">
|
||||
<n-code
|
||||
:code="JSON.stringify(fileInfo.file_meta, null, 4)"
|
||||
language="json"
|
||||
:hljs="hljs"
|
||||
/>
|
||||
</div>
|
||||
</n-card>
|
||||
</div>
|
||||
</n-collapse-transition>
|
||||
</n-card>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-3">
|
||||
<n-input
|
||||
v-if="fileInfo.is_encrypted"
|
||||
placeholder="Password"
|
||||
v-model:value="filePass"
|
||||
type="password"
|
||||
/>
|
||||
<div class="flex gap-2">
|
||||
<n-button class="flex-grow-1" @click="downloadFile">Download</n-button>
|
||||
<n-popover placement="bottom" trigger="hover">
|
||||
<template #trigger>
|
||||
<n-button>
|
||||
<n-icon>
|
||||
<qr-code-round />
|
||||
</n-icon>
|
||||
</n-button>
|
||||
</template>
|
||||
<n-qr-code
|
||||
type="svg"
|
||||
:value="currentUrl"
|
||||
:size="160"
|
||||
icon-src="/favicon.png"
|
||||
error-correction-level="H"
|
||||
/>
|
||||
</n-popover>
|
||||
</div>
|
||||
</div>
|
||||
<n-collapse-transition :show="!!progress">
|
||||
<n-progress
|
||||
:processing="!!progress && progress < 100"
|
||||
:percentage="progress"
|
||||
indicator-placement="inside"
|
||||
class="mt-4"
|
||||
/>
|
||||
</n-collapse-transition>
|
||||
</n-gi>
|
||||
</n-grid>
|
||||
</n-card>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
NCard,
|
||||
NInput,
|
||||
NButton,
|
||||
NProgress,
|
||||
NResult,
|
||||
NSpin,
|
||||
NImage,
|
||||
NAlert,
|
||||
NIcon,
|
||||
NCollapseTransition,
|
||||
NCode,
|
||||
NGrid,
|
||||
NGi,
|
||||
NPopover,
|
||||
NQrCode,
|
||||
useMessage,
|
||||
} from 'naive-ui'
|
||||
import {
|
||||
DataUsageRound,
|
||||
InfoRound,
|
||||
DetailsRound,
|
||||
FileUploadOutlined,
|
||||
QrCodeRound,
|
||||
} from '@vicons/material'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
|
||||
import { downloadAndDecryptFile } from './secure'
|
||||
import { formatBytes } from './format'
|
||||
|
||||
import hljs from 'highlight.js/lib/core'
|
||||
import json from 'highlight.js/lib/languages/json'
|
||||
|
||||
hljs.registerLanguage('json', json)
|
||||
|
||||
const route = useRoute()
|
||||
|
||||
const error = ref<string | null>(null)
|
||||
|
||||
const filePass = ref<string>('')
|
||||
const fileId = route.params.fileId
|
||||
const passcode = route.query.passcode as string | undefined
|
||||
|
||||
const progress = ref<number | undefined>(0)
|
||||
|
||||
const showTechDetails = ref<boolean>(false)
|
||||
|
||||
const messageDisplay = useMessage()
|
||||
|
||||
const currentUrl = window.location.href
|
||||
|
||||
const fileInfo = ref<any>(null)
|
||||
async function fetchFileInfo() {
|
||||
try {
|
||||
let url = '/api/files/' + fileId + '/info'
|
||||
if (passcode) {
|
||||
url += `?passcode=${passcode}`
|
||||
}
|
||||
const resp = await fetch(url)
|
||||
if (!resp.ok) {
|
||||
throw new Error('Failed to fetch file info: ' + resp.statusText)
|
||||
}
|
||||
fileInfo.value = await resp.json()
|
||||
} catch (err) {
|
||||
error.value = (err as Error).message
|
||||
}
|
||||
}
|
||||
onMounted(() => fetchFileInfo())
|
||||
|
||||
const fileType = computed(() => {
|
||||
if (!fileInfo.value) return 'unknown'
|
||||
return fileInfo.value.mime_type?.split('/')[0] || 'unknown'
|
||||
})
|
||||
const fileSource = computed(() => {
|
||||
let url = `/api/files/${fileId}`
|
||||
if (passcode) {
|
||||
url += `?passcode=${passcode}`
|
||||
}
|
||||
return url
|
||||
})
|
||||
|
||||
async function downloadFile() {
|
||||
if (fileInfo.value.is_encrypted && !filePass.value) {
|
||||
messageDisplay.error('Please enter the password to download the file.')
|
||||
return
|
||||
}
|
||||
if (fileInfo.value.is_encrypted) {
|
||||
downloadAndDecryptFile(fileSource.value, filePass.value, fileInfo.value.name, (p: number) => {
|
||||
progress.value = p * 100
|
||||
}).catch((err) => {
|
||||
messageDisplay.error('Download failed: ' + err.message, { closable: true, duration: 10000 })
|
||||
progress.value = undefined
|
||||
})
|
||||
} else {
|
||||
const res = await fetch(fileSource.value)
|
||||
if (!res.ok) {
|
||||
throw new Error(`Failed to download ${fileInfo.value.name}: ${res.statusText}`)
|
||||
}
|
||||
|
||||
const contentLength = res.headers.get('content-length')
|
||||
if (!contentLength) {
|
||||
throw new Error('Content-Length response header is missing.')
|
||||
}
|
||||
|
||||
const total = parseInt(contentLength, 10)
|
||||
const reader = res.body!.getReader()
|
||||
const chunks: Uint8Array[] = []
|
||||
let received = 0
|
||||
|
||||
while (true) {
|
||||
const { done, value } = await reader.read()
|
||||
if (done) break
|
||||
if (value) {
|
||||
chunks.push(value)
|
||||
received += value.length
|
||||
progress.value = (received / total) * 100
|
||||
}
|
||||
}
|
||||
|
||||
const blob = new Blob(chunks)
|
||||
const blobUrl = window.URL.createObjectURL(blob)
|
||||
const a = document.createElement('a')
|
||||
a.href = blobUrl
|
||||
a.download = fileInfo.value.name || 'download'
|
||||
document.body.appendChild(a)
|
||||
a.click()
|
||||
a.remove()
|
||||
window.URL.revokeObjectURL(blobUrl)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -1,8 +0,0 @@
|
||||
export function formatBytes(bytes: number, decimals = 2): string {
|
||||
if (bytes === 0) return '0 Bytes'
|
||||
const k = 1024
|
||||
const dm = decimals < 0 ? 0 : decimals
|
||||
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k))
|
||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i]
|
||||
}
|
||||
@@ -1,164 +0,0 @@
|
||||
<template>
|
||||
<section class="h-full relative flex flex-col items-center justify-center">
|
||||
<n-card class="max-w-lg my-4 mx-8" title="About" v-if="!userStore.user">
|
||||
<p>Welcome to the <b>Solar Drive</b></p>
|
||||
<p>We help you upload, collect, and share files with ease in mind.</p>
|
||||
<p>To continue, login first.</p>
|
||||
</n-card>
|
||||
|
||||
<n-card class="max-w-2xl" v-else content-style="padding: 0;">
|
||||
<n-tabs type="line" animated :tabs-padding="20" pane-style="padding: 20px">
|
||||
<template #suffix>
|
||||
<div class="flex gap-2 items-center me-4">
|
||||
<p>Advance Mode</p>
|
||||
<n-switch v-model:value="modeAdvanced" size="small" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<n-tab-pane name="direct" tab="Direct Upload" :disabled="isBundleMode">
|
||||
<div class="mb-3">
|
||||
<file-pool-select v-model="filePool" @update:pool="currentFilePool = $event" />
|
||||
</div>
|
||||
<upload-area
|
||||
:filePool="filePool"
|
||||
:pools="pools as SnFilePool[]"
|
||||
:modeAdvanced="modeAdvanced"
|
||||
/>
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="bundle" tab="Bundle Upload">
|
||||
<div class="mb-3">
|
||||
<bundle-select v-model:bundle="selectedBundleId" :disabled="isBundleMode" />
|
||||
</div>
|
||||
|
||||
<n-modal v-model:show="showCreateBundleModal" preset="dialog" title="Create New Bundle">
|
||||
<bundle-form ref="bundleFormRef" :value="newBundle" />
|
||||
<template #action>
|
||||
<n-button @click="showCreateBundleModal = false">Cancel</n-button>
|
||||
<n-button type="primary" @click="createBundle">Create</n-button>
|
||||
</template>
|
||||
</n-modal>
|
||||
|
||||
<div class="flex justify-between">
|
||||
<n-button @click="showCreateBundleModal = true" class="mb-3" :disabled="isBundleMode">
|
||||
Create New Bundle
|
||||
</n-button>
|
||||
<n-button
|
||||
type="primary"
|
||||
:disabled="!selectedBundleId && !newBundleId && !isBundleMode"
|
||||
@click="isBundleMode ? cancelBundleUpload() : proceedToBundleUpload()"
|
||||
>
|
||||
{{ isBundleMode ? 'Cancel' : 'Proceed to Upload' }}
|
||||
</n-button>
|
||||
</div>
|
||||
|
||||
<div v-if="bundleUploadMode" class="mt-3">
|
||||
<div class="mb-3">
|
||||
<file-pool-select v-model="filePool" @update:pool="currentFilePool = $event" />
|
||||
</div>
|
||||
<upload-area
|
||||
:filePool="filePool"
|
||||
:pools="pools as SnFilePool[]"
|
||||
:modeAdvanced="modeAdvanced"
|
||||
:bundleId="currentBundleId!"
|
||||
/>
|
||||
</div>
|
||||
</n-tab-pane>
|
||||
</n-tabs>
|
||||
</n-card>
|
||||
|
||||
<p class="mt-4 opacity-75 text-xs">
|
||||
<span v-if="version == null">Loading...</span>
|
||||
<span v-else>
|
||||
v{{ version.version }} @
|
||||
{{ version.commit.substring(0, 6) }}
|
||||
{{ version.updatedAt }}
|
||||
</span>
|
||||
</p>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { NCard, NSwitch, NTabs, NTabPane, NButton, NModal } from 'naive-ui'
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import { useUserStore } from '@/stores/user'
|
||||
import type { SnFilePool } from '@/types/pool'
|
||||
import FilePoolSelect from '@/components/FilePoolSelect.vue'
|
||||
import UploadArea from '@/components/UploadArea.vue'
|
||||
import BundleSelect from '@/components/BundleSelect.vue'
|
||||
import BundleForm from '@/components/form/BundleForm.vue'
|
||||
|
||||
const userStore = useUserStore()
|
||||
|
||||
const version = ref<any>(null)
|
||||
async function fetchVersion() {
|
||||
const resp = await fetch('/api/version')
|
||||
version.value = await resp.json()
|
||||
}
|
||||
onMounted(() => fetchVersion())
|
||||
|
||||
type SnFilePoolOption = SnFilePool & any
|
||||
|
||||
const pools = ref<SnFilePoolOption[] | undefined>()
|
||||
async function fetchPools() {
|
||||
const resp = await fetch('/api/pools')
|
||||
pools.value = await resp.json()
|
||||
}
|
||||
onMounted(() => fetchPools())
|
||||
|
||||
const modeAdvanced = ref(false)
|
||||
|
||||
const filePool = ref<string | null>(null)
|
||||
|
||||
const currentFilePool = computed(() => {
|
||||
if (!filePool.value) return null
|
||||
return pools.value?.find((pool) => pool.id === filePool.value) ?? null
|
||||
})
|
||||
|
||||
const bundles = ref<any>([])
|
||||
const selectedBundleId = ref<string | null>(null)
|
||||
const showCreateBundleModal = ref(false)
|
||||
const newBundle = ref<any>({})
|
||||
const bundleFormRef = ref<any>(null)
|
||||
const bundleUploadMode = ref(false)
|
||||
const currentBundleId = ref<string | null>(null)
|
||||
const newBundleId = ref<string | null>(null)
|
||||
const isBundleMode = ref(false)
|
||||
|
||||
async function createBundle() {
|
||||
try {
|
||||
await bundleFormRef.value?.formRef?.validate()
|
||||
const resp = await fetch('/api/bundles', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(newBundle.value),
|
||||
})
|
||||
if (!resp.ok) {
|
||||
throw new Error('Failed to create bundle')
|
||||
}
|
||||
const createdBundle = await resp.json()
|
||||
bundles.value.push(createdBundle)
|
||||
selectedBundleId.value = createdBundle.id
|
||||
newBundleId.value = createdBundle.id
|
||||
showCreateBundleModal.value = false
|
||||
newBundle.value = {}
|
||||
} catch (error) {
|
||||
console.error('Failed to create bundle:', error)
|
||||
}
|
||||
}
|
||||
|
||||
function proceedToBundleUpload() {
|
||||
currentBundleId.value = selectedBundleId.value || newBundleId.value
|
||||
bundleUploadMode.value = true
|
||||
isBundleMode.value = true
|
||||
}
|
||||
|
||||
function cancelBundleUpload() {
|
||||
bundleUploadMode.value = false
|
||||
isBundleMode.value = false
|
||||
currentBundleId.value = null
|
||||
selectedBundleId.value = null
|
||||
newBundleId.value = null
|
||||
}
|
||||
</script>
|
||||
@@ -1,16 +0,0 @@
|
||||
<template>
|
||||
<section class="h-full flex items-center justify-center">
|
||||
<n-result status="404" title="404" description="Page not found">
|
||||
<template #footer>
|
||||
<n-button @click="router.push('/')">Go to Home</n-button>
|
||||
</template>
|
||||
</n-result>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { NResult, NButton } from 'naive-ui'
|
||||
import { useRouter } from 'vue-router';
|
||||
|
||||
const router = useRouter()
|
||||
</script>
|
||||
@@ -1,94 +0,0 @@
|
||||
export async function downloadAndDecryptFile(
|
||||
url: string,
|
||||
password: string,
|
||||
fileName: string,
|
||||
onProgress?: (progress: number) => void,
|
||||
): Promise<void> {
|
||||
const response = await fetch(url)
|
||||
if (!response.ok) throw new Error(`Failed to fetch: ${response.status}`)
|
||||
|
||||
const contentLength = +(response.headers.get('Content-Length') || 0)
|
||||
const reader = response.body!.getReader()
|
||||
const chunks: Uint8Array[] = []
|
||||
let received = 0
|
||||
|
||||
while (true) {
|
||||
const { done, value } = await reader.read()
|
||||
if (done) break
|
||||
if (value) {
|
||||
chunks.push(value)
|
||||
received += value.length
|
||||
if (contentLength && onProgress) {
|
||||
onProgress(received / contentLength)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const fullBuffer = new Uint8Array(received)
|
||||
let offset = 0
|
||||
for (const chunk of chunks) {
|
||||
fullBuffer.set(chunk, offset)
|
||||
offset += chunk.length
|
||||
}
|
||||
|
||||
const decryptedBytes = await decryptFile(fullBuffer, password)
|
||||
|
||||
// Create a blob and trigger a download
|
||||
const blob = new Blob([decryptedBytes])
|
||||
const downloadUrl = URL.createObjectURL(blob)
|
||||
const a = document.createElement('a')
|
||||
a.href = downloadUrl
|
||||
a.download = fileName
|
||||
document.body.appendChild(a)
|
||||
a.click()
|
||||
a.remove()
|
||||
URL.revokeObjectURL(downloadUrl)
|
||||
}
|
||||
|
||||
export async function decryptFile(fileBuffer: Uint8Array, password: string): Promise<Uint8Array> {
|
||||
const salt = fileBuffer.slice(0, 16)
|
||||
const nonce = fileBuffer.slice(16, 28)
|
||||
const tag = fileBuffer.slice(28, 44)
|
||||
const ciphertext = fileBuffer.slice(44)
|
||||
|
||||
const enc = new TextEncoder()
|
||||
const keyMaterial = await crypto.subtle.importKey(
|
||||
'raw',
|
||||
enc.encode(password),
|
||||
{ name: 'PBKDF2' },
|
||||
false,
|
||||
['deriveKey'],
|
||||
)
|
||||
const key = await crypto.subtle.deriveKey(
|
||||
{ name: 'PBKDF2', salt, iterations: 100000, hash: 'SHA-256' },
|
||||
keyMaterial,
|
||||
{ name: 'AES-GCM', length: 256 },
|
||||
false,
|
||||
['decrypt'],
|
||||
)
|
||||
|
||||
const fullCiphertext = new Uint8Array(ciphertext.length + tag.length)
|
||||
fullCiphertext.set(ciphertext)
|
||||
fullCiphertext.set(tag, ciphertext.length)
|
||||
|
||||
let decrypted: ArrayBuffer
|
||||
try {
|
||||
decrypted = await crypto.subtle.decrypt(
|
||||
{ name: 'AES-GCM', iv: nonce, tagLength: 128 },
|
||||
key,
|
||||
fullCiphertext,
|
||||
)
|
||||
} catch {
|
||||
throw new Error('Incorrect password or corrupted file.')
|
||||
}
|
||||
|
||||
const magic = new TextEncoder().encode('DYSON1')
|
||||
const decryptedBytes = new Uint8Array(decrypted)
|
||||
for (let i = 0; i < magic.length; i++) {
|
||||
if (decryptedBytes[i] !== magic[i]) {
|
||||
throw new Error('Incorrect password or corrupted file.')
|
||||
}
|
||||
}
|
||||
|
||||
return decryptedBytes.slice(magic.length)
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
{
|
||||
"extends": "@vue/tsconfig/tsconfig.dom.json",
|
||||
"include": ["env.d.ts", "src/**/*", "src/**/*.vue", "./**/*.d.ts"],
|
||||
"exclude": ["src/**/__tests__/*"],
|
||||
"compilerOptions": {
|
||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
|
||||
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
{
|
||||
"files": [],
|
||||
"references": [
|
||||
{
|
||||
"path": "./tsconfig.node.json"
|
||||
},
|
||||
{
|
||||
"path": "./tsconfig.app.json"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
{
|
||||
"extends": "@tsconfig/node22/tsconfig.json",
|
||||
"include": [
|
||||
"vite.config.*",
|
||||
"vitest.config.*",
|
||||
"cypress.config.*",
|
||||
"nightwatch.conf.*",
|
||||
"playwright.config.*",
|
||||
"eslint.config.*"
|
||||
],
|
||||
"compilerOptions": {
|
||||
"noEmit": true,
|
||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
|
||||
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "Bundler",
|
||||
"types": ["node"]
|
||||
}
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
import { fileURLToPath, URL } from 'node:url'
|
||||
|
||||
import { defineConfig } from 'vite'
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
import vueJsx from '@vitejs/plugin-vue-jsx'
|
||||
import vueDevTools from 'vite-plugin-vue-devtools'
|
||||
import tailwindcss from '@tailwindcss/vite'
|
||||
|
||||
process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0'
|
||||
|
||||
// https://vite.dev/config/
|
||||
export default defineConfig({
|
||||
base: '/',
|
||||
plugins: [vue(), vueJsx(), vueDevTools(), tailwindcss()],
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': fileURLToPath(new URL('./src', import.meta.url)),
|
||||
},
|
||||
},
|
||||
server: {
|
||||
proxy: {
|
||||
'/api': {
|
||||
target: 'http://localhost:5090',
|
||||
changeOrigin: true,
|
||||
},
|
||||
'/cgi': {
|
||||
target: 'http://localhost:5090',
|
||||
changeOrigin: true,
|
||||
}
|
||||
},
|
||||
},
|
||||
})
|
||||
@@ -1,8 +1,10 @@
|
||||
FROM mcr.microsoft.com/dotnet/aspnet:9.0 AS base
|
||||
FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS base
|
||||
WORKDIR /app
|
||||
EXPOSE 8080
|
||||
EXPOSE 8081
|
||||
|
||||
# Stage 1: Install runtime dependencies
|
||||
|
||||
# Install only necessary dependencies
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
libfontconfig1 \
|
||||
@@ -17,34 +19,14 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
|
||||
USER $APP_UID
|
||||
|
||||
# Stage 2: Build SPA
|
||||
FROM node:22-alpine AS spa-builder
|
||||
WORKDIR /src
|
||||
|
||||
# Copy package files for SPA
|
||||
COPY ["DysonNetwork.Drive/Client/package.json", "DysonNetwork.Drive/Client/package-lock.json*", "./Client/"]
|
||||
|
||||
# Install SPA dependencies
|
||||
WORKDIR /src/Client
|
||||
RUN npm install
|
||||
|
||||
# Copy SPA source
|
||||
COPY ["DysonNetwork.Drive/Client/", "./"]
|
||||
|
||||
# Build SPA
|
||||
RUN npm run build
|
||||
|
||||
# Stage 3: Build .NET application
|
||||
FROM mcr.microsoft.com/dotnet/sdk:9.0 AS build
|
||||
# Stage 2: Build .NET application
|
||||
FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
|
||||
ARG BUILD_CONFIGURATION=Release
|
||||
WORKDIR /src
|
||||
COPY ["DysonNetwork.Drive/DysonNetwork.Drive.csproj", "DysonNetwork.Drive/"]
|
||||
RUN dotnet restore "DysonNetwork.Drive/DysonNetwork.Drive.csproj"
|
||||
COPY . .
|
||||
|
||||
# Copy built SPA to wwwroot
|
||||
COPY --from=spa-builder /src/Client/dist /src/DysonNetwork.Drive/wwwroot/dist
|
||||
|
||||
WORKDIR "/src/DysonNetwork.Drive"
|
||||
RUN dotnet build "./DysonNetwork.Drive.csproj" -c $BUILD_CONFIGURATION -o /app/build \
|
||||
-p:TypeScriptCompileBlocked=true \
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>
|
||||
@@ -10,54 +10,35 @@
|
||||
<ItemGroup>
|
||||
<PackageReference Include="BCrypt.Net-Next" Version="4.0.3" />
|
||||
<PackageReference Include="BlurHashSharp.SkiaSharp" Version="1.3.4" />
|
||||
<PackageReference Include="FFMpegCore" Version="5.2.0" />
|
||||
<PackageReference Include="FFMpegCore" Version="5.4.0" />
|
||||
<PackageReference Include="Grpc.AspNetCore.Server" Version="2.71.0" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="9.0.7" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.7">
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="10.0.0" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.11">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="MimeKit" Version="4.14.0" />
|
||||
<PackageReference Include="MimeTypes" Version="2.5.2">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Minio" Version="6.0.5" />
|
||||
<PackageReference Include="Nerdbank.GitVersioning" Version="3.7.115">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Minio" Version="7.0.0" />
|
||||
<PackageReference Include="Nanoid" Version="3.1.0" />
|
||||
<PackageReference Include="NetVips" Version="3.1.0" />
|
||||
<PackageReference Include="NetVips.Native.linux-x64" Version="8.17.1" />
|
||||
<PackageReference Include="NetVips.Native.osx-arm64" Version="8.17.1" />
|
||||
<PackageReference Include="NetVips.Native.linux-x64" Version="8.17.3" />
|
||||
<PackageReference Include="NetVips.Native.osx-arm64" Version="8.17.3" />
|
||||
<PackageReference Include="NodaTime" Version="3.2.2" />
|
||||
<PackageReference Include="NodaTime.Serialization.JsonNet" Version="3.2.0" />
|
||||
<PackageReference Include="NodaTime.Serialization.Protobuf" Version="2.0.2" />
|
||||
<PackageReference Include="NodaTime.Serialization.SystemTextJson" Version="1.3.0" />
|
||||
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="9.0.4" />
|
||||
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL.Design" Version="1.1.0" />
|
||||
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL.NetTopologySuite" Version="9.0.4" />
|
||||
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL.NodaTime" Version="9.0.4" />
|
||||
<PackageReference Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" Version="1.12.0" />
|
||||
<PackageReference Include="OpenTelemetry.Extensions.Hosting" Version="1.12.0" />
|
||||
<PackageReference Include="OpenTelemetry.Instrumentation.AspNetCore" Version="1.12.0" />
|
||||
<PackageReference Include="OpenTelemetry.Instrumentation.Http" Version="1.12.0" />
|
||||
<PackageReference Include="OpenTelemetry.Instrumentation.Runtime" Version="1.12.0" />
|
||||
<PackageReference Include="prometheus-net.AspNetCore" Version="8.2.1" />
|
||||
<PackageReference Include="prometheus-net.AspNetCore.HealthChecks" Version="8.2.1" />
|
||||
<PackageReference Include="prometheus-net.DotNetRuntime" Version="4.4.1" />
|
||||
<PackageReference Include="prometheus-net.EntityFramework" Version="0.9.5" />
|
||||
<PackageReference Include="prometheus-net.SystemMetrics" Version="3.1.0" />
|
||||
<PackageReference Include="Quartz" Version="3.14.0" />
|
||||
<PackageReference Include="Quartz.AspNetCore" Version="3.14.0" />
|
||||
<PackageReference Include="Quartz.Extensions.Hosting" Version="3.14.0" />
|
||||
<PackageReference Include="EFCore.BulkExtensions" Version="9.0.1" />
|
||||
<PackageReference Include="EFCore.BulkExtensions.PostgreSql" Version="9.0.1" />
|
||||
<PackageReference Include="EFCore.NamingConventions" Version="9.0.0" />
|
||||
<PackageReference Include="Quartz" Version="3.15.1" />
|
||||
<PackageReference Include="Quartz.AspNetCore" Version="3.15.1" />
|
||||
<PackageReference Include="Quartz.Extensions.Hosting" Version="3.15.1" />
|
||||
<PackageReference Include="EFCore.BulkExtensions.PostgreSql" Version="9.0.2" />
|
||||
<!-- Pin the SkiaSharp version at the 2.88.9 due to the BlurHash need this specific version -->
|
||||
<PackageReference Include="SkiaSharp" Version="2.88.9" />
|
||||
<PackageReference Include="SkiaSharp.NativeAssets.Linux" Version="2.88.9" />
|
||||
<PackageReference Include="SkiaSharp.NativeAssets.Linux.NoDependencies" Version="2.88.9" />
|
||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="9.0.3" />
|
||||
<PackageReference Include="Swashbuckle.AspNetCore.SwaggerUI" Version="9.0.3" />
|
||||
<PackageReference Include="tusdotnet" Version="2.10.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
@@ -69,13 +50,4 @@
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\DysonNetwork.Shared\DysonNetwork.Shared.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<_ContentIncludedByDefault Remove="Pages\Emails\AccountDeletionEmail.razor" />
|
||||
<_ContentIncludedByDefault Remove="Pages\Emails\ContactVerificationEmail.razor" />
|
||||
<_ContentIncludedByDefault Remove="Pages\Emails\EmailLayout.razor" />
|
||||
<_ContentIncludedByDefault Remove="Pages\Emails\LandingEmail.razor" />
|
||||
<_ContentIncludedByDefault Remove="Pages\Emails\PasswordResetEmail.razor" />
|
||||
<_ContentIncludedByDefault Remove="Pages\Emails\VerificationEmail.razor" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
||||
585
DysonNetwork.Drive/Index/FileIndexController.cs
Normal file
585
DysonNetwork.Drive/Index/FileIndexController.cs
Normal file
@@ -0,0 +1,585 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using DysonNetwork.Drive.Storage;
|
||||
using DysonNetwork.Shared.Auth;
|
||||
using DysonNetwork.Shared.Http;
|
||||
using DysonNetwork.Shared.Models;
|
||||
using DysonNetwork.Shared.Proto;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace DysonNetwork.Drive.Index;
|
||||
|
||||
[ApiController]
|
||||
[Route("/api/index")]
|
||||
[Authorize]
|
||||
public class FileIndexController(
|
||||
FileIndexService fileIndexService,
|
||||
AppDatabase db,
|
||||
ILogger<FileIndexController> logger
|
||||
) : ControllerBase
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets files in a specific path for the current user
|
||||
/// </summary>
|
||||
/// <param name="path">The path to browse (defaults to root "/")</param>
|
||||
/// <param name="query">Optional query to filter files by name</param>
|
||||
/// <param name="order">The field to order by (date, size, name - defaults to date)</param>
|
||||
/// <param name="orderDesc">Whether to order in descending order (defaults to true)</param>
|
||||
/// <returns>List of files in the specified path</returns>
|
||||
[HttpGet("browse")]
|
||||
public async Task<IActionResult> BrowseFiles(
|
||||
[FromQuery] string path = "/",
|
||||
[FromQuery] string? query = null,
|
||||
[FromQuery] string order = "date",
|
||||
[FromQuery] bool orderDesc = true
|
||||
)
|
||||
{
|
||||
if (HttpContext.Items["CurrentUser"] is not Account currentUser)
|
||||
return new ObjectResult(ApiError.Unauthorized()) { StatusCode = 401 };
|
||||
|
||||
var accountId = Guid.Parse(currentUser.Id);
|
||||
|
||||
try
|
||||
{
|
||||
var fileIndexes = await fileIndexService.GetByPathAsync(accountId, path);
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(query))
|
||||
{
|
||||
fileIndexes = fileIndexes
|
||||
.Where(fi => fi.File.Name.Contains(query, StringComparison.OrdinalIgnoreCase))
|
||||
.ToList();
|
||||
}
|
||||
|
||||
// Apply sorting
|
||||
fileIndexes = order.ToLower() switch
|
||||
{
|
||||
"name" => orderDesc ? fileIndexes.OrderByDescending(fi => fi.File.Name).ToList()
|
||||
: fileIndexes.OrderBy(fi => fi.File.Name).ToList(),
|
||||
"size" => orderDesc ? fileIndexes.OrderByDescending(fi => fi.File.Size).ToList()
|
||||
: fileIndexes.OrderBy(fi => fi.File.Size).ToList(),
|
||||
_ => orderDesc ? fileIndexes.OrderByDescending(fi => fi.File.CreatedAt).ToList()
|
||||
: fileIndexes.OrderBy(fi => fi.File.CreatedAt).ToList()
|
||||
};
|
||||
|
||||
// Get all file indexes for this account to extract child folders
|
||||
var allFileIndexes = await fileIndexService.GetByAccountIdAsync(accountId);
|
||||
|
||||
// Extract unique child folder paths
|
||||
var childFolders = ExtractChildFolders(allFileIndexes, path);
|
||||
|
||||
return Ok(new
|
||||
{
|
||||
Path = path,
|
||||
Files = fileIndexes,
|
||||
Folders = childFolders,
|
||||
TotalCount = fileIndexes.Count
|
||||
});
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError(ex, "Failed to browse files for account {AccountId} at path {Path}", accountId, path);
|
||||
return new ObjectResult(new ApiError
|
||||
{
|
||||
Code = "BROWSE_FAILED",
|
||||
Message = "Failed to browse files",
|
||||
Status = 500
|
||||
}) { StatusCode = 500 };
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Extracts unique child folder paths from all file indexes for a given parent path
|
||||
/// </summary>
|
||||
/// <param name="allFileIndexes">All file indexes for the account</param>
|
||||
/// <param name="parentPath">The parent path to find children for</param>
|
||||
/// <returns>List of unique child folder names</returns>
|
||||
private List<string> ExtractChildFolders(List<SnCloudFileIndex> allFileIndexes, string parentPath)
|
||||
{
|
||||
var normalizedParentPath = FileIndexService.NormalizePath(parentPath);
|
||||
var childFolders = new HashSet<string>();
|
||||
|
||||
foreach (var index in allFileIndexes)
|
||||
{
|
||||
var normalizedIndexPath = FileIndexService.NormalizePath(index.Path);
|
||||
|
||||
// Check if this path is a direct child of the parent path
|
||||
if (normalizedIndexPath.StartsWith(normalizedParentPath) &&
|
||||
normalizedIndexPath != normalizedParentPath)
|
||||
{
|
||||
// Remove the parent path prefix to get the relative path
|
||||
var relativePath = normalizedIndexPath.Substring(normalizedParentPath.Length);
|
||||
|
||||
// Extract the first folder name (direct child)
|
||||
var firstSlashIndex = relativePath.IndexOf('/');
|
||||
if (firstSlashIndex > 0)
|
||||
{
|
||||
var folderName = relativePath.Substring(0, firstSlashIndex);
|
||||
childFolders.Add(folderName);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return childFolders.OrderBy(f => f).ToList();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets all files for the current user (across all paths)
|
||||
/// </summary>
|
||||
/// <param name="query">Optional query to filter files by name</param>
|
||||
/// <param name="order">The field to order by (date, size, name - defaults to date)</param>
|
||||
/// <param name="orderDesc">Whether to order in descending order (defaults to true)</param>
|
||||
/// <returns>List of all files for the user</returns>
|
||||
[HttpGet("all")]
|
||||
public async Task<IActionResult> GetAllFiles(
|
||||
[FromQuery] string? query = null,
|
||||
[FromQuery] string order = "date",
|
||||
[FromQuery] bool orderDesc = true
|
||||
)
|
||||
{
|
||||
if (HttpContext.Items["CurrentUser"] is not Account currentUser)
|
||||
return new ObjectResult(ApiError.Unauthorized()) { StatusCode = 401 };
|
||||
|
||||
var accountId = Guid.Parse(currentUser.Id);
|
||||
|
||||
try
|
||||
{
|
||||
var fileIndexes = await fileIndexService.GetByAccountIdAsync(accountId);
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(query))
|
||||
{
|
||||
fileIndexes = fileIndexes
|
||||
.Where(fi => fi.File.Name.Contains(query, StringComparison.OrdinalIgnoreCase))
|
||||
.ToList();
|
||||
}
|
||||
|
||||
// Apply sorting
|
||||
fileIndexes = order.ToLower() switch
|
||||
{
|
||||
"name" => orderDesc ? fileIndexes.OrderByDescending(fi => fi.File.Name).ToList()
|
||||
: fileIndexes.OrderBy(fi => fi.File.Name).ToList(),
|
||||
"size" => orderDesc ? fileIndexes.OrderByDescending(fi => fi.File.Size).ToList()
|
||||
: fileIndexes.OrderBy(fi => fi.File.Size).ToList(),
|
||||
_ => orderDesc ? fileIndexes.OrderByDescending(fi => fi.File.CreatedAt).ToList()
|
||||
: fileIndexes.OrderBy(fi => fi.File.CreatedAt).ToList()
|
||||
};
|
||||
|
||||
return Ok(new
|
||||
{
|
||||
Files = fileIndexes,
|
||||
TotalCount = fileIndexes.Count()
|
||||
});
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError(ex, "Failed to get all files for account {AccountId}", accountId);
|
||||
return new ObjectResult(new ApiError
|
||||
{
|
||||
Code = "GET_ALL_FAILED",
|
||||
Message = "Failed to get files",
|
||||
Status = 500
|
||||
}) { StatusCode = 500 };
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets files that have not been indexed for the current user.
|
||||
/// </summary>
|
||||
/// <param name="recycled">Shows recycled files or not</param>
|
||||
/// <param name="offset">The number of files to skip</param>
|
||||
/// <param name="take">The number of files to return</param>
|
||||
/// <param name="pool">The pool ID of those files</param>
|
||||
/// <param name="query">Optional query to filter files by name</param>
|
||||
/// <param name="order">The field to order by (date, size, name - defaults to date)</param>
|
||||
/// <param name="orderDesc">Whether to order in descending order (defaults to true)</param>
|
||||
/// <returns>List of unindexed files</returns>
|
||||
[HttpGet("unindexed")]
|
||||
public async Task<IActionResult> GetUnindexedFiles(
|
||||
[FromQuery] Guid? pool,
|
||||
[FromQuery] bool recycled = false,
|
||||
[FromQuery] int offset = 0,
|
||||
[FromQuery] int take = 20,
|
||||
[FromQuery] string? query = null,
|
||||
[FromQuery] string order = "date",
|
||||
[FromQuery] bool orderDesc = true
|
||||
)
|
||||
{
|
||||
if (HttpContext.Items["CurrentUser"] is not Account currentUser)
|
||||
return new ObjectResult(ApiError.Unauthorized()) { StatusCode = 401 };
|
||||
|
||||
var accountId = Guid.Parse(currentUser.Id);
|
||||
|
||||
try
|
||||
{
|
||||
var filesQuery = db.Files
|
||||
.Where(f => f.AccountId == accountId
|
||||
&& f.IsMarkedRecycle == recycled
|
||||
&& !db.FileIndexes.Any(fi => fi.FileId == f.Id && fi.AccountId == accountId)
|
||||
)
|
||||
.AsQueryable();
|
||||
|
||||
// Apply sorting
|
||||
filesQuery = order.ToLower() switch
|
||||
{
|
||||
"name" => orderDesc ? filesQuery.OrderByDescending(f => f.Name)
|
||||
: filesQuery.OrderBy(f => f.Name),
|
||||
"size" => orderDesc ? filesQuery.OrderByDescending(f => f.Size)
|
||||
: filesQuery.OrderBy(f => f.Size),
|
||||
_ => orderDesc ? filesQuery.OrderByDescending(f => f.CreatedAt)
|
||||
: filesQuery.OrderBy(f => f.CreatedAt)
|
||||
};
|
||||
|
||||
if (pool.HasValue) filesQuery = filesQuery.Where(f => f.PoolId == pool);
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(query))
|
||||
{
|
||||
filesQuery = filesQuery.Where(f => f.Name.Contains(query));
|
||||
}
|
||||
|
||||
var totalCount = await filesQuery.CountAsync();
|
||||
|
||||
Response.Headers.Append("X-Total", totalCount.ToString());
|
||||
|
||||
var unindexedFiles = await filesQuery
|
||||
.Skip(offset)
|
||||
.Take(take)
|
||||
.ToListAsync();
|
||||
|
||||
return Ok(unindexedFiles);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError(ex, "Failed to get unindexed files for account {AccountId}", accountId);
|
||||
return new ObjectResult(new ApiError
|
||||
{
|
||||
Code = "GET_UNINDEXED_FAILED",
|
||||
Message = "Failed to get unindexed files",
|
||||
Status = 500
|
||||
}) { StatusCode = 500 };
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Moves a file to a new path
|
||||
/// </summary>
|
||||
/// <param name="indexId">The file index ID</param>
|
||||
/// <param name="newPath">The new path</param>
|
||||
/// <returns>The updated file index</returns>
|
||||
[HttpPost("move/{indexId}")]
|
||||
public async Task<IActionResult> MoveFile(Guid indexId, [FromBody] MoveFileRequest request)
|
||||
{
|
||||
if (HttpContext.Items["CurrentUser"] is not Account currentUser)
|
||||
return new ObjectResult(ApiError.Unauthorized()) { StatusCode = 401 };
|
||||
|
||||
var accountId = Guid.Parse(currentUser.Id);
|
||||
|
||||
try
|
||||
{
|
||||
// Verify ownership
|
||||
var existingIndex = await db.FileIndexes
|
||||
.Include(fi => fi.File)
|
||||
.FirstOrDefaultAsync(fi => fi.Id == indexId && fi.AccountId == accountId);
|
||||
|
||||
if (existingIndex == null)
|
||||
return new ObjectResult(ApiError.NotFound("File index")) { StatusCode = 404 };
|
||||
|
||||
var updatedIndex = await fileIndexService.UpdateAsync(indexId, request.NewPath);
|
||||
|
||||
if (updatedIndex == null)
|
||||
return new ObjectResult(ApiError.NotFound("File index")) { StatusCode = 404 };
|
||||
|
||||
return Ok(new
|
||||
{
|
||||
updatedIndex.FileId,
|
||||
IndexId = updatedIndex.Id,
|
||||
OldPath = existingIndex.Path,
|
||||
NewPath = updatedIndex.Path,
|
||||
Message = "File moved successfully"
|
||||
});
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError(ex, "Failed to move file index {IndexId} for account {AccountId}", indexId, accountId);
|
||||
return new ObjectResult(new ApiError
|
||||
{
|
||||
Code = "MOVE_FAILED",
|
||||
Message = "Failed to move file",
|
||||
Status = 500
|
||||
}) { StatusCode = 500 };
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Removes a file index (does not delete the actual file by default)
|
||||
/// </summary>
|
||||
/// <param name="indexId">The file index ID</param>
|
||||
/// <param name="deleteFile">Whether to also delete the actual file data</param>
|
||||
/// <returns>Success message</returns>
|
||||
[HttpDelete("remove/{indexId}")]
|
||||
public async Task<IActionResult> RemoveFileIndex(Guid indexId, [FromQuery] bool deleteFile = false)
|
||||
{
|
||||
if (HttpContext.Items["CurrentUser"] is not Account currentUser)
|
||||
return new ObjectResult(ApiError.Unauthorized()) { StatusCode = 401 };
|
||||
|
||||
var accountId = Guid.Parse(currentUser.Id);
|
||||
|
||||
try
|
||||
{
|
||||
// Verify ownership
|
||||
var existingIndex = await db.FileIndexes
|
||||
.Include(fi => fi.File)
|
||||
.FirstOrDefaultAsync(fi => fi.Id == indexId && fi.AccountId == accountId);
|
||||
|
||||
if (existingIndex == null)
|
||||
return new ObjectResult(ApiError.NotFound("File index")) { StatusCode = 404 };
|
||||
|
||||
var fileId = existingIndex.FileId;
|
||||
var fileName = existingIndex.File.Name;
|
||||
var filePath = existingIndex.Path;
|
||||
|
||||
// Remove the index
|
||||
var removed = await fileIndexService.RemoveAsync(indexId);
|
||||
|
||||
if (!removed)
|
||||
return new ObjectResult(ApiError.NotFound("File index")) { StatusCode = 404 };
|
||||
|
||||
// Optionally delete the actual file
|
||||
if (!deleteFile)
|
||||
return Ok(new
|
||||
{
|
||||
Message = deleteFile
|
||||
? "File index and file data removed successfully"
|
||||
: "File index removed successfully",
|
||||
FileId = fileId,
|
||||
FileName = fileName,
|
||||
Path = filePath,
|
||||
FileDataDeleted = deleteFile
|
||||
});
|
||||
try
|
||||
{
|
||||
// Check if there are any other indexes for this file
|
||||
var remainingIndexes = await fileIndexService.GetByFileIdAsync(fileId);
|
||||
if (remainingIndexes.Count == 0)
|
||||
{
|
||||
// No other indexes exist, safe to delete the file
|
||||
var file = await db.Files.FirstOrDefaultAsync(f => f.Id == fileId.ToString());
|
||||
if (file != null)
|
||||
{
|
||||
db.Files.Remove(file);
|
||||
await db.SaveChangesAsync();
|
||||
logger.LogInformation("Deleted file {FileId} ({FileName}) as requested", fileId, fileName);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogWarning(ex, "Failed to delete file {FileId} while removing index", fileId);
|
||||
// Continue even if file deletion fails
|
||||
}
|
||||
|
||||
return Ok(new
|
||||
{
|
||||
Message = deleteFile
|
||||
? "File index and file data removed successfully"
|
||||
: "File index removed successfully",
|
||||
FileId = fileId,
|
||||
FileName = fileName,
|
||||
Path = filePath,
|
||||
FileDataDeleted = deleteFile
|
||||
});
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError(ex, "Failed to remove file index {IndexId} for account {AccountId}", indexId, accountId);
|
||||
return new ObjectResult(new ApiError
|
||||
{
|
||||
Code = "REMOVE_FAILED",
|
||||
Message = "Failed to remove file",
|
||||
Status = 500
|
||||
}) { StatusCode = 500 };
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Removes all file indexes in a specific path
|
||||
/// </summary>
|
||||
/// <param name="path">The path to clear</param>
|
||||
/// <param name="deleteFiles">Whether to also delete the actual file data</param>
|
||||
/// <returns>Success message with count of removed items</returns>
|
||||
[HttpDelete("clear-path")]
|
||||
public async Task<IActionResult> ClearPath([FromQuery] string path = "/", [FromQuery] bool deleteFiles = false)
|
||||
{
|
||||
if (HttpContext.Items["CurrentUser"] is not Account currentUser)
|
||||
return new ObjectResult(ApiError.Unauthorized()) { StatusCode = 401 };
|
||||
|
||||
var accountId = Guid.Parse(currentUser.Id);
|
||||
|
||||
try
|
||||
{
|
||||
var removedCount = await fileIndexService.RemoveByPathAsync(accountId, path);
|
||||
|
||||
if (!deleteFiles || removedCount <= 0)
|
||||
return Ok(new
|
||||
{
|
||||
Message = deleteFiles
|
||||
? $"Cleared {removedCount} file indexes from path and deleted orphaned files"
|
||||
: $"Cleared {removedCount} file indexes from path",
|
||||
Path = path,
|
||||
RemovedCount = removedCount,
|
||||
FilesDeleted = deleteFiles
|
||||
});
|
||||
// Get the files that were in this path and check if they have other indexes
|
||||
var filesInPath = await fileIndexService.GetByPathAsync(accountId, path);
|
||||
var fileIdsToCheck = filesInPath.Select(fi => fi.FileId).Distinct().ToList();
|
||||
|
||||
foreach (var fileId in fileIdsToCheck)
|
||||
{
|
||||
var remainingIndexes = await fileIndexService.GetByFileIdAsync(fileId);
|
||||
if (remainingIndexes.Count != 0) continue;
|
||||
// No other indexes exist, safe to delete the file
|
||||
var file = await db.Files.FirstOrDefaultAsync(f => f.Id == fileId.ToString());
|
||||
if (file == null) continue;
|
||||
db.Files.Remove(file);
|
||||
logger.LogInformation("Deleted orphaned file {FileId} after clearing path {Path}", fileId, path);
|
||||
}
|
||||
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
return Ok(new
|
||||
{
|
||||
Message = deleteFiles
|
||||
? $"Cleared {removedCount} file indexes from path and deleted orphaned files"
|
||||
: $"Cleared {removedCount} file indexes from path",
|
||||
Path = path,
|
||||
RemovedCount = removedCount,
|
||||
FilesDeleted = deleteFiles
|
||||
});
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError(ex, "Failed to clear path {Path} for account {AccountId}", path, accountId);
|
||||
return new ObjectResult(new ApiError
|
||||
{
|
||||
Code = "CLEAR_PATH_FAILED",
|
||||
Message = "Failed to clear path",
|
||||
Status = 500
|
||||
}) { StatusCode = 500 };
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new file index (useful for adding existing files to a path)
|
||||
/// </summary>
|
||||
/// <param name="request">The create index request</param>
|
||||
/// <returns>The created file index</returns>
|
||||
[HttpPost("create")]
|
||||
public async Task<IActionResult> CreateFileIndex([FromBody] CreateFileIndexRequest request)
|
||||
{
|
||||
if (HttpContext.Items["CurrentUser"] is not Account currentUser)
|
||||
return new ObjectResult(ApiError.Unauthorized()) { StatusCode = 401 };
|
||||
|
||||
var accountId = Guid.Parse(currentUser.Id);
|
||||
|
||||
try
|
||||
{
|
||||
// Verify the file exists and belongs to the user
|
||||
var file = await db.Files.FirstOrDefaultAsync(f => f.Id == request.FileId);
|
||||
if (file == null)
|
||||
return new ObjectResult(ApiError.NotFound("File")) { StatusCode = 404 };
|
||||
|
||||
if (file.AccountId != accountId)
|
||||
return new ObjectResult(ApiError.Unauthorized(forbidden: true)) { StatusCode = 403 };
|
||||
|
||||
// Check if index already exists for this file and path
|
||||
var existingIndex = await db.FileIndexes
|
||||
.FirstOrDefaultAsync(fi =>
|
||||
fi.FileId == request.FileId && fi.Path == request.Path && fi.AccountId == accountId);
|
||||
|
||||
if (existingIndex != null)
|
||||
return new ObjectResult(ApiError.Validation(new Dictionary<string, string[]>
|
||||
{
|
||||
{ "fileId", ["File index already exists for this path"] }
|
||||
})) { StatusCode = 400 };
|
||||
|
||||
var fileIndex = await fileIndexService.CreateAsync(request.Path, request.FileId, accountId);
|
||||
|
||||
return Ok(new
|
||||
{
|
||||
IndexId = fileIndex.Id,
|
||||
fileIndex.FileId,
|
||||
fileIndex.Path,
|
||||
Message = "File index created successfully"
|
||||
});
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError(ex, "Failed to create file index for file {FileId} at path {Path} for account {AccountId}",
|
||||
request.FileId, request.Path, accountId);
|
||||
return new ObjectResult(new ApiError
|
||||
{
|
||||
Code = "CREATE_INDEX_FAILED",
|
||||
Message = "Failed to create file index",
|
||||
Status = 500
|
||||
}) { StatusCode = 500 };
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Searches for files by name or metadata
|
||||
/// </summary>
|
||||
/// <param name="query">The search query</param>
|
||||
/// <param name="path">Optional path to limit search to</param>
|
||||
/// <returns>Matching files</returns>
|
||||
[HttpGet("search")]
|
||||
public async Task<IActionResult> SearchFiles([FromQuery] string query, [FromQuery] string? path = null)
|
||||
{
|
||||
if (HttpContext.Items["CurrentUser"] is not Account currentUser)
|
||||
return new ObjectResult(ApiError.Unauthorized()) { StatusCode = 401 };
|
||||
|
||||
var accountId = Guid.Parse(currentUser.Id);
|
||||
|
||||
try
|
||||
{
|
||||
// Build the query with all conditions at once
|
||||
var searchTerm = query.ToLower();
|
||||
var fileIndexes = await db.FileIndexes
|
||||
.Where(fi => fi.AccountId == accountId)
|
||||
.Include(fi => fi.File)
|
||||
.Where(fi =>
|
||||
(string.IsNullOrEmpty(path) || fi.Path == FileIndexService.NormalizePath(path)) &&
|
||||
(fi.File.Name.ToLower().Contains(searchTerm) ||
|
||||
(fi.File.Description != null && fi.File.Description.ToLower().Contains(searchTerm)) ||
|
||||
(fi.File.MimeType != null && fi.File.MimeType.ToLower().Contains(searchTerm))))
|
||||
.ToListAsync();
|
||||
|
||||
return Ok(new
|
||||
{
|
||||
Query = query,
|
||||
Path = path,
|
||||
Results = fileIndexes,
|
||||
TotalCount = fileIndexes.Count()
|
||||
});
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError(ex, "Failed to search files for account {AccountId} with query {Query}", accountId, query);
|
||||
return new ObjectResult(new ApiError
|
||||
{
|
||||
Code = "SEARCH_FAILED",
|
||||
Message = "Failed to search files",
|
||||
Status = 500
|
||||
}) { StatusCode = 500 };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public class MoveFileRequest
|
||||
{
|
||||
public string NewPath { get; set; } = null!;
|
||||
}
|
||||
|
||||
public class CreateFileIndexRequest
|
||||
{
|
||||
[MaxLength(32)] public string FileId { get; set; } = null!;
|
||||
public string Path { get; set; } = null!;
|
||||
}
|
||||
197
DysonNetwork.Drive/Index/FileIndexService.cs
Normal file
197
DysonNetwork.Drive/Index/FileIndexService.cs
Normal file
@@ -0,0 +1,197 @@
|
||||
using DysonNetwork.Shared.Models;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace DysonNetwork.Drive.Index;
|
||||
|
||||
public class FileIndexService(AppDatabase db)
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates a new file index entry
|
||||
/// </summary>
|
||||
/// <param name="path">The parent folder path with a trailing slash</param>
|
||||
/// <param name="fileId">The file ID</param>
|
||||
/// <param name="accountId">The account ID</param>
|
||||
/// <returns>The created file index</returns>
|
||||
public async Task<SnCloudFileIndex> CreateAsync(string path, string fileId, Guid accountId)
|
||||
{
|
||||
// Ensure a path has a trailing slash and is query-safe
|
||||
var normalizedPath = NormalizePath(path);
|
||||
|
||||
// Check if a file with the same name already exists in the same path for this account
|
||||
var existingFileIndex = await db.FileIndexes
|
||||
.FirstOrDefaultAsync(fi => fi.AccountId == accountId && fi.Path == normalizedPath && fi.FileId == fileId);
|
||||
|
||||
if (existingFileIndex != null)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"A file with ID '{fileId}' already exists in path '{normalizedPath}' for account '{accountId}'");
|
||||
}
|
||||
|
||||
var fileIndex = new SnCloudFileIndex
|
||||
{
|
||||
Path = normalizedPath,
|
||||
FileId = fileId,
|
||||
AccountId = accountId
|
||||
};
|
||||
|
||||
db.FileIndexes.Add(fileIndex);
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
return fileIndex;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates an existing file index entry by removing the old one and creating a new one
|
||||
/// </summary>
|
||||
/// <param name="id">The file index ID</param>
|
||||
/// <param name="newPath">The new parent folder path with trailing slash</param>
|
||||
/// <returns>The updated file index</returns>
|
||||
public async Task<SnCloudFileIndex?> UpdateAsync(Guid id, string newPath)
|
||||
{
|
||||
var fileIndex = await db.FileIndexes.FindAsync(id);
|
||||
if (fileIndex == null)
|
||||
return null;
|
||||
|
||||
// Since properties are init-only, we need to remove the old index and create a new one
|
||||
db.FileIndexes.Remove(fileIndex);
|
||||
|
||||
var newFileIndex = new SnCloudFileIndex
|
||||
{
|
||||
Path = NormalizePath(newPath),
|
||||
FileId = fileIndex.FileId,
|
||||
AccountId = fileIndex.AccountId
|
||||
};
|
||||
|
||||
db.FileIndexes.Add(newFileIndex);
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
return newFileIndex;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Removes a file index entry by ID
|
||||
/// </summary>
|
||||
/// <param name="id">The file index ID</param>
|
||||
/// <returns>True if the index was found and removed, false otherwise</returns>
|
||||
public async Task<bool> RemoveAsync(Guid id)
|
||||
{
|
||||
var fileIndex = await db.FileIndexes.FindAsync(id);
|
||||
if (fileIndex == null)
|
||||
return false;
|
||||
|
||||
db.FileIndexes.Remove(fileIndex);
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Removes file index entries by file ID
|
||||
/// </summary>
|
||||
/// <param name="fileId">The file ID</param>
|
||||
/// <returns>The number of indexes removed</returns>
|
||||
public async Task<int> RemoveByFileIdAsync(string fileId)
|
||||
{
|
||||
var indexes = await db.FileIndexes
|
||||
.Where(fi => fi.FileId == fileId)
|
||||
.ToListAsync();
|
||||
|
||||
if (indexes.Count == 0)
|
||||
return 0;
|
||||
|
||||
db.FileIndexes.RemoveRange(indexes);
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
return indexes.Count;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Removes file index entries by account ID and path
|
||||
/// </summary>
|
||||
/// <param name="accountId">The account ID</param>
|
||||
/// <param name="path">The parent folder path</param>
|
||||
/// <returns>The number of indexes removed</returns>
|
||||
public async Task<int> RemoveByPathAsync(Guid accountId, string path)
|
||||
{
|
||||
var normalizedPath = NormalizePath(path);
|
||||
|
||||
var indexes = await db.FileIndexes
|
||||
.Where(fi => fi.AccountId == accountId && fi.Path == normalizedPath)
|
||||
.ToListAsync();
|
||||
|
||||
if (!indexes.Any())
|
||||
return 0;
|
||||
|
||||
db.FileIndexes.RemoveRange(indexes);
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
return indexes.Count;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets file indexes by account ID and path
|
||||
/// </summary>
|
||||
/// <param name="accountId">The account ID</param>
|
||||
/// <param name="path">The parent folder path</param>
|
||||
/// <returns>List of file indexes</returns>
|
||||
public async Task<List<SnCloudFileIndex>> GetByPathAsync(Guid accountId, string path)
|
||||
{
|
||||
var normalizedPath = NormalizePath(path);
|
||||
|
||||
return await db.FileIndexes
|
||||
.Where(fi => fi.AccountId == accountId && fi.Path == normalizedPath)
|
||||
.Include(fi => fi.File)
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets file indexes by file ID
|
||||
/// </summary>
|
||||
/// <param name="fileId">The file ID</param>
|
||||
/// <returns>List of file indexes</returns>
|
||||
public async Task<List<SnCloudFileIndex>> GetByFileIdAsync(string fileId)
|
||||
{
|
||||
return await db.FileIndexes
|
||||
.Where(fi => fi.FileId == fileId)
|
||||
.Include(fi => fi.File)
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets all file indexes for an account
|
||||
/// </summary>
|
||||
/// <param name="accountId">The account ID</param>
|
||||
/// <returns>List of file indexes</returns>
|
||||
public async Task<List<SnCloudFileIndex>> GetByAccountIdAsync(Guid accountId)
|
||||
{
|
||||
return await db.FileIndexes
|
||||
.Where(fi => fi.AccountId == accountId)
|
||||
.Include(fi => fi.File)
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Normalizes the path to ensure it has a trailing slash and is query-safe
|
||||
/// </summary>
|
||||
/// <param name="path">The original path</param>
|
||||
/// <returns>The normalized path</returns>
|
||||
public static string NormalizePath(string path)
|
||||
{
|
||||
if (string.IsNullOrEmpty(path))
|
||||
return "/";
|
||||
|
||||
// Ensure the path starts with a slash
|
||||
if (!path.StartsWith('/'))
|
||||
path = "/" + path;
|
||||
|
||||
// Ensure the path ends with a slash (unless it's just the root)
|
||||
if (path != "/" && !path.EndsWith('/'))
|
||||
path += "/";
|
||||
|
||||
// Make path query-safe by removing problematic characters
|
||||
// This is a basic implementation - you might want to add more robust validation
|
||||
path = path.Replace("%", "").Replace("'", "").Replace("\"", "");
|
||||
|
||||
return path;
|
||||
}
|
||||
}
|
||||
341
DysonNetwork.Drive/Index/README.md
Normal file
341
DysonNetwork.Drive/Index/README.md
Normal file
@@ -0,0 +1,341 @@
|
||||
# File Indexing System Documentation
|
||||
|
||||
## Overview
|
||||
|
||||
The File Indexing System provides a hierarchical file organization layer on top of the existing file storage system in DysonNetwork Drive. It allows users to organize their files in folders and paths while maintaining the underlying file storage capabilities.
|
||||
|
||||
When using with the gateway, replace the `/api` with the `/drive` in the path.
|
||||
And all the arguments will be transformed into snake case via the gateway.
|
||||
|
||||
## Architecture
|
||||
|
||||
### Core Components
|
||||
|
||||
1. **SnCloudFileIndex Model** - Represents the file-to-path mapping
|
||||
2. **FileIndexService** - Business logic for file index operations
|
||||
3. **FileIndexController** - REST API endpoints for file management
|
||||
4. **FileUploadController Integration** - Automatic index creation during upload
|
||||
|
||||
### Database Schema
|
||||
|
||||
```sql
|
||||
-- File Indexes table
|
||||
CREATE TABLE "FileIndexes" (
|
||||
"Id" uuid NOT NULL DEFAULT gen_random_uuid(),
|
||||
"Path" character varying(8192) NOT NULL,
|
||||
"FileId" uuid NOT NULL,
|
||||
"AccountId" uuid NOT NULL,
|
||||
"CreatedAt" timestamp with time zone NOT NULL DEFAULT (now() at time zone 'utc'),
|
||||
"UpdatedAt" timestamp with time zone NOT NULL DEFAULT (now() at time zone 'utc'),
|
||||
CONSTRAINT "PK_FileIndexes" PRIMARY KEY ("Id"),
|
||||
CONSTRAINT "FK_FileIndexes_Files_FileId" FOREIGN KEY ("FileId") REFERENCES "Files" ("Id") ON DELETE CASCADE,
|
||||
INDEX "IX_FileIndexes_Path_AccountId" ("Path", "AccountId")
|
||||
);
|
||||
```
|
||||
|
||||
## API Endpoints
|
||||
|
||||
### Browse Files
|
||||
**GET** `/api/index/browse?path=/documents/`
|
||||
|
||||
Browse files in a specific path.
|
||||
|
||||
**Query Parameters:**
|
||||
- `path` (optional, default: "/") - The path to browse
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"path": "/documents/",
|
||||
"files": [
|
||||
{
|
||||
"id": "guid",
|
||||
"path": "/documents/",
|
||||
"fileId": "guid",
|
||||
"accountId": "guid",
|
||||
"createdAt": "2024-01-01T00:00:00Z",
|
||||
"updatedAt": "2024-01-01T00:00:00Z",
|
||||
"file": {
|
||||
"id": "string",
|
||||
"name": "document.pdf",
|
||||
"size": 1024,
|
||||
"mimeType": "application/pdf",
|
||||
"hash": "sha256-hash",
|
||||
"uploadedAt": "2024-01-01T00:00:00Z",
|
||||
"expiredAt": null,
|
||||
"hasCompression": false,
|
||||
"hasThumbnail": true,
|
||||
"isEncrypted": false,
|
||||
"description": null
|
||||
}
|
||||
}
|
||||
],
|
||||
"totalCount": 1
|
||||
}
|
||||
```
|
||||
|
||||
### Get All Files
|
||||
**GET** `/api/index/all`
|
||||
|
||||
Get all files for the current user across all paths.
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"files": [
|
||||
// Same structure as browse endpoint
|
||||
],
|
||||
"totalCount": 10
|
||||
}
|
||||
```
|
||||
|
||||
### Move File
|
||||
**POST** `/api/index/move/{indexId}`
|
||||
|
||||
Move a file to a new path.
|
||||
|
||||
**Path Parameters:**
|
||||
- `indexId` - The file index ID
|
||||
|
||||
**Request Body:**
|
||||
```json
|
||||
{
|
||||
"newPath": "/archived/"
|
||||
}
|
||||
```
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"fileId": "guid",
|
||||
"indexId": "guid",
|
||||
"oldPath": "/documents/",
|
||||
"newPath": "/archived/",
|
||||
"message": "File moved successfully"
|
||||
}
|
||||
```
|
||||
|
||||
### Remove File Index
|
||||
**DELETE** `/api/index/remove/{indexId}?deleteFile=false`
|
||||
|
||||
Remove a file index. Optionally delete the actual file data.
|
||||
|
||||
**Path Parameters:**
|
||||
- `indexId` - The file index ID
|
||||
|
||||
**Query Parameters:**
|
||||
- `deleteFile` (optional, default: false) - Whether to also delete the file data
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"message": "File index removed successfully",
|
||||
"fileId": "guid",
|
||||
"fileName": "document.pdf",
|
||||
"path": "/documents/",
|
||||
"fileDataDeleted": false
|
||||
}
|
||||
```
|
||||
|
||||
### Clear Path
|
||||
**DELETE** `/api/index/clear-path?path=/temp/&deleteFiles=false`
|
||||
|
||||
Remove all file indexes in a specific path.
|
||||
|
||||
**Query Parameters:**
|
||||
- `path` (optional, default: "/") - The path to clear
|
||||
- `deleteFiles` (optional, default: false) - Whether to also delete orphaned files
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"message": "Cleared 5 file indexes from path",
|
||||
"path": "/temp/",
|
||||
"removedCount": 5,
|
||||
"filesDeleted": false
|
||||
}
|
||||
```
|
||||
|
||||
### Create File Index
|
||||
**POST** `/api/index/create`
|
||||
|
||||
Create a new file index for an existing file.
|
||||
|
||||
**Request Body:**
|
||||
```json
|
||||
{
|
||||
"fileId": "guid",
|
||||
"path": "/documents/"
|
||||
}
|
||||
```
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"indexId": "guid",
|
||||
"fileId": "guid",
|
||||
"path": "/documents/",
|
||||
"message": "File index created successfully"
|
||||
}
|
||||
```
|
||||
|
||||
### Search Files
|
||||
**GET** `/api/index/search?query=report&path=/documents/`
|
||||
|
||||
Search for files by name or metadata.
|
||||
|
||||
**Query Parameters:**
|
||||
- `query` (required) - The search query
|
||||
- `path` (optional) - Limit search to specific path
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"query": "report",
|
||||
"path": "/documents/",
|
||||
"results": [
|
||||
// Same structure as browse endpoint
|
||||
],
|
||||
"totalCount": 3
|
||||
}
|
||||
```
|
||||
|
||||
## Path Normalization
|
||||
|
||||
The system automatically normalizes paths to ensure consistency:
|
||||
|
||||
- **Trailing Slash**: All paths end with `/`
|
||||
- **Root Path**: User home folder is represented as `/`
|
||||
- **Query Safety**: Paths are validated to avoid SQL injection
|
||||
- **Examples**:
|
||||
- `/documents/` ✅ (correct)
|
||||
- `/documents` → `/documents/` ✅ (normalized)
|
||||
- `/documents/reports/` ✅ (correct)
|
||||
- `/documents/reports` → `/documents/reports/` ✅ (normalized)
|
||||
|
||||
## File Upload Integration
|
||||
|
||||
When uploading files with the `FileUploadController`, you can specify a path to automatically create file indexes:
|
||||
|
||||
**Create Upload Task Request:**
|
||||
```json
|
||||
{
|
||||
"fileName": "document.pdf",
|
||||
"fileSize": 1024,
|
||||
"contentType": "application/pdf",
|
||||
"hash": "sha256-hash",
|
||||
"path": "/documents/" // New field for file indexing
|
||||
}
|
||||
```
|
||||
|
||||
The system will automatically create a file index when the upload completes successfully.
|
||||
|
||||
## Service Methods
|
||||
|
||||
### FileIndexService
|
||||
|
||||
```csharp
|
||||
public class FileIndexService
|
||||
{
|
||||
// Create a new file index
|
||||
Task<SnCloudFileIndex> CreateAsync(string path, Guid fileId, Guid accountId);
|
||||
|
||||
// Get files by path
|
||||
Task<List<SnCloudFileIndex>> GetByPathAsync(Guid accountId, string path);
|
||||
|
||||
// Get all files for account
|
||||
Task<List<SnCloudFileIndex>> GetByAccountIdAsync(Guid accountId);
|
||||
|
||||
// Get indexes for specific file
|
||||
Task<List<SnCloudFileIndex>> GetByFileIdAsync(Guid fileId);
|
||||
|
||||
// Move file to new path
|
||||
Task<SnCloudFileIndex?> UpdateAsync(Guid indexId, string newPath);
|
||||
|
||||
// Remove file index
|
||||
Task<bool> RemoveAsync(Guid indexId);
|
||||
|
||||
// Remove all indexes in path
|
||||
Task<int> RemoveByPathAsync(Guid accountId, string path);
|
||||
|
||||
// Normalize path format
|
||||
public static string NormalizePath(string path);
|
||||
}
|
||||
```
|
||||
|
||||
## Error Handling
|
||||
|
||||
The API returns appropriate HTTP status codes and error messages:
|
||||
|
||||
- **400 Bad Request**: Invalid input parameters
|
||||
- **401 Unauthorized**: User not authenticated
|
||||
- **403 Forbidden**: User lacks permission
|
||||
- **404 Not Found**: Resource not found
|
||||
- **500 Internal Server Error**: Server-side error
|
||||
|
||||
**Error Response Format:**
|
||||
```json
|
||||
{
|
||||
"code": "BROWSE_FAILED",
|
||||
"message": "Failed to browse files",
|
||||
"status": 500
|
||||
}
|
||||
```
|
||||
|
||||
## Security Considerations
|
||||
|
||||
1. **Ownership Verification**: All operations verify that the user owns the file indexes
|
||||
2. **Path Validation**: Paths are normalized and validated
|
||||
3. **Cascade Deletion**: File indexes are automatically removed when files are deleted
|
||||
4. **Safe File Deletion**: Files are only deleted when no other indexes reference them
|
||||
|
||||
## Usage Examples
|
||||
|
||||
### Upload File to Specific Path
|
||||
```bash
|
||||
# Create upload task with path
|
||||
curl -X POST /api/files/upload/create \
|
||||
-H "Authorization: Bearer {token}" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"fileName": "report.pdf",
|
||||
"fileSize": 2048,
|
||||
"contentType": "application/pdf",
|
||||
"path": "/documents/reports/"
|
||||
}'
|
||||
```
|
||||
|
||||
### Browse Files
|
||||
```bash
|
||||
curl -X GET "/api/index/browse?path=/documents/reports/" \
|
||||
-H "Authorization: Bearer {token}"
|
||||
```
|
||||
|
||||
### Move File
|
||||
```bash
|
||||
curl -X POST "/api/index/move/{indexId}" \
|
||||
-H "Authorization: Bearer {token}" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"newPath": "/archived/"}'
|
||||
```
|
||||
|
||||
### Search Files
|
||||
```bash
|
||||
curl -X GET "/api/index/search?query=invoice&path=/documents/" \
|
||||
-H "Authorization: Bearer {token}"
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Use Trailing Slashes**: Always include trailing slashes in paths
|
||||
2. **Organize Hierarchically**: Use meaningful folder structures
|
||||
3. **Search Efficiently**: Use the search endpoint instead of client-side filtering
|
||||
4. **Clean Up**: Use the clear-path endpoint for temporary directories
|
||||
5. **Monitor Usage**: Check total file counts for quota management
|
||||
|
||||
## Integration Notes
|
||||
|
||||
- The file indexing system works alongside the existing file storage
|
||||
- Files can exist in multiple paths (hard links)
|
||||
- File deletion is optional and only removes data when safe
|
||||
- The system maintains referential integrity between files and indexes
|
||||
@@ -3,7 +3,7 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using DysonNetwork.Drive;
|
||||
using DysonNetwork.Drive.Storage;
|
||||
using DysonNetwork.Shared.Data;
|
||||
using DysonNetwork.Shared.Models;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using DysonNetwork.Drive.Storage;
|
||||
using DysonNetwork.Shared.Data;
|
||||
using DysonNetwork.Shared.Models;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using NodaTime;
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using DysonNetwork.Drive;
|
||||
using DysonNetwork.Shared.Data;
|
||||
using DysonNetwork.Shared.Models;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
using System.Collections.Generic;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
|
||||
@@ -2,8 +2,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using DysonNetwork.Drive;
|
||||
using DysonNetwork.Drive.Storage;
|
||||
using DysonNetwork.Shared.Data;
|
||||
using DysonNetwork.Shared.Models;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using DysonNetwork.Drive.Storage;
|
||||
using DysonNetwork.Shared.Models;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using NodaTime;
|
||||
|
||||
|
||||
@@ -2,8 +2,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using DysonNetwork.Drive;
|
||||
using DysonNetwork.Drive.Storage;
|
||||
using DysonNetwork.Shared.Data;
|
||||
using DysonNetwork.Shared.Models;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
@@ -2,8 +2,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using DysonNetwork.Drive;
|
||||
using DysonNetwork.Drive.Storage;
|
||||
using DysonNetwork.Shared.Data;
|
||||
using DysonNetwork.Shared.Models;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
@@ -2,8 +2,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using DysonNetwork.Drive;
|
||||
using DysonNetwork.Drive.Storage;
|
||||
using DysonNetwork.Shared.Data;
|
||||
using DysonNetwork.Shared.Models;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using NodaTime;
|
||||
|
||||
#nullable disable
|
||||
|
||||
@@ -2,8 +2,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using DysonNetwork.Drive;
|
||||
using DysonNetwork.Drive.Storage;
|
||||
using DysonNetwork.Shared.Data;
|
||||
using DysonNetwork.Shared.Models;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using NodaTime;
|
||||
|
||||
#nullable disable
|
||||
|
||||
@@ -2,8 +2,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using DysonNetwork.Drive;
|
||||
using DysonNetwork.Drive.Storage;
|
||||
using DysonNetwork.Shared.Data;
|
||||
using DysonNetwork.Shared.Models;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
403
DysonNetwork.Drive/Migrations/20250819164302_RemoveUploadedTo.Designer.cs
generated
Normal file
403
DysonNetwork.Drive/Migrations/20250819164302_RemoveUploadedTo.Designer.cs
generated
Normal file
@@ -0,0 +1,403 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using DysonNetwork.Drive;
|
||||
using DysonNetwork.Shared.Models;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
using NodaTime;
|
||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace DysonNetwork.Drive.Migrations
|
||||
{
|
||||
[DbContext(typeof(AppDatabase))]
|
||||
[Migration("20250819164302_RemoveUploadedTo")]
|
||||
partial class RemoveUploadedTo
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "9.0.7")
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||
|
||||
NpgsqlModelBuilderExtensions.HasPostgresExtension(modelBuilder, "postgis");
|
||||
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||
|
||||
modelBuilder.Entity("DysonNetwork.Drive.Billing.QuotaRecord", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<Guid>("AccountId")
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("account_id");
|
||||
|
||||
b.Property<Instant>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("created_at");
|
||||
|
||||
b.Property<Instant?>("DeletedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("deleted_at");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.IsRequired()
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("description");
|
||||
|
||||
b.Property<Instant?>("ExpiredAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("expired_at");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("name");
|
||||
|
||||
b.Property<long>("Quota")
|
||||
.HasColumnType("bigint")
|
||||
.HasColumnName("quota");
|
||||
|
||||
b.Property<Instant>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("updated_at");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_quota_records");
|
||||
|
||||
b.ToTable("quota_records", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DysonNetwork.Drive.Storage.CloudFile", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.HasMaxLength(32)
|
||||
.HasColumnType("character varying(32)")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<Guid>("AccountId")
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("account_id");
|
||||
|
||||
b.Property<Guid?>("BundleId")
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("bundle_id");
|
||||
|
||||
b.Property<Instant>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("created_at");
|
||||
|
||||
b.Property<Instant?>("DeletedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("deleted_at");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.HasMaxLength(4096)
|
||||
.HasColumnType("character varying(4096)")
|
||||
.HasColumnName("description");
|
||||
|
||||
b.Property<Instant?>("ExpiredAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("expired_at");
|
||||
|
||||
b.Property<Dictionary<string, object>>("FileMeta")
|
||||
.HasColumnType("jsonb")
|
||||
.HasColumnName("file_meta");
|
||||
|
||||
b.Property<bool>("HasCompression")
|
||||
.HasColumnType("boolean")
|
||||
.HasColumnName("has_compression");
|
||||
|
||||
b.Property<bool>("HasThumbnail")
|
||||
.HasColumnType("boolean")
|
||||
.HasColumnName("has_thumbnail");
|
||||
|
||||
b.Property<string>("Hash")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("character varying(256)")
|
||||
.HasColumnName("hash");
|
||||
|
||||
b.Property<bool>("IsEncrypted")
|
||||
.HasColumnType("boolean")
|
||||
.HasColumnName("is_encrypted");
|
||||
|
||||
b.Property<bool>("IsMarkedRecycle")
|
||||
.HasColumnType("boolean")
|
||||
.HasColumnName("is_marked_recycle");
|
||||
|
||||
b.Property<string>("MimeType")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("character varying(256)")
|
||||
.HasColumnName("mime_type");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(1024)
|
||||
.HasColumnType("character varying(1024)")
|
||||
.HasColumnName("name");
|
||||
|
||||
b.Property<Guid?>("PoolId")
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("pool_id");
|
||||
|
||||
b.Property<List<ContentSensitiveMark>>("SensitiveMarks")
|
||||
.HasColumnType("jsonb")
|
||||
.HasColumnName("sensitive_marks");
|
||||
|
||||
b.Property<long>("Size")
|
||||
.HasColumnType("bigint")
|
||||
.HasColumnName("size");
|
||||
|
||||
b.Property<string>("StorageId")
|
||||
.HasMaxLength(32)
|
||||
.HasColumnType("character varying(32)")
|
||||
.HasColumnName("storage_id");
|
||||
|
||||
b.Property<string>("StorageUrl")
|
||||
.HasMaxLength(4096)
|
||||
.HasColumnType("character varying(4096)")
|
||||
.HasColumnName("storage_url");
|
||||
|
||||
b.Property<Instant>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("updated_at");
|
||||
|
||||
b.Property<Instant?>("UploadedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("uploaded_at");
|
||||
|
||||
b.Property<Dictionary<string, object>>("UserMeta")
|
||||
.HasColumnType("jsonb")
|
||||
.HasColumnName("user_meta");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_files");
|
||||
|
||||
b.HasIndex("BundleId")
|
||||
.HasDatabaseName("ix_files_bundle_id");
|
||||
|
||||
b.HasIndex("PoolId")
|
||||
.HasDatabaseName("ix_files_pool_id");
|
||||
|
||||
b.ToTable("files", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DysonNetwork.Drive.Storage.CloudFileReference", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<Instant>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("created_at");
|
||||
|
||||
b.Property<Instant?>("DeletedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("deleted_at");
|
||||
|
||||
b.Property<Instant?>("ExpiredAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("expired_at");
|
||||
|
||||
b.Property<string>("FileId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(32)
|
||||
.HasColumnType("character varying(32)")
|
||||
.HasColumnName("file_id");
|
||||
|
||||
b.Property<string>("ResourceId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(1024)
|
||||
.HasColumnType("character varying(1024)")
|
||||
.HasColumnName("resource_id");
|
||||
|
||||
b.Property<Instant>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("updated_at");
|
||||
|
||||
b.Property<string>("Usage")
|
||||
.IsRequired()
|
||||
.HasMaxLength(1024)
|
||||
.HasColumnType("character varying(1024)")
|
||||
.HasColumnName("usage");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_file_references");
|
||||
|
||||
b.HasIndex("FileId")
|
||||
.HasDatabaseName("ix_file_references_file_id");
|
||||
|
||||
b.ToTable("file_references", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DysonNetwork.Drive.Storage.FileBundle", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<Guid>("AccountId")
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("account_id");
|
||||
|
||||
b.Property<Instant>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("created_at");
|
||||
|
||||
b.Property<Instant?>("DeletedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("deleted_at");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.HasMaxLength(8192)
|
||||
.HasColumnType("character varying(8192)")
|
||||
.HasColumnName("description");
|
||||
|
||||
b.Property<Instant?>("ExpiredAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("expired_at");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(1024)
|
||||
.HasColumnType("character varying(1024)")
|
||||
.HasColumnName("name");
|
||||
|
||||
b.Property<string>("Passcode")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("character varying(256)")
|
||||
.HasColumnName("passcode");
|
||||
|
||||
b.Property<string>("Slug")
|
||||
.IsRequired()
|
||||
.HasMaxLength(1024)
|
||||
.HasColumnType("character varying(1024)")
|
||||
.HasColumnName("slug");
|
||||
|
||||
b.Property<Instant>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("updated_at");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_bundles");
|
||||
|
||||
b.HasIndex("Slug")
|
||||
.IsUnique()
|
||||
.HasDatabaseName("ix_bundles_slug");
|
||||
|
||||
b.ToTable("bundles", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DysonNetwork.Drive.Storage.FilePool", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<Guid?>("AccountId")
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("account_id");
|
||||
|
||||
b.Property<BillingConfig>("BillingConfig")
|
||||
.IsRequired()
|
||||
.HasColumnType("jsonb")
|
||||
.HasColumnName("billing_config");
|
||||
|
||||
b.Property<Instant>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("created_at");
|
||||
|
||||
b.Property<Instant?>("DeletedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("deleted_at");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.IsRequired()
|
||||
.HasMaxLength(8192)
|
||||
.HasColumnType("character varying(8192)")
|
||||
.HasColumnName("description");
|
||||
|
||||
b.Property<bool>("IsHidden")
|
||||
.HasColumnType("boolean")
|
||||
.HasColumnName("is_hidden");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(1024)
|
||||
.HasColumnType("character varying(1024)")
|
||||
.HasColumnName("name");
|
||||
|
||||
b.Property<PolicyConfig>("PolicyConfig")
|
||||
.IsRequired()
|
||||
.HasColumnType("jsonb")
|
||||
.HasColumnName("policy_config");
|
||||
|
||||
b.Property<RemoteStorageConfig>("StorageConfig")
|
||||
.IsRequired()
|
||||
.HasColumnType("jsonb")
|
||||
.HasColumnName("storage_config");
|
||||
|
||||
b.Property<Instant>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("updated_at");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_pools");
|
||||
|
||||
b.ToTable("pools", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DysonNetwork.Drive.Storage.CloudFile", b =>
|
||||
{
|
||||
b.HasOne("DysonNetwork.Drive.Storage.FileBundle", "Bundle")
|
||||
.WithMany("Files")
|
||||
.HasForeignKey("BundleId")
|
||||
.HasConstraintName("fk_files_bundles_bundle_id");
|
||||
|
||||
b.HasOne("DysonNetwork.Drive.Storage.FilePool", "Pool")
|
||||
.WithMany()
|
||||
.HasForeignKey("PoolId")
|
||||
.HasConstraintName("fk_files_pools_pool_id");
|
||||
|
||||
b.Navigation("Bundle");
|
||||
|
||||
b.Navigation("Pool");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DysonNetwork.Drive.Storage.CloudFileReference", b =>
|
||||
{
|
||||
b.HasOne("DysonNetwork.Drive.Storage.CloudFile", "File")
|
||||
.WithMany("References")
|
||||
.HasForeignKey("FileId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired()
|
||||
.HasConstraintName("fk_file_references_files_file_id");
|
||||
|
||||
b.Navigation("File");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DysonNetwork.Drive.Storage.CloudFile", b =>
|
||||
{
|
||||
b.Navigation("References");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DysonNetwork.Drive.Storage.FileBundle", b =>
|
||||
{
|
||||
b.Navigation("Files");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace DysonNetwork.Drive.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class RemoveUploadedTo : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "uploaded_to",
|
||||
table: "files");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "uploaded_to",
|
||||
table: "files",
|
||||
type: "character varying(128)",
|
||||
maxLength: 128,
|
||||
nullable: true);
|
||||
}
|
||||
}
|
||||
}
|
||||
402
DysonNetwork.Drive/Migrations/20250907070034_RemoveNetTopo.Designer.cs
generated
Normal file
402
DysonNetwork.Drive/Migrations/20250907070034_RemoveNetTopo.Designer.cs
generated
Normal file
@@ -0,0 +1,402 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using DysonNetwork.Drive;
|
||||
using DysonNetwork.Shared.Models;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
using NodaTime;
|
||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace DysonNetwork.Drive.Migrations
|
||||
{
|
||||
[DbContext(typeof(AppDatabase))]
|
||||
[Migration("20250907070034_RemoveNetTopo")]
|
||||
partial class RemoveNetTopo
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "9.0.7")
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||
|
||||
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||
|
||||
modelBuilder.Entity("DysonNetwork.Drive.Billing.QuotaRecord", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<Guid>("AccountId")
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("account_id");
|
||||
|
||||
b.Property<Instant>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("created_at");
|
||||
|
||||
b.Property<Instant?>("DeletedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("deleted_at");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.IsRequired()
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("description");
|
||||
|
||||
b.Property<Instant?>("ExpiredAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("expired_at");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("name");
|
||||
|
||||
b.Property<long>("Quota")
|
||||
.HasColumnType("bigint")
|
||||
.HasColumnName("quota");
|
||||
|
||||
b.Property<Instant>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("updated_at");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_quota_records");
|
||||
|
||||
b.ToTable("quota_records", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DysonNetwork.Drive.Storage.CloudFile", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.HasMaxLength(32)
|
||||
.HasColumnType("character varying(32)")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<Guid>("AccountId")
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("account_id");
|
||||
|
||||
b.Property<Guid?>("BundleId")
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("bundle_id");
|
||||
|
||||
b.Property<Instant>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("created_at");
|
||||
|
||||
b.Property<Instant?>("DeletedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("deleted_at");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.HasMaxLength(4096)
|
||||
.HasColumnType("character varying(4096)")
|
||||
.HasColumnName("description");
|
||||
|
||||
b.Property<Instant?>("ExpiredAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("expired_at");
|
||||
|
||||
b.Property<Dictionary<string, object>>("FileMeta")
|
||||
.HasColumnType("jsonb")
|
||||
.HasColumnName("file_meta");
|
||||
|
||||
b.Property<bool>("HasCompression")
|
||||
.HasColumnType("boolean")
|
||||
.HasColumnName("has_compression");
|
||||
|
||||
b.Property<bool>("HasThumbnail")
|
||||
.HasColumnType("boolean")
|
||||
.HasColumnName("has_thumbnail");
|
||||
|
||||
b.Property<string>("Hash")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("character varying(256)")
|
||||
.HasColumnName("hash");
|
||||
|
||||
b.Property<bool>("IsEncrypted")
|
||||
.HasColumnType("boolean")
|
||||
.HasColumnName("is_encrypted");
|
||||
|
||||
b.Property<bool>("IsMarkedRecycle")
|
||||
.HasColumnType("boolean")
|
||||
.HasColumnName("is_marked_recycle");
|
||||
|
||||
b.Property<string>("MimeType")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("character varying(256)")
|
||||
.HasColumnName("mime_type");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(1024)
|
||||
.HasColumnType("character varying(1024)")
|
||||
.HasColumnName("name");
|
||||
|
||||
b.Property<Guid?>("PoolId")
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("pool_id");
|
||||
|
||||
b.Property<List<ContentSensitiveMark>>("SensitiveMarks")
|
||||
.HasColumnType("jsonb")
|
||||
.HasColumnName("sensitive_marks");
|
||||
|
||||
b.Property<long>("Size")
|
||||
.HasColumnType("bigint")
|
||||
.HasColumnName("size");
|
||||
|
||||
b.Property<string>("StorageId")
|
||||
.HasMaxLength(32)
|
||||
.HasColumnType("character varying(32)")
|
||||
.HasColumnName("storage_id");
|
||||
|
||||
b.Property<string>("StorageUrl")
|
||||
.HasMaxLength(4096)
|
||||
.HasColumnType("character varying(4096)")
|
||||
.HasColumnName("storage_url");
|
||||
|
||||
b.Property<Instant>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("updated_at");
|
||||
|
||||
b.Property<Instant?>("UploadedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("uploaded_at");
|
||||
|
||||
b.Property<Dictionary<string, object>>("UserMeta")
|
||||
.HasColumnType("jsonb")
|
||||
.HasColumnName("user_meta");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_files");
|
||||
|
||||
b.HasIndex("BundleId")
|
||||
.HasDatabaseName("ix_files_bundle_id");
|
||||
|
||||
b.HasIndex("PoolId")
|
||||
.HasDatabaseName("ix_files_pool_id");
|
||||
|
||||
b.ToTable("files", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DysonNetwork.Drive.Storage.CloudFileReference", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<Instant>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("created_at");
|
||||
|
||||
b.Property<Instant?>("DeletedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("deleted_at");
|
||||
|
||||
b.Property<Instant?>("ExpiredAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("expired_at");
|
||||
|
||||
b.Property<string>("FileId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(32)
|
||||
.HasColumnType("character varying(32)")
|
||||
.HasColumnName("file_id");
|
||||
|
||||
b.Property<string>("ResourceId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(1024)
|
||||
.HasColumnType("character varying(1024)")
|
||||
.HasColumnName("resource_id");
|
||||
|
||||
b.Property<Instant>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("updated_at");
|
||||
|
||||
b.Property<string>("Usage")
|
||||
.IsRequired()
|
||||
.HasMaxLength(1024)
|
||||
.HasColumnType("character varying(1024)")
|
||||
.HasColumnName("usage");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_file_references");
|
||||
|
||||
b.HasIndex("FileId")
|
||||
.HasDatabaseName("ix_file_references_file_id");
|
||||
|
||||
b.ToTable("file_references", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DysonNetwork.Drive.Storage.FileBundle", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<Guid>("AccountId")
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("account_id");
|
||||
|
||||
b.Property<Instant>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("created_at");
|
||||
|
||||
b.Property<Instant?>("DeletedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("deleted_at");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.HasMaxLength(8192)
|
||||
.HasColumnType("character varying(8192)")
|
||||
.HasColumnName("description");
|
||||
|
||||
b.Property<Instant?>("ExpiredAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("expired_at");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(1024)
|
||||
.HasColumnType("character varying(1024)")
|
||||
.HasColumnName("name");
|
||||
|
||||
b.Property<string>("Passcode")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("character varying(256)")
|
||||
.HasColumnName("passcode");
|
||||
|
||||
b.Property<string>("Slug")
|
||||
.IsRequired()
|
||||
.HasMaxLength(1024)
|
||||
.HasColumnType("character varying(1024)")
|
||||
.HasColumnName("slug");
|
||||
|
||||
b.Property<Instant>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("updated_at");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_bundles");
|
||||
|
||||
b.HasIndex("Slug")
|
||||
.IsUnique()
|
||||
.HasDatabaseName("ix_bundles_slug");
|
||||
|
||||
b.ToTable("bundles", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DysonNetwork.Drive.Storage.FilePool", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<Guid?>("AccountId")
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("account_id");
|
||||
|
||||
b.Property<BillingConfig>("BillingConfig")
|
||||
.IsRequired()
|
||||
.HasColumnType("jsonb")
|
||||
.HasColumnName("billing_config");
|
||||
|
||||
b.Property<Instant>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("created_at");
|
||||
|
||||
b.Property<Instant?>("DeletedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("deleted_at");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.IsRequired()
|
||||
.HasMaxLength(8192)
|
||||
.HasColumnType("character varying(8192)")
|
||||
.HasColumnName("description");
|
||||
|
||||
b.Property<bool>("IsHidden")
|
||||
.HasColumnType("boolean")
|
||||
.HasColumnName("is_hidden");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(1024)
|
||||
.HasColumnType("character varying(1024)")
|
||||
.HasColumnName("name");
|
||||
|
||||
b.Property<PolicyConfig>("PolicyConfig")
|
||||
.IsRequired()
|
||||
.HasColumnType("jsonb")
|
||||
.HasColumnName("policy_config");
|
||||
|
||||
b.Property<RemoteStorageConfig>("StorageConfig")
|
||||
.IsRequired()
|
||||
.HasColumnType("jsonb")
|
||||
.HasColumnName("storage_config");
|
||||
|
||||
b.Property<Instant>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("updated_at");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_pools");
|
||||
|
||||
b.ToTable("pools", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DysonNetwork.Drive.Storage.CloudFile", b =>
|
||||
{
|
||||
b.HasOne("DysonNetwork.Drive.Storage.FileBundle", "Bundle")
|
||||
.WithMany("Files")
|
||||
.HasForeignKey("BundleId")
|
||||
.HasConstraintName("fk_files_bundles_bundle_id");
|
||||
|
||||
b.HasOne("DysonNetwork.Drive.Storage.FilePool", "Pool")
|
||||
.WithMany()
|
||||
.HasForeignKey("PoolId")
|
||||
.HasConstraintName("fk_files_pools_pool_id");
|
||||
|
||||
b.Navigation("Bundle");
|
||||
|
||||
b.Navigation("Pool");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DysonNetwork.Drive.Storage.CloudFileReference", b =>
|
||||
{
|
||||
b.HasOne("DysonNetwork.Drive.Storage.CloudFile", "File")
|
||||
.WithMany("References")
|
||||
.HasForeignKey("FileId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired()
|
||||
.HasConstraintName("fk_file_references_files_file_id");
|
||||
|
||||
b.Navigation("File");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DysonNetwork.Drive.Storage.CloudFile", b =>
|
||||
{
|
||||
b.Navigation("References");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DysonNetwork.Drive.Storage.FileBundle", b =>
|
||||
{
|
||||
b.Navigation("Files");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace DysonNetwork.Drive.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class RemoveNetTopo : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AlterDatabase()
|
||||
.OldAnnotation("Npgsql:PostgresExtension:postgis", ",,");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AlterDatabase()
|
||||
.Annotation("Npgsql:PostgresExtension:postgis", ",,");
|
||||
}
|
||||
}
|
||||
}
|
||||
567
DysonNetwork.Drive/Migrations/20251108191230_AddPersistentTask.Designer.cs
generated
Normal file
567
DysonNetwork.Drive/Migrations/20251108191230_AddPersistentTask.Designer.cs
generated
Normal file
@@ -0,0 +1,567 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using DysonNetwork.Drive;
|
||||
using DysonNetwork.Shared.Models;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
using NodaTime;
|
||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace DysonNetwork.Drive.Migrations
|
||||
{
|
||||
[DbContext(typeof(AppDatabase))]
|
||||
[Migration("20251108191230_AddPersistentTask")]
|
||||
partial class AddPersistentTask
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "9.0.10")
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||
|
||||
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||
|
||||
modelBuilder.Entity("DysonNetwork.Drive.Billing.QuotaRecord", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<Guid>("AccountId")
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("account_id");
|
||||
|
||||
b.Property<Instant>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("created_at");
|
||||
|
||||
b.Property<Instant?>("DeletedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("deleted_at");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.IsRequired()
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("description");
|
||||
|
||||
b.Property<Instant?>("ExpiredAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("expired_at");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("name");
|
||||
|
||||
b.Property<long>("Quota")
|
||||
.HasColumnType("bigint")
|
||||
.HasColumnName("quota");
|
||||
|
||||
b.Property<Instant>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("updated_at");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_quota_records");
|
||||
|
||||
b.ToTable("quota_records", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DysonNetwork.Drive.Storage.Model.PersistentTask", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<Guid>("AccountId")
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("account_id");
|
||||
|
||||
b.Property<Instant?>("CompletedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("completed_at");
|
||||
|
||||
b.Property<Instant>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("created_at");
|
||||
|
||||
b.Property<Instant?>("DeletedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("deleted_at");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.HasMaxLength(1024)
|
||||
.HasColumnType("character varying(1024)")
|
||||
.HasColumnName("description");
|
||||
|
||||
b.Property<string>("Discriminator")
|
||||
.IsRequired()
|
||||
.HasMaxLength(21)
|
||||
.HasColumnType("character varying(21)")
|
||||
.HasColumnName("discriminator");
|
||||
|
||||
b.Property<string>("ErrorMessage")
|
||||
.HasMaxLength(1024)
|
||||
.HasColumnType("character varying(1024)")
|
||||
.HasColumnName("error_message");
|
||||
|
||||
b.Property<long?>("EstimatedDurationSeconds")
|
||||
.HasColumnType("bigint")
|
||||
.HasColumnName("estimated_duration_seconds");
|
||||
|
||||
b.Property<Instant?>("ExpiredAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("expired_at");
|
||||
|
||||
b.Property<Instant>("LastActivity")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("last_activity");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("character varying(256)")
|
||||
.HasColumnName("name");
|
||||
|
||||
b.Property<Dictionary<string, object>>("Parameters")
|
||||
.IsRequired()
|
||||
.HasColumnType("jsonb")
|
||||
.HasColumnName("parameters");
|
||||
|
||||
b.Property<int>("Priority")
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("priority");
|
||||
|
||||
b.Property<double>("Progress")
|
||||
.HasColumnType("double precision")
|
||||
.HasColumnName("progress");
|
||||
|
||||
b.Property<Dictionary<string, object>>("Results")
|
||||
.IsRequired()
|
||||
.HasColumnType("jsonb")
|
||||
.HasColumnName("results");
|
||||
|
||||
b.Property<Instant?>("StartedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("started_at");
|
||||
|
||||
b.Property<int>("Status")
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("status");
|
||||
|
||||
b.Property<string>("TaskId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("character varying(64)")
|
||||
.HasColumnName("task_id");
|
||||
|
||||
b.Property<int>("Type")
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("type");
|
||||
|
||||
b.Property<Instant>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("updated_at");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_tasks");
|
||||
|
||||
b.ToTable("tasks", (string)null);
|
||||
|
||||
b.HasDiscriminator().HasValue("PersistentTask");
|
||||
|
||||
b.UseTphMappingStrategy();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DysonNetwork.Shared.Models.CloudFileReference", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<Instant>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("created_at");
|
||||
|
||||
b.Property<Instant?>("DeletedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("deleted_at");
|
||||
|
||||
b.Property<Instant?>("ExpiredAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("expired_at");
|
||||
|
||||
b.Property<string>("FileId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(32)
|
||||
.HasColumnType("character varying(32)")
|
||||
.HasColumnName("file_id");
|
||||
|
||||
b.Property<string>("ResourceId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(1024)
|
||||
.HasColumnType("character varying(1024)")
|
||||
.HasColumnName("resource_id");
|
||||
|
||||
b.Property<Instant>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("updated_at");
|
||||
|
||||
b.Property<string>("Usage")
|
||||
.IsRequired()
|
||||
.HasMaxLength(1024)
|
||||
.HasColumnType("character varying(1024)")
|
||||
.HasColumnName("usage");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_file_references");
|
||||
|
||||
b.HasIndex("FileId")
|
||||
.HasDatabaseName("ix_file_references_file_id");
|
||||
|
||||
b.ToTable("file_references", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DysonNetwork.Shared.Models.FilePool", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<Guid?>("AccountId")
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("account_id");
|
||||
|
||||
b.Property<BillingConfig>("BillingConfig")
|
||||
.IsRequired()
|
||||
.HasColumnType("jsonb")
|
||||
.HasColumnName("billing_config");
|
||||
|
||||
b.Property<Instant>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("created_at");
|
||||
|
||||
b.Property<Instant?>("DeletedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("deleted_at");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.IsRequired()
|
||||
.HasMaxLength(8192)
|
||||
.HasColumnType("character varying(8192)")
|
||||
.HasColumnName("description");
|
||||
|
||||
b.Property<bool>("IsHidden")
|
||||
.HasColumnType("boolean")
|
||||
.HasColumnName("is_hidden");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(1024)
|
||||
.HasColumnType("character varying(1024)")
|
||||
.HasColumnName("name");
|
||||
|
||||
b.Property<PolicyConfig>("PolicyConfig")
|
||||
.IsRequired()
|
||||
.HasColumnType("jsonb")
|
||||
.HasColumnName("policy_config");
|
||||
|
||||
b.Property<RemoteStorageConfig>("StorageConfig")
|
||||
.IsRequired()
|
||||
.HasColumnType("jsonb")
|
||||
.HasColumnName("storage_config");
|
||||
|
||||
b.Property<Instant>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("updated_at");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_pools");
|
||||
|
||||
b.ToTable("pools", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DysonNetwork.Shared.Models.SnCloudFile", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.HasMaxLength(32)
|
||||
.HasColumnType("character varying(32)")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<Guid>("AccountId")
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("account_id");
|
||||
|
||||
b.Property<Guid?>("BundleId")
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("bundle_id");
|
||||
|
||||
b.Property<Instant>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("created_at");
|
||||
|
||||
b.Property<Instant?>("DeletedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("deleted_at");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.HasMaxLength(4096)
|
||||
.HasColumnType("character varying(4096)")
|
||||
.HasColumnName("description");
|
||||
|
||||
b.Property<Instant?>("ExpiredAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("expired_at");
|
||||
|
||||
b.Property<Dictionary<string, object>>("FileMeta")
|
||||
.HasColumnType("jsonb")
|
||||
.HasColumnName("file_meta");
|
||||
|
||||
b.Property<bool>("HasCompression")
|
||||
.HasColumnType("boolean")
|
||||
.HasColumnName("has_compression");
|
||||
|
||||
b.Property<bool>("HasThumbnail")
|
||||
.HasColumnType("boolean")
|
||||
.HasColumnName("has_thumbnail");
|
||||
|
||||
b.Property<string>("Hash")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("character varying(256)")
|
||||
.HasColumnName("hash");
|
||||
|
||||
b.Property<bool>("IsEncrypted")
|
||||
.HasColumnType("boolean")
|
||||
.HasColumnName("is_encrypted");
|
||||
|
||||
b.Property<bool>("IsMarkedRecycle")
|
||||
.HasColumnType("boolean")
|
||||
.HasColumnName("is_marked_recycle");
|
||||
|
||||
b.Property<string>("MimeType")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("character varying(256)")
|
||||
.HasColumnName("mime_type");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(1024)
|
||||
.HasColumnType("character varying(1024)")
|
||||
.HasColumnName("name");
|
||||
|
||||
b.Property<Guid?>("PoolId")
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("pool_id");
|
||||
|
||||
b.Property<List<ContentSensitiveMark>>("SensitiveMarks")
|
||||
.HasColumnType("jsonb")
|
||||
.HasColumnName("sensitive_marks");
|
||||
|
||||
b.Property<long>("Size")
|
||||
.HasColumnType("bigint")
|
||||
.HasColumnName("size");
|
||||
|
||||
b.Property<string>("StorageId")
|
||||
.HasMaxLength(32)
|
||||
.HasColumnType("character varying(32)")
|
||||
.HasColumnName("storage_id");
|
||||
|
||||
b.Property<string>("StorageUrl")
|
||||
.HasMaxLength(4096)
|
||||
.HasColumnType("character varying(4096)")
|
||||
.HasColumnName("storage_url");
|
||||
|
||||
b.Property<Instant>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("updated_at");
|
||||
|
||||
b.Property<Instant?>("UploadedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("uploaded_at");
|
||||
|
||||
b.Property<Dictionary<string, object>>("UserMeta")
|
||||
.HasColumnType("jsonb")
|
||||
.HasColumnName("user_meta");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_files");
|
||||
|
||||
b.HasIndex("BundleId")
|
||||
.HasDatabaseName("ix_files_bundle_id");
|
||||
|
||||
b.HasIndex("PoolId")
|
||||
.HasDatabaseName("ix_files_pool_id");
|
||||
|
||||
b.ToTable("files", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DysonNetwork.Shared.Models.SnFileBundle", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<Guid>("AccountId")
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("account_id");
|
||||
|
||||
b.Property<Instant>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("created_at");
|
||||
|
||||
b.Property<Instant?>("DeletedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("deleted_at");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.HasMaxLength(8192)
|
||||
.HasColumnType("character varying(8192)")
|
||||
.HasColumnName("description");
|
||||
|
||||
b.Property<Instant?>("ExpiredAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("expired_at");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(1024)
|
||||
.HasColumnType("character varying(1024)")
|
||||
.HasColumnName("name");
|
||||
|
||||
b.Property<string>("Passcode")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("character varying(256)")
|
||||
.HasColumnName("passcode");
|
||||
|
||||
b.Property<string>("Slug")
|
||||
.IsRequired()
|
||||
.HasMaxLength(1024)
|
||||
.HasColumnType("character varying(1024)")
|
||||
.HasColumnName("slug");
|
||||
|
||||
b.Property<Instant>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("updated_at");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_bundles");
|
||||
|
||||
b.HasIndex("Slug")
|
||||
.IsUnique()
|
||||
.HasDatabaseName("ix_bundles_slug");
|
||||
|
||||
b.ToTable("bundles", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DysonNetwork.Drive.Storage.Model.PersistentUploadTask", b =>
|
||||
{
|
||||
b.HasBaseType("DysonNetwork.Drive.Storage.Model.PersistentTask");
|
||||
|
||||
b.Property<Guid?>("BundleId")
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("bundle_id");
|
||||
|
||||
b.Property<long>("ChunkSize")
|
||||
.HasColumnType("bigint")
|
||||
.HasColumnName("chunk_size");
|
||||
|
||||
b.Property<int>("ChunksCount")
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("chunks_count");
|
||||
|
||||
b.Property<int>("ChunksUploaded")
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("chunks_uploaded");
|
||||
|
||||
b.Property<string>("ContentType")
|
||||
.IsRequired()
|
||||
.HasMaxLength(128)
|
||||
.HasColumnType("character varying(128)")
|
||||
.HasColumnName("content_type");
|
||||
|
||||
b.Property<string>("EncryptPassword")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("character varying(256)")
|
||||
.HasColumnName("encrypt_password");
|
||||
|
||||
b.Property<string>("FileName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("character varying(256)")
|
||||
.HasColumnName("file_name");
|
||||
|
||||
b.Property<long>("FileSize")
|
||||
.HasColumnType("bigint")
|
||||
.HasColumnName("file_size");
|
||||
|
||||
b.Property<string>("Hash")
|
||||
.IsRequired()
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("hash");
|
||||
|
||||
b.Property<Guid>("PoolId")
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("pool_id");
|
||||
|
||||
b.PrimitiveCollection<List<int>>("UploadedChunks")
|
||||
.IsRequired()
|
||||
.HasColumnType("integer[]")
|
||||
.HasColumnName("uploaded_chunks");
|
||||
|
||||
b.HasDiscriminator().HasValue("PersistentUploadTask");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DysonNetwork.Shared.Models.CloudFileReference", b =>
|
||||
{
|
||||
b.HasOne("DysonNetwork.Shared.Models.SnCloudFile", "File")
|
||||
.WithMany("References")
|
||||
.HasForeignKey("FileId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired()
|
||||
.HasConstraintName("fk_file_references_files_file_id");
|
||||
|
||||
b.Navigation("File");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DysonNetwork.Shared.Models.SnCloudFile", b =>
|
||||
{
|
||||
b.HasOne("DysonNetwork.Shared.Models.SnFileBundle", "Bundle")
|
||||
.WithMany("Files")
|
||||
.HasForeignKey("BundleId")
|
||||
.HasConstraintName("fk_files_bundles_bundle_id");
|
||||
|
||||
b.HasOne("DysonNetwork.Shared.Models.FilePool", "Pool")
|
||||
.WithMany()
|
||||
.HasForeignKey("PoolId")
|
||||
.HasConstraintName("fk_files_pools_pool_id");
|
||||
|
||||
b.Navigation("Bundle");
|
||||
|
||||
b.Navigation("Pool");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DysonNetwork.Shared.Models.SnCloudFile", b =>
|
||||
{
|
||||
b.Navigation("References");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DysonNetwork.Shared.Models.SnFileBundle", b =>
|
||||
{
|
||||
b.Navigation("Files");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using NodaTime;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace DysonNetwork.Drive.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddPersistentTask : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.CreateTable(
|
||||
name: "tasks",
|
||||
columns: table => new
|
||||
{
|
||||
id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
task_id = table.Column<string>(type: "character varying(64)", maxLength: 64, nullable: false),
|
||||
name = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: false),
|
||||
description = table.Column<string>(type: "character varying(1024)", maxLength: 1024, nullable: true),
|
||||
type = table.Column<int>(type: "integer", nullable: false),
|
||||
status = table.Column<int>(type: "integer", nullable: false),
|
||||
account_id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
progress = table.Column<double>(type: "double precision", nullable: false),
|
||||
parameters = table.Column<Dictionary<string, object>>(type: "jsonb", nullable: false),
|
||||
results = table.Column<Dictionary<string, object>>(type: "jsonb", nullable: false),
|
||||
error_message = table.Column<string>(type: "character varying(1024)", maxLength: 1024, nullable: true),
|
||||
started_at = table.Column<Instant>(type: "timestamp with time zone", nullable: true),
|
||||
completed_at = table.Column<Instant>(type: "timestamp with time zone", nullable: true),
|
||||
expired_at = table.Column<Instant>(type: "timestamp with time zone", nullable: true),
|
||||
last_activity = table.Column<Instant>(type: "timestamp with time zone", nullable: false),
|
||||
priority = table.Column<int>(type: "integer", nullable: false),
|
||||
estimated_duration_seconds = table.Column<long>(type: "bigint", nullable: true),
|
||||
discriminator = table.Column<string>(type: "character varying(21)", maxLength: 21, nullable: false),
|
||||
file_name = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: true),
|
||||
file_size = table.Column<long>(type: "bigint", nullable: true),
|
||||
content_type = table.Column<string>(type: "character varying(128)", maxLength: 128, nullable: true),
|
||||
chunk_size = table.Column<long>(type: "bigint", nullable: true),
|
||||
chunks_count = table.Column<int>(type: "integer", nullable: true),
|
||||
chunks_uploaded = table.Column<int>(type: "integer", nullable: true),
|
||||
pool_id = table.Column<Guid>(type: "uuid", nullable: true),
|
||||
bundle_id = table.Column<Guid>(type: "uuid", nullable: true),
|
||||
encrypt_password = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: true),
|
||||
hash = table.Column<string>(type: "text", nullable: true),
|
||||
uploaded_chunks = table.Column<List<int>>(type: "integer[]", nullable: true),
|
||||
created_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false),
|
||||
updated_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false),
|
||||
deleted_at = table.Column<Instant>(type: "timestamp with time zone", nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("pk_tasks", x => x.id);
|
||||
});
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "tasks");
|
||||
}
|
||||
}
|
||||
}
|
||||
632
DysonNetwork.Drive/Migrations/20251112135535_AddFileIndex.Designer.cs
generated
Normal file
632
DysonNetwork.Drive/Migrations/20251112135535_AddFileIndex.Designer.cs
generated
Normal file
@@ -0,0 +1,632 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using DysonNetwork.Drive;
|
||||
using DysonNetwork.Shared.Models;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
using NodaTime;
|
||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace DysonNetwork.Drive.Migrations
|
||||
{
|
||||
[DbContext(typeof(AppDatabase))]
|
||||
[Migration("20251112135535_AddFileIndex")]
|
||||
partial class AddFileIndex
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "9.0.10")
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||
|
||||
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||
|
||||
modelBuilder.Entity("DysonNetwork.Drive.Billing.QuotaRecord", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<Guid>("AccountId")
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("account_id");
|
||||
|
||||
b.Property<Instant>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("created_at");
|
||||
|
||||
b.Property<Instant?>("DeletedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("deleted_at");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.IsRequired()
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("description");
|
||||
|
||||
b.Property<Instant?>("ExpiredAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("expired_at");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("name");
|
||||
|
||||
b.Property<long>("Quota")
|
||||
.HasColumnType("bigint")
|
||||
.HasColumnName("quota");
|
||||
|
||||
b.Property<Instant>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("updated_at");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_quota_records");
|
||||
|
||||
b.ToTable("quota_records", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DysonNetwork.Drive.Storage.Model.PersistentTask", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<Guid>("AccountId")
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("account_id");
|
||||
|
||||
b.Property<Instant?>("CompletedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("completed_at");
|
||||
|
||||
b.Property<Instant>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("created_at");
|
||||
|
||||
b.Property<Instant?>("DeletedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("deleted_at");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.HasMaxLength(1024)
|
||||
.HasColumnType("character varying(1024)")
|
||||
.HasColumnName("description");
|
||||
|
||||
b.Property<string>("Discriminator")
|
||||
.IsRequired()
|
||||
.HasMaxLength(21)
|
||||
.HasColumnType("character varying(21)")
|
||||
.HasColumnName("discriminator");
|
||||
|
||||
b.Property<string>("ErrorMessage")
|
||||
.HasMaxLength(1024)
|
||||
.HasColumnType("character varying(1024)")
|
||||
.HasColumnName("error_message");
|
||||
|
||||
b.Property<long?>("EstimatedDurationSeconds")
|
||||
.HasColumnType("bigint")
|
||||
.HasColumnName("estimated_duration_seconds");
|
||||
|
||||
b.Property<Instant?>("ExpiredAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("expired_at");
|
||||
|
||||
b.Property<Instant>("LastActivity")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("last_activity");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("character varying(256)")
|
||||
.HasColumnName("name");
|
||||
|
||||
b.Property<Dictionary<string, object>>("Parameters")
|
||||
.IsRequired()
|
||||
.HasColumnType("jsonb")
|
||||
.HasColumnName("parameters");
|
||||
|
||||
b.Property<int>("Priority")
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("priority");
|
||||
|
||||
b.Property<double>("Progress")
|
||||
.HasColumnType("double precision")
|
||||
.HasColumnName("progress");
|
||||
|
||||
b.Property<Dictionary<string, object>>("Results")
|
||||
.IsRequired()
|
||||
.HasColumnType("jsonb")
|
||||
.HasColumnName("results");
|
||||
|
||||
b.Property<Instant?>("StartedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("started_at");
|
||||
|
||||
b.Property<int>("Status")
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("status");
|
||||
|
||||
b.Property<string>("TaskId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("character varying(64)")
|
||||
.HasColumnName("task_id");
|
||||
|
||||
b.Property<int>("Type")
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("type");
|
||||
|
||||
b.Property<Instant>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("updated_at");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_tasks");
|
||||
|
||||
b.ToTable("tasks", (string)null);
|
||||
|
||||
b.HasDiscriminator().HasValue("PersistentTask");
|
||||
|
||||
b.UseTphMappingStrategy();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DysonNetwork.Shared.Models.CloudFileReference", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<Instant>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("created_at");
|
||||
|
||||
b.Property<Instant?>("DeletedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("deleted_at");
|
||||
|
||||
b.Property<Instant?>("ExpiredAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("expired_at");
|
||||
|
||||
b.Property<string>("FileId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(32)
|
||||
.HasColumnType("character varying(32)")
|
||||
.HasColumnName("file_id");
|
||||
|
||||
b.Property<string>("ResourceId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(1024)
|
||||
.HasColumnType("character varying(1024)")
|
||||
.HasColumnName("resource_id");
|
||||
|
||||
b.Property<Instant>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("updated_at");
|
||||
|
||||
b.Property<string>("Usage")
|
||||
.IsRequired()
|
||||
.HasMaxLength(1024)
|
||||
.HasColumnType("character varying(1024)")
|
||||
.HasColumnName("usage");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_file_references");
|
||||
|
||||
b.HasIndex("FileId")
|
||||
.HasDatabaseName("ix_file_references_file_id");
|
||||
|
||||
b.ToTable("file_references", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DysonNetwork.Shared.Models.FilePool", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<Guid?>("AccountId")
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("account_id");
|
||||
|
||||
b.Property<BillingConfig>("BillingConfig")
|
||||
.IsRequired()
|
||||
.HasColumnType("jsonb")
|
||||
.HasColumnName("billing_config");
|
||||
|
||||
b.Property<Instant>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("created_at");
|
||||
|
||||
b.Property<Instant?>("DeletedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("deleted_at");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.IsRequired()
|
||||
.HasMaxLength(8192)
|
||||
.HasColumnType("character varying(8192)")
|
||||
.HasColumnName("description");
|
||||
|
||||
b.Property<bool>("IsHidden")
|
||||
.HasColumnType("boolean")
|
||||
.HasColumnName("is_hidden");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(1024)
|
||||
.HasColumnType("character varying(1024)")
|
||||
.HasColumnName("name");
|
||||
|
||||
b.Property<PolicyConfig>("PolicyConfig")
|
||||
.IsRequired()
|
||||
.HasColumnType("jsonb")
|
||||
.HasColumnName("policy_config");
|
||||
|
||||
b.Property<RemoteStorageConfig>("StorageConfig")
|
||||
.IsRequired()
|
||||
.HasColumnType("jsonb")
|
||||
.HasColumnName("storage_config");
|
||||
|
||||
b.Property<Instant>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("updated_at");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_pools");
|
||||
|
||||
b.ToTable("pools", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DysonNetwork.Shared.Models.SnCloudFile", b =>
|
||||
{
|
||||
b.Property<string>("Id")
|
||||
.HasMaxLength(32)
|
||||
.HasColumnType("character varying(32)")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<Guid>("AccountId")
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("account_id");
|
||||
|
||||
b.Property<Guid?>("BundleId")
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("bundle_id");
|
||||
|
||||
b.Property<Instant>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("created_at");
|
||||
|
||||
b.Property<Instant?>("DeletedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("deleted_at");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.HasMaxLength(4096)
|
||||
.HasColumnType("character varying(4096)")
|
||||
.HasColumnName("description");
|
||||
|
||||
b.Property<Instant?>("ExpiredAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("expired_at");
|
||||
|
||||
b.Property<Dictionary<string, object>>("FileMeta")
|
||||
.HasColumnType("jsonb")
|
||||
.HasColumnName("file_meta");
|
||||
|
||||
b.Property<bool>("HasCompression")
|
||||
.HasColumnType("boolean")
|
||||
.HasColumnName("has_compression");
|
||||
|
||||
b.Property<bool>("HasThumbnail")
|
||||
.HasColumnType("boolean")
|
||||
.HasColumnName("has_thumbnail");
|
||||
|
||||
b.Property<string>("Hash")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("character varying(256)")
|
||||
.HasColumnName("hash");
|
||||
|
||||
b.Property<bool>("IsEncrypted")
|
||||
.HasColumnType("boolean")
|
||||
.HasColumnName("is_encrypted");
|
||||
|
||||
b.Property<bool>("IsMarkedRecycle")
|
||||
.HasColumnType("boolean")
|
||||
.HasColumnName("is_marked_recycle");
|
||||
|
||||
b.Property<string>("MimeType")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("character varying(256)")
|
||||
.HasColumnName("mime_type");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(1024)
|
||||
.HasColumnType("character varying(1024)")
|
||||
.HasColumnName("name");
|
||||
|
||||
b.Property<Guid?>("PoolId")
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("pool_id");
|
||||
|
||||
b.Property<List<ContentSensitiveMark>>("SensitiveMarks")
|
||||
.HasColumnType("jsonb")
|
||||
.HasColumnName("sensitive_marks");
|
||||
|
||||
b.Property<long>("Size")
|
||||
.HasColumnType("bigint")
|
||||
.HasColumnName("size");
|
||||
|
||||
b.Property<string>("StorageId")
|
||||
.HasMaxLength(32)
|
||||
.HasColumnType("character varying(32)")
|
||||
.HasColumnName("storage_id");
|
||||
|
||||
b.Property<string>("StorageUrl")
|
||||
.HasMaxLength(4096)
|
||||
.HasColumnType("character varying(4096)")
|
||||
.HasColumnName("storage_url");
|
||||
|
||||
b.Property<Instant>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("updated_at");
|
||||
|
||||
b.Property<Instant?>("UploadedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("uploaded_at");
|
||||
|
||||
b.Property<Dictionary<string, object>>("UserMeta")
|
||||
.HasColumnType("jsonb")
|
||||
.HasColumnName("user_meta");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_files");
|
||||
|
||||
b.HasIndex("BundleId")
|
||||
.HasDatabaseName("ix_files_bundle_id");
|
||||
|
||||
b.HasIndex("PoolId")
|
||||
.HasDatabaseName("ix_files_pool_id");
|
||||
|
||||
b.ToTable("files", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DysonNetwork.Shared.Models.SnCloudFileIndex", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<Guid>("AccountId")
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("account_id");
|
||||
|
||||
b.Property<Instant>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("created_at");
|
||||
|
||||
b.Property<Instant?>("DeletedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("deleted_at");
|
||||
|
||||
b.Property<string>("FileId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(32)
|
||||
.HasColumnType("character varying(32)")
|
||||
.HasColumnName("file_id");
|
||||
|
||||
b.Property<string>("Path")
|
||||
.IsRequired()
|
||||
.HasMaxLength(8192)
|
||||
.HasColumnType("character varying(8192)")
|
||||
.HasColumnName("path");
|
||||
|
||||
b.Property<Instant>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("updated_at");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_file_indexes");
|
||||
|
||||
b.HasIndex("FileId")
|
||||
.HasDatabaseName("ix_file_indexes_file_id");
|
||||
|
||||
b.HasIndex("Path", "AccountId")
|
||||
.HasDatabaseName("ix_file_indexes_path_account_id");
|
||||
|
||||
b.ToTable("file_indexes", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DysonNetwork.Shared.Models.SnFileBundle", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<Guid>("AccountId")
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("account_id");
|
||||
|
||||
b.Property<Instant>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("created_at");
|
||||
|
||||
b.Property<Instant?>("DeletedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("deleted_at");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.HasMaxLength(8192)
|
||||
.HasColumnType("character varying(8192)")
|
||||
.HasColumnName("description");
|
||||
|
||||
b.Property<Instant?>("ExpiredAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("expired_at");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(1024)
|
||||
.HasColumnType("character varying(1024)")
|
||||
.HasColumnName("name");
|
||||
|
||||
b.Property<string>("Passcode")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("character varying(256)")
|
||||
.HasColumnName("passcode");
|
||||
|
||||
b.Property<string>("Slug")
|
||||
.IsRequired()
|
||||
.HasMaxLength(1024)
|
||||
.HasColumnType("character varying(1024)")
|
||||
.HasColumnName("slug");
|
||||
|
||||
b.Property<Instant>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("updated_at");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_bundles");
|
||||
|
||||
b.HasIndex("Slug")
|
||||
.IsUnique()
|
||||
.HasDatabaseName("ix_bundles_slug");
|
||||
|
||||
b.ToTable("bundles", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DysonNetwork.Drive.Storage.Model.PersistentUploadTask", b =>
|
||||
{
|
||||
b.HasBaseType("DysonNetwork.Drive.Storage.Model.PersistentTask");
|
||||
|
||||
b.Property<Guid?>("BundleId")
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("bundle_id");
|
||||
|
||||
b.Property<long>("ChunkSize")
|
||||
.HasColumnType("bigint")
|
||||
.HasColumnName("chunk_size");
|
||||
|
||||
b.Property<int>("ChunksCount")
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("chunks_count");
|
||||
|
||||
b.Property<int>("ChunksUploaded")
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("chunks_uploaded");
|
||||
|
||||
b.Property<string>("ContentType")
|
||||
.IsRequired()
|
||||
.HasMaxLength(128)
|
||||
.HasColumnType("character varying(128)")
|
||||
.HasColumnName("content_type");
|
||||
|
||||
b.Property<string>("EncryptPassword")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("character varying(256)")
|
||||
.HasColumnName("encrypt_password");
|
||||
|
||||
b.Property<string>("FileName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("character varying(256)")
|
||||
.HasColumnName("file_name");
|
||||
|
||||
b.Property<long>("FileSize")
|
||||
.HasColumnType("bigint")
|
||||
.HasColumnName("file_size");
|
||||
|
||||
b.Property<string>("Hash")
|
||||
.IsRequired()
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("hash");
|
||||
|
||||
b.Property<string>("Path")
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("path");
|
||||
|
||||
b.Property<Guid>("PoolId")
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("pool_id");
|
||||
|
||||
b.PrimitiveCollection<List<int>>("UploadedChunks")
|
||||
.IsRequired()
|
||||
.HasColumnType("integer[]")
|
||||
.HasColumnName("uploaded_chunks");
|
||||
|
||||
b.HasDiscriminator().HasValue("PersistentUploadTask");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DysonNetwork.Shared.Models.CloudFileReference", b =>
|
||||
{
|
||||
b.HasOne("DysonNetwork.Shared.Models.SnCloudFile", "File")
|
||||
.WithMany("References")
|
||||
.HasForeignKey("FileId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired()
|
||||
.HasConstraintName("fk_file_references_files_file_id");
|
||||
|
||||
b.Navigation("File");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DysonNetwork.Shared.Models.SnCloudFile", b =>
|
||||
{
|
||||
b.HasOne("DysonNetwork.Shared.Models.SnFileBundle", "Bundle")
|
||||
.WithMany("Files")
|
||||
.HasForeignKey("BundleId")
|
||||
.HasConstraintName("fk_files_bundles_bundle_id");
|
||||
|
||||
b.HasOne("DysonNetwork.Shared.Models.FilePool", "Pool")
|
||||
.WithMany()
|
||||
.HasForeignKey("PoolId")
|
||||
.HasConstraintName("fk_files_pools_pool_id");
|
||||
|
||||
b.Navigation("Bundle");
|
||||
|
||||
b.Navigation("Pool");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DysonNetwork.Shared.Models.SnCloudFileIndex", b =>
|
||||
{
|
||||
b.HasOne("DysonNetwork.Shared.Models.SnCloudFile", "File")
|
||||
.WithMany("FileIndexes")
|
||||
.HasForeignKey("FileId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired()
|
||||
.HasConstraintName("fk_file_indexes_files_file_id");
|
||||
|
||||
b.Navigation("File");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DysonNetwork.Shared.Models.SnCloudFile", b =>
|
||||
{
|
||||
b.Navigation("FileIndexes");
|
||||
|
||||
b.Navigation("References");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("DysonNetwork.Shared.Models.SnFileBundle", b =>
|
||||
{
|
||||
b.Navigation("Files");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
66
DysonNetwork.Drive/Migrations/20251112135535_AddFileIndex.cs
Normal file
66
DysonNetwork.Drive/Migrations/20251112135535_AddFileIndex.cs
Normal file
@@ -0,0 +1,66 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using NodaTime;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace DysonNetwork.Drive.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddFileIndex : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "path",
|
||||
table: "tasks",
|
||||
type: "text",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "file_indexes",
|
||||
columns: table => new
|
||||
{
|
||||
id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
path = table.Column<string>(type: "character varying(8192)", maxLength: 8192, nullable: false),
|
||||
file_id = table.Column<string>(type: "character varying(32)", maxLength: 32, nullable: false),
|
||||
account_id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
created_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false),
|
||||
updated_at = table.Column<Instant>(type: "timestamp with time zone", nullable: false),
|
||||
deleted_at = table.Column<Instant>(type: "timestamp with time zone", nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("pk_file_indexes", x => x.id);
|
||||
table.ForeignKey(
|
||||
name: "fk_file_indexes_files_file_id",
|
||||
column: x => x.file_id,
|
||||
principalTable: "files",
|
||||
principalColumn: "id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "ix_file_indexes_file_id",
|
||||
table: "file_indexes",
|
||||
column: "file_id");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "ix_file_indexes_path_account_id",
|
||||
table: "file_indexes",
|
||||
columns: new[] { "path", "account_id" });
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "file_indexes");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "path",
|
||||
table: "tasks");
|
||||
}
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user