Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c01097196a | ||
|
|
5f1bee018d | ||
|
|
a7d411f6ff | ||
|
|
6a84da0f96 | ||
|
|
ac1c8552e1 | ||
|
|
6a18eefe34 | ||
|
|
527a090f1b | ||
|
|
370b86337f | ||
|
|
1a72be323b | ||
|
|
bf252f22f2 | ||
|
|
937d92f686 | ||
|
|
5572957731 | ||
|
|
d2db5722b7 | ||
|
|
5404185a6c | ||
|
|
800f2dfbbb | ||
|
|
ed0b5410f6 | ||
|
|
ae72e262aa | ||
|
|
c4c7050c3f | ||
|
|
81e5227270 | ||
|
|
39319d9162 | ||
|
|
12206fa856 | ||
|
|
9d8c6e26f1 | ||
|
|
eb624672e6 | ||
|
|
1b026b5f08 | ||
|
|
21b9b8b44d | ||
|
|
6ef7fcc0c2 | ||
|
|
fc4ce178cb | ||
|
|
98b72988c4 | ||
|
|
317473e528 | ||
|
|
e13c5ddf51 | ||
|
|
90be3ab731 | ||
|
|
85c0445a64 | ||
|
|
6a227ccd7c | ||
|
|
3d9a21f854 | ||
|
|
d4229a985d | ||
|
|
c6e0921dea | ||
|
|
346a8a4375 | ||
|
|
4799faa827 | ||
|
|
2c6f5c38ed | ||
|
|
4a884ab5cc | ||
|
|
e59beaf749 | ||
|
|
0d4e1229ab | ||
|
|
8810fd91c9 | ||
|
|
095444610d | ||
|
|
7cf43c5e7c | ||
|
|
77183af08d | ||
|
|
3a2c5be5dd | ||
|
|
11c764c2f3 | ||
|
|
d128234ae6 | ||
|
|
ee31ee225d | ||
|
|
e2195616d3 | ||
|
|
2ca818b996 | ||
|
|
131f2b4538 | ||
|
|
6731c3fea9 | ||
|
|
d3da5e8c13 | ||
|
|
3b0d4225ce | ||
|
|
66f0930164 | ||
|
|
3979b83a0a | ||
|
|
b65e432564 | ||
|
|
340e7c3e00 | ||
|
|
dcdce63e15 | ||
|
|
c2eaed757d | ||
|
|
920743bea7 | ||
|
|
0970f5baae | ||
|
|
291aa13e9a | ||
|
|
e644cde514 | ||
|
|
2350e68f4f | ||
|
|
cce3b681e3 | ||
|
|
755799f19d | ||
|
|
ec02dc8579 | ||
|
|
7028982ba1 | ||
|
|
f04503e38e | ||
|
|
f1edffed17 | ||
|
|
fac87acee7 | ||
|
|
c46dd91ede | ||
|
|
648753f8cf | ||
|
|
76b3bda313 | ||
|
|
8102dd1a8f | ||
|
|
aceb4e7f4c | ||
|
|
4224389e96 | ||
|
|
7c66c10b6d | ||
|
|
bf06a2e50c | ||
|
|
8c69fdef95 | ||
|
|
883c1d2f57 | ||
|
|
e7f1652b4e | ||
|
|
ea07224a52 | ||
|
|
b655154182 | ||
|
|
a173892163 | ||
|
|
14f48c4a35 | ||
|
|
88aa4daf74 | ||
|
|
25326dc9be | ||
|
|
030cd13f7c | ||
|
|
d557620c81 | ||
|
|
3273f8dd11 | ||
|
|
646719557c | ||
|
|
1640b326be | ||
|
|
cc19c9ec19 | ||
|
|
bb84ab9697 | ||
|
|
0f7664201a | ||
|
|
0b8a8213e6 | ||
|
|
476b3da66b | ||
|
|
41c632ccfa | ||
|
|
dbf5addf12 | ||
|
|
8d414080a4 | ||
|
|
c8d390726c | ||
|
|
31285dd0d6 | ||
|
|
b9ef58044e | ||
|
|
e320f74dfc | ||
|
|
b9deb3cdf0 | ||
|
|
5dbf293e1e | ||
|
|
47f2308acc | ||
|
|
496be692e7 | ||
|
|
5df6dd169a | ||
|
|
7232f17cb7 | ||
|
|
5b1c3d0606 | ||
|
|
8e6ca64fa2 | ||
|
|
9116b4d126 | ||
|
|
4ce38d9857 | ||
|
|
8811c48384 | ||
|
|
078d0d1ee2 | ||
|
|
56d755afa6 | ||
|
|
9e3abb366f | ||
|
|
4ebac1c46f | ||
|
|
1476df7a1f | ||
|
|
833872603a | ||
|
|
737834310b | ||
|
|
4474c8ee5e | ||
|
|
ebe5c63a7b | ||
|
|
afd98ae652 | ||
|
|
8f905c8128 | ||
|
|
f43f30c9b9 | ||
|
|
6b1bea9b17 | ||
|
|
cdaed9766e | ||
|
|
d31a9ab2d2 | ||
|
|
07d49749fb | ||
|
|
a1f1da8a07 | ||
|
|
1b791fd287 | ||
|
|
2a4af58a51 | ||
|
|
b12f8ca044 | ||
|
|
519b3abd1d | ||
|
|
e08a5bd6b6 | ||
|
|
f6c597b42d | ||
|
|
f3b9032174 | ||
|
|
44b4ee2953 | ||
|
|
b77f79f42d | ||
|
|
8017bf7c8a | ||
|
|
85f4cda752 | ||
|
|
1fd2f553ed | ||
|
|
d68170eda8 | ||
|
|
51f27226e7 | ||
|
|
d54c366323 | ||
|
|
0e7cb9fbab | ||
|
|
63e83032f1 | ||
|
|
b4da6fdd9f | ||
|
|
12535a59e8 | ||
|
|
b345eda7aa | ||
|
|
660c1dcadf | ||
|
|
be66856034 | ||
|
|
80e3027107 | ||
|
|
3bf731055f | ||
|
|
362ca53efb | ||
|
|
7ab213e5e4 | ||
|
|
f0eafb1162 | ||
|
|
ad5baa7d6f | ||
|
|
5840e30afb | ||
|
|
8f95606dd7 | ||
|
|
ca9bc7ae94 | ||
|
|
288a40e697 | ||
|
|
fa91e8c289 | ||
|
|
4ab5fe4c6f | ||
|
|
d87782d180 | ||
|
|
1c97c97fa1 | ||
|
|
2426166676 | ||
|
|
6f6c1c7c52 | ||
|
|
7b9cb2ecb9 | ||
|
|
fc029af654 | ||
|
|
110a69dc4b | ||
|
|
a61791fdb2 | ||
|
|
c01440f9d4 | ||
|
|
39584dd7a1 | ||
|
|
d9badeacba | ||
|
|
d4cde2ba9c | ||
|
|
824ef4528e | ||
|
|
feb06a85ae | ||
|
|
cfad645e33 | ||
|
|
675f8dec20 | ||
|
|
11e85b613c | ||
|
|
eb82833c7e | ||
|
|
399a3e09de | ||
|
|
1a735bda42 | ||
|
|
991196cc30 | ||
|
|
5f48903a73 | ||
|
|
5dfe1b758e | ||
|
|
d24e534828 | ||
|
|
80bdca138e | ||
|
|
0a2ca8c0b6 | ||
|
|
921a355ae0 | ||
|
|
eff7b24b7a | ||
|
|
c328620f3d | ||
|
|
a49b6d6573 | ||
|
|
baffd2efaf | ||
|
|
292693a1ed | ||
|
|
cdd01e695c | ||
|
|
614a9e057c | ||
|
|
24d90bb750 | ||
|
|
c2726e6b18 | ||
|
|
a746ce9169 | ||
|
|
c00b967179 | ||
|
|
f6e749ecf9 | ||
|
|
8f5e0686c8 | ||
|
|
b9f0d9279b | ||
|
|
cd69f0c82f | ||
|
|
884746f3a5 | ||
|
|
9c4d862297 | ||
|
|
cc87cf68b5 | ||
|
|
db1a0235ed | ||
|
|
2cd6a7cae0 | ||
|
|
bd80fdc960 | ||
|
|
7c5d57ddc1 | ||
|
|
5653ff18c8 | ||
|
|
83ea444675 | ||
|
|
966edfa85a | ||
|
|
30b9fc7765 | ||
|
|
95a60e9a96 | ||
|
|
9327331adc | ||
|
|
e8baa441cd | ||
|
|
61d142afbd | ||
|
|
46022e8711 | ||
|
|
e59f3c46fd | ||
|
|
7fcb3f2c31 | ||
|
|
a2243585a0 | ||
|
|
6231002043 | ||
|
|
33eb476009 | ||
|
|
ebb0a4c7e5 | ||
|
|
d010ed6bc5 | ||
|
|
1941e29175 | ||
|
|
6e8932ff52 | ||
|
|
f9bfae9b9d | ||
|
|
2fca928cf0 | ||
|
|
8f3a362a3a | ||
|
|
62a6189788 | ||
|
|
2a6d631719 | ||
|
|
4942591d7f | ||
|
|
1760c1b852 | ||
|
|
86d7ab9864 | ||
|
|
f201e4593a | ||
|
|
71129e556e | ||
|
|
0e7ebb79db | ||
|
|
727d9eb57c | ||
|
|
228ff75468 | ||
|
|
80018cac08 | ||
|
|
b2c19e7ed0 | ||
|
|
a64a80910d | ||
|
|
4eeeef3427 | ||
|
|
657586ddff | ||
|
|
797b287899 | ||
|
|
db2d95f2c5 | ||
|
|
16a8f259ab | ||
|
|
ee1cd8d08d | ||
|
|
e53046324c | ||
|
|
dd71977317 | ||
|
|
bee0da1454 | ||
|
|
b60feaa447 | ||
|
|
743e54614a | ||
|
|
bd96713af4 | ||
|
|
8f7f9b457a | ||
|
|
4c0d68eab8 | ||
|
|
8211407cf2 | ||
|
|
5fb6c7d36a | ||
|
|
30b1cf4493 | ||
|
|
f4789ac5fe | ||
|
|
5fd826ae71 | ||
|
|
22ce08f06c | ||
|
|
696c88f03f | ||
|
|
9d5d87e47b | ||
|
|
e32539e02c | ||
|
|
46ba931a6c | ||
|
|
4bac5143d6 | ||
|
|
bcb1674491 | ||
|
|
3cbd9677c2 | ||
|
|
b216ee1a80 | ||
|
|
32ec482449 | ||
|
|
5074748055 | ||
|
|
6fa11f834b | ||
|
|
25c7702127 | ||
|
|
b546556d08 | ||
|
|
379a85f6f6 | ||
|
|
92d2c14d6a | ||
|
|
a6c1f7d3f2 | ||
|
|
6fa6187cc4 | ||
|
|
848e7a1609 | ||
|
|
3e8c0a2896 | ||
|
|
11900b1500 | ||
|
|
85044fe3e0 | ||
|
|
c0499d40ea | ||
|
|
8ee44ffea1 | ||
|
|
5668a1411f | ||
|
|
6db60f111d | ||
|
|
3d695709d5 | ||
|
|
5c682addc9 | ||
|
|
24810cc818 | ||
|
|
8caeb11da5 | ||
|
|
5e07b446fc | ||
|
|
f29444a219 | ||
|
|
bce1c5d9b8 | ||
|
|
5b980fb382 | ||
|
|
e63e62d3e0 | ||
|
|
53f82792d8 | ||
|
|
760388307c | ||
|
|
eebd8500ee | ||
|
|
12112afa37 | ||
|
|
424fa581b9 | ||
|
|
cddb0a7e05 | ||
|
|
83ef0ee74e | ||
|
|
21b317e4f1 | ||
|
|
7d818206b0 | ||
|
|
0216c40bfe | ||
|
|
ecf80280f1 | ||
|
|
6e9568b4fb | ||
|
|
8d968e7d63 | ||
|
|
bb264f0b9b | ||
|
|
be4f770c35 | ||
|
|
17d955dd69 | ||
|
|
3f31e85c06 | ||
|
|
5a0c230ead | ||
|
|
e10350268d | ||
|
|
969a48bef2 | ||
|
|
8158934cb9 | ||
|
|
670f6a1974 | ||
|
|
dd1f791b3e | ||
|
|
91f25bc25d | ||
|
|
d8e9e2dac3 | ||
|
|
67a89a12ac | ||
|
|
faaa4033bd | ||
|
|
f3eedf6f30 | ||
|
|
29718d2a0c | ||
|
|
c7e6bd8b72 | ||
|
|
a67ff12786 | ||
|
|
c92e671622 | ||
|
|
2bd25353da | ||
|
|
e1bee19c18 | ||
|
|
0ec49af72a | ||
|
|
6d4eb836ab | ||
|
|
0c3cfecb1d | ||
|
|
f9186e7354 | ||
|
|
bc603a08cf | ||
|
|
da5c65d5a6 | ||
|
|
077aada056 | ||
|
|
4ed5f9872d | ||
|
|
2d6be20b30 | ||
|
|
be8f3bd98c | ||
|
|
a3a20bee4a | ||
|
|
09a1e75acf | ||
|
|
ce6bdd01ef | ||
|
|
a03a1d635f | ||
|
|
419b6d4891 | ||
|
|
b4840318d5 | ||
|
|
bf81f437d1 | ||
|
|
81a4d33df1 | ||
|
|
baab364b56 | ||
|
|
585ee041ec | ||
|
|
da3d4350cc | ||
|
|
df9390847f | ||
|
|
2a1acbfde8 | ||
|
|
16ef8507b0 | ||
|
|
df5d4e336c | ||
|
|
ad2015329b | ||
|
|
4cc4bed785 | ||
|
|
6c506e6907 | ||
|
|
977f9344c7 | ||
|
|
2adb2f3b68 | ||
|
|
40a6e9e69f | ||
|
|
e1d1a3fb9a | ||
|
|
75120c0a56 | ||
|
|
33f1ef3bb3 | ||
|
|
b6e742333c | ||
|
|
9f8c66fc20 | ||
|
|
3b93257890 | ||
|
|
1104e2a074 | ||
|
|
3e950652e8 | ||
|
|
98e40466a0 | ||
|
|
324d80a735 | ||
|
|
6770be63b6 | ||
|
|
89aa33ebd8 | ||
|
|
619c32f09d | ||
|
|
c471423775 | ||
|
|
c589b9b8b8 | ||
|
|
bae9ba0973 | ||
|
|
7bfc5c1d9b | ||
|
|
8bd3b0d731 | ||
|
|
06cd4de141 | ||
|
|
33eb2df023 | ||
|
|
540ecd4b60 | ||
|
|
237418f009 | ||
|
|
4447180e9e | ||
|
|
f111946592 | ||
|
|
475e404fae | ||
|
|
07ce5215d4 | ||
|
|
9f4895fd63 | ||
|
|
c18a9da0d3 | ||
|
|
8e3eb32631 | ||
|
|
f0598b3f59 | ||
|
|
011120fdf9 | ||
|
|
ff175e1edd | ||
|
|
3f56008c39 | ||
|
|
5df95bf7c1 | ||
|
|
23d52d0447 | ||
|
|
795a127998 | ||
|
|
fd73d383f9 | ||
|
|
ee285df7b5 | ||
|
|
7f7f8e642f | ||
|
|
d9997ff5e9 | ||
|
|
36adcb323a | ||
|
|
5302060f44 | ||
|
|
d19a46bee6 | ||
|
|
d504866aad | ||
|
|
601dae4909 | ||
|
|
08f2ad0521 | ||
|
|
3b356e33e7 | ||
|
|
57d9b8da1e | ||
|
|
86f50e5741 | ||
|
|
a6e52fd03d | ||
|
|
116229a29e | ||
|
|
428782e8a4 | ||
|
|
b8442803d9 | ||
|
|
82ab2c63d6 | ||
|
|
e953de0e06 | ||
|
|
f6d927f866 | ||
|
|
3773c4b068 | ||
|
|
bb0dbdadae | ||
|
|
dd8aec50eb | ||
|
|
7410aaea38 | ||
|
|
0f9bbbda3c | ||
|
|
25f9bc6e79 | ||
|
|
caf00b7877 | ||
|
|
9c0c462192 | ||
|
|
ef7570db76 | ||
|
|
e1bc1ec6c8 | ||
|
|
ca232250cc | ||
|
|
fde7688485 | ||
|
|
9458b12d69 | ||
|
|
5cf6cf075e | ||
|
|
8980e6d4a8 | ||
|
|
ac951378e8 | ||
|
|
934e635cdb | ||
|
|
d968aa9c16 | ||
|
|
eb471c7cdb | ||
|
|
e886bdd58d | ||
|
|
fe5d8bae1b | ||
|
|
aabdea06a9 | ||
|
|
37a3ace161 | ||
|
|
933ab7d68a | ||
|
|
f42cb37d67 | ||
|
|
7dfd9f02e8 | ||
|
|
411e09f452 | ||
|
|
f21149f42e | ||
|
|
099b6fb0e8 | ||
|
|
eb2e0ef468 | ||
|
|
4644ed2f41 | ||
|
|
cf8e5b7094 | ||
|
|
b0775e82ea | ||
|
|
f9b3461a44 | ||
|
|
c4ebc876bf | ||
|
|
a435a92d41 | ||
|
|
bcbd6bbea1 | ||
|
|
82559b49bd | ||
|
|
8c1b869004 | ||
|
|
c4a0692985 | ||
|
|
750f1cb145 | ||
|
|
b7d3487c53 | ||
|
|
529f157c6e | ||
|
|
c62be9854a | ||
|
|
9c1aa66b41 | ||
|
|
9894e2582d | ||
|
|
8a8ef81fce | ||
|
|
2fdd9f1296 | ||
|
|
5251e01011 | ||
|
|
84dab35f4b | ||
|
|
ae766dab2f | ||
|
|
027928c29f | ||
|
|
e3239639f3 | ||
|
|
7d6038e5f1 | ||
|
|
2a90d4933d | ||
|
|
1fbd979d40 | ||
|
|
4e06d2bf97 | ||
|
|
8ce3988613 | ||
|
|
b7f161d200 | ||
|
|
a3f5c4e494 |
@@ -0,0 +1,61 @@
|
|||||||
|
# Environment
|
||||||
|
NODE_ENV=development
|
||||||
|
|
||||||
|
# Database
|
||||||
|
DATABASE_URL=postgresql://devuser:changeme@localhost:5433/devdb?schema=public
|
||||||
|
|
||||||
|
# Project
|
||||||
|
PROJECT_NAME="Portabase"
|
||||||
|
PROJECT_DESCRIPTION="Portabase is a powerful database manager"
|
||||||
|
PROJECT_URL=http://localhost:8887
|
||||||
|
PROJECT_SECRET=
|
||||||
|
|
||||||
|
# SMTP (email)
|
||||||
|
SMTP_HOST=
|
||||||
|
SMTP_PORT=
|
||||||
|
SMTP_USER=
|
||||||
|
SMTP_PASSWORD=
|
||||||
|
SMTP_FROM=
|
||||||
|
#SMTP_SECURE="true"
|
||||||
|
#Port 587 -> secure: false
|
||||||
|
#Port 465 -> secure: true
|
||||||
|
#By default : true
|
||||||
|
|
||||||
|
# OIDC SSO Authentification
|
||||||
|
AUTH_OIDC_ID=""
|
||||||
|
AUTH_OIDC_TITLE=""
|
||||||
|
AUTH_OIDC_DESC=""
|
||||||
|
AUTH_OIDC_ICON=""
|
||||||
|
AUTH_OIDC_CLIENT=""
|
||||||
|
AUTH_OIDC_SECRET=""
|
||||||
|
AUTH_OIDC_ISSUER_URL=""
|
||||||
|
AUTH_OIDC_HOST=""
|
||||||
|
AUTH_OIDC_SCOPES=""
|
||||||
|
AUTH_OIDC_DISCOVERY_ENDPOINT=""
|
||||||
|
AUTH_OIDC_JWKS_ENDPOINT=""
|
||||||
|
AUTH_OIDC_PKCE=true
|
||||||
|
ALLOWED_GROUP="admin"
|
||||||
|
AUTH_OIDC_ROLE_MAP=""
|
||||||
|
|
||||||
|
AUTH_ALLOW_LINKING=true
|
||||||
|
AUTH_ALLOW_UNLINKING=true
|
||||||
|
|
||||||
|
|
||||||
|
# Social OAuth2 Authentification
|
||||||
|
AUTH_SOCIAL_ID=""
|
||||||
|
AUTH_SOCIAL_TITLE=""
|
||||||
|
AUTH_SOCIAL_DESC=""
|
||||||
|
AUTH_SOCIAL_ICON=""
|
||||||
|
AUTH_SOCIAL_CLIENT=""
|
||||||
|
AUTH_SOCIAL_SECRET=""
|
||||||
|
AUTH_SOCIAL_APPLE_APP_BUNDLE_IDENTIFIER=""
|
||||||
|
|
||||||
|
# If true, the user's role will be updated from OIDC claims on every login.
|
||||||
|
# If false, the role is only set when the user is first created.
|
||||||
|
AUTH_SYNC_OIDC_ROLES_ON_LOGIN=true
|
||||||
|
AUTH_EMAIL_PASSWORD_ENABLED=true
|
||||||
|
AUTH_SIGNUP_ENABLED=true
|
||||||
|
AUTH_PASSKEY_ENABLED=true
|
||||||
|
|
||||||
|
# Retention
|
||||||
|
RETENTION_CRON="* * * * *"
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
# Environnement
|
|
||||||
NODE_ENV=production
|
|
||||||
|
|
||||||
# Base de données
|
|
||||||
DATABASE_URL=postgresql://devuser:changeme@db:5432/devdb?schema=public
|
|
||||||
|
|
||||||
# Projet
|
|
||||||
NEXT_PUBLIC_PROJECT_NAME="Portabase"
|
|
||||||
NEXT_PUBLIC_PROJECT_DESCRIPTION="Portabase is a powerful database manager"
|
|
||||||
NEXT_PUBLIC_PROJECT_URL=http://app.portabase.io
|
|
||||||
PROJECT_SECRET=
|
|
||||||
|
|
||||||
# SMTP (email)
|
|
||||||
SMTP_HOST=
|
|
||||||
SMTP_PORT=587
|
|
||||||
SMTP_USER=
|
|
||||||
SMTP_PASSWORD=
|
|
||||||
SMTP_FROM=
|
|
||||||
|
|
||||||
# Google
|
|
||||||
AUTH_GOOGLE_ID=
|
|
||||||
AUTH_GOOGLE_SECRET=
|
|
||||||
|
|
||||||
# S3
|
|
||||||
S3_ENDPOINT=http://app.s3.portabase.io
|
|
||||||
S3_ACCESS_KEY=
|
|
||||||
S3_SECRET_KEY=
|
|
||||||
S3_BUCKET_NAME=portabase
|
|
||||||
S3_PORT=9000
|
|
||||||
S3_USE_SSL=true
|
|
||||||
|
|
||||||
# Storage Type (s3, local)
|
|
||||||
STORAGE_TYPE=local
|
|
||||||
@@ -26,7 +26,7 @@ Please take a moment to review this guide. It will help you understand how to co
|
|||||||
|
|
||||||
2. **Clone the repository**
|
2. **Clone the repository**
|
||||||
```bash
|
```bash
|
||||||
git clone https://github.com/Soluce-Technologies/portabase
|
git clone https://github.com/Portabase/portabase
|
||||||
```
|
```
|
||||||
|
|
||||||
3. **Set up the development environment**
|
3. **Set up the development environment**
|
||||||
@@ -72,7 +72,7 @@ If you encounter a bug or have a suggestion for improvement, follow these steps:
|
|||||||
|
|
||||||
3. **Push your branch**
|
3. **Push your branch**
|
||||||
```bash
|
```bash
|
||||||
git push origin feature/<feature-name>
|
git push origin feat/<feature-name>
|
||||||
```
|
```
|
||||||
|
|
||||||
4. **Open a Pull Request (PR)**
|
4. **Open a Pull Request (PR)**
|
||||||
@@ -82,7 +82,7 @@ If you encounter a bug or have a suggestion for improvement, follow these steps:
|
|||||||
|
|
||||||
## Code Style Guidelines
|
## Code Style Guidelines
|
||||||
|
|
||||||
- Follow the [specific coding style guide] (e.g., Prettier, ESLint, PEP8).
|
- Follow the [specific coding style guide] (e.g., Prettier, ESLint, PEP8,Biome).
|
||||||
- Use meaningful variable names and include comments where necessary.
|
- Use meaningful variable names and include comments where necessary.
|
||||||
- Tests before submitting your changes.
|
- Tests before submitting your changes.
|
||||||
|
|
||||||
@@ -107,4 +107,4 @@ If you encounter a bug or have a suggestion for improvement, follow these steps:
|
|||||||
|
|
||||||
Thank you for contributing! 🙌
|
Thank you for contributing! 🙌
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
# These are supported funding model platforms
|
||||||
|
|
||||||
|
github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
|
||||||
|
patreon: # Replace with a single Patreon username
|
||||||
|
open_collective: # Replace with a single Open Collective username
|
||||||
|
ko_fi: # Replace with a single Ko-fi username
|
||||||
|
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
|
||||||
|
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
|
||||||
|
liberapay: # Replace with a single Liberapay username
|
||||||
|
issuehunt: # Replace with a single IssueHunt username
|
||||||
|
lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry
|
||||||
|
polar: # Replace with a single Polar username
|
||||||
|
buy_me_a_coffee: portabase
|
||||||
|
thanks_dev: # Replace with a single thanks.dev username
|
||||||
|
custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
|
||||||
@@ -7,9 +7,9 @@
|
|||||||
|
|
||||||
We take security seriously and aim to support the following versions of the project with security updates:
|
We take security seriously and aim to support the following versions of the project with security updates:
|
||||||
|
|
||||||
| Version | Supported |
|
| Version | Supported |
|
||||||
|-----------|--------------------|
|
|---------|--------------------|
|
||||||
| 1.x | ✅ Fully Supported |
|
| Latest | ✅ Fully Supported |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
After Width: | Height: | Size: 35 KiB |
@@ -0,0 +1,47 @@
|
|||||||
|
name: Auto Release bot
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
types: [closed]
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
release:
|
||||||
|
if: github.event.pull_request.merged == true
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/create-github-app-token@v1
|
||||||
|
id: app-token
|
||||||
|
with:
|
||||||
|
app-id: ${{ vars.APP_ID }}
|
||||||
|
private-key: ${{ secrets.APP_PRIVATE_KEY }}
|
||||||
|
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
token: ${{ steps.app-token.outputs.token }}
|
||||||
|
ref: ${{ github.base_ref }}
|
||||||
|
|
||||||
|
- uses: pnpm/action-setup@v4
|
||||||
|
|
||||||
|
- uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: "lts/*"
|
||||||
|
cache: "pnpm"
|
||||||
|
|
||||||
|
- run: pnpm install --frozen-lockfile
|
||||||
|
|
||||||
|
- run: git config --global user.name 'github-actions[bot]'
|
||||||
|
- run: git config --global user.email 'github-actions[bot]@users.noreply.github.com'
|
||||||
|
|
||||||
|
- name: Run release-it
|
||||||
|
run: |
|
||||||
|
git pull origin main
|
||||||
|
pnpm exec release-it --ci
|
||||||
|
git push origin --tags
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ steps.app-token.outputs.token }}
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
name: Discord Notification
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_call:
|
||||||
|
inputs:
|
||||||
|
discord_title:
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
discord_color:
|
||||||
|
required: true
|
||||||
|
type: number
|
||||||
|
discord_footer:
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
secrets:
|
||||||
|
DISCORD_WEBHOOK:
|
||||||
|
required: true
|
||||||
|
GH_TOKEN:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
notify-discord:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Send Discord Notification
|
||||||
|
env:
|
||||||
|
DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }}
|
||||||
|
GH_TOKEN: ${{ secrets.GH_TOKEN }}
|
||||||
|
run: |
|
||||||
|
RELEASE_INFO=$(gh release view "${{ github.ref_name }}" -R ${{ github.repository }} --json name,url,body,author)
|
||||||
|
|
||||||
|
RELEASE_TITLE=$(echo "$RELEASE_INFO" | jq -r .name)
|
||||||
|
if [ -z "$RELEASE_TITLE" ] || [ "$RELEASE_TITLE" = "null" ]; then RELEASE_TITLE="${{ github.ref_name }}"; fi
|
||||||
|
|
||||||
|
RELEASE_URL=$(echo "$RELEASE_INFO" | jq -r .url)
|
||||||
|
RELEASE_BODY=$(echo "$RELEASE_INFO" | jq -r .body)
|
||||||
|
|
||||||
|
AUTHOR_NAME="Portabase"
|
||||||
|
AUTHOR_ICON="https://github.com/Portabase.png"
|
||||||
|
|
||||||
|
PAYLOAD=$(jq -n \
|
||||||
|
--arg title "$RELEASE_TITLE" \
|
||||||
|
--arg description "$RELEASE_BODY" \
|
||||||
|
--arg url "$RELEASE_URL" \
|
||||||
|
--arg author "$AUTHOR_NAME" \
|
||||||
|
--arg icon "$AUTHOR_ICON" \
|
||||||
|
--arg discord_title "${{ inputs.discord_title }}" \
|
||||||
|
--arg discord_footer "${{ inputs.discord_footer }}" \
|
||||||
|
--argjson discord_color ${{ inputs.discord_color }} \
|
||||||
|
'{
|
||||||
|
content: $discord_title,
|
||||||
|
embeds: [{
|
||||||
|
title: $title,
|
||||||
|
url: $url,
|
||||||
|
description: $description,
|
||||||
|
color: $discord_color,
|
||||||
|
author: {
|
||||||
|
name: $author,
|
||||||
|
icon_url: $icon
|
||||||
|
},
|
||||||
|
footer: {
|
||||||
|
text: $discord_footer
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
}'
|
||||||
|
)
|
||||||
|
|
||||||
|
curl -H "Content-Type: application/json" \
|
||||||
|
-d "$PAYLOAD" \
|
||||||
|
"$DISCORD_WEBHOOK"
|
||||||
@@ -0,0 +1,114 @@
|
|||||||
|
name: Docker Publish
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_call:
|
||||||
|
inputs:
|
||||||
|
image_name:
|
||||||
|
required: false
|
||||||
|
type: string
|
||||||
|
default: "portabase/portabase"
|
||||||
|
add_latest:
|
||||||
|
required: false
|
||||||
|
type: boolean
|
||||||
|
default: false
|
||||||
|
target:
|
||||||
|
required: false
|
||||||
|
type: string
|
||||||
|
default: "prod"
|
||||||
|
dockerfile:
|
||||||
|
required: false
|
||||||
|
type: string
|
||||||
|
default: "./docker/dockerfile/Dockerfile"
|
||||||
|
secrets:
|
||||||
|
DOCKER_USERNAME:
|
||||||
|
required: true
|
||||||
|
DOCKER_PASSWORD:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
name: Build architectures
|
||||||
|
runs-on: ${{ matrix.platform == 'linux/amd64' && 'ubuntu-latest' || 'ubuntu-24.04-arm' }}
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
platform: [linux/amd64, linux/arm64]
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
|
- name: Login to Docker
|
||||||
|
uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
username: ${{ secrets.DOCKER_USERNAME }}
|
||||||
|
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||||
|
|
||||||
|
- name: Prepare Image Tags
|
||||||
|
id: prep
|
||||||
|
run: |
|
||||||
|
ARCH=${{ matrix.platform == 'linux/amd64' && 'amd64' || 'arm64' }}
|
||||||
|
echo "image=${{ inputs.image_name }}:${GITHUB_REF#refs/tags/}-$ARCH" >> $GITHUB_OUTPUT
|
||||||
|
echo "safe_platform=${{ matrix.platform == 'linux/amd64' && 'linux-amd64' || 'linux-arm64' }}" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
|
- name: Build and push image
|
||||||
|
uses: docker/build-push-action@v6
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
file: ${{ inputs.dockerfile }}
|
||||||
|
platforms: ${{ matrix.platform }}
|
||||||
|
push: true
|
||||||
|
tags: ${{ steps.prep.outputs.image }}
|
||||||
|
target: ${{ inputs.target }}
|
||||||
|
cache-from: type=gha,scope=build-${{ matrix.platform }}
|
||||||
|
cache-to: type=gha,mode=max,scope=build-${{ matrix.platform }}
|
||||||
|
|
||||||
|
- name: Save image name for manifest
|
||||||
|
run: echo "${{ steps.prep.outputs.image }}" > image.txt
|
||||||
|
|
||||||
|
- uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: image-${{ steps.prep.outputs.safe_platform }}
|
||||||
|
path: image.txt
|
||||||
|
retention-days: 1
|
||||||
|
|
||||||
|
create-manifest:
|
||||||
|
name: Create multi-arch manifest
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: build
|
||||||
|
steps:
|
||||||
|
- uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
name: image-linux-amd64
|
||||||
|
path: /tmp/digests/amd64
|
||||||
|
|
||||||
|
- uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
name: image-linux-arm64
|
||||||
|
path: /tmp/digests/arm64
|
||||||
|
|
||||||
|
- name: Login to Docker
|
||||||
|
uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
username: ${{ secrets.DOCKER_USERNAME }}
|
||||||
|
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||||
|
|
||||||
|
- name: Extract Docker metadata
|
||||||
|
id: meta
|
||||||
|
uses: docker/metadata-action@v5
|
||||||
|
with:
|
||||||
|
images: ${{ inputs.image_name }}
|
||||||
|
tags: |
|
||||||
|
type=semver,pattern={{version}}
|
||||||
|
type=semver,pattern={{major}}.{{minor}}
|
||||||
|
type=semver,pattern={{major}}
|
||||||
|
type=raw,value=latest,enable=${{ inputs.add_latest }}
|
||||||
|
|
||||||
|
- name: Create and push manifest list
|
||||||
|
working-directory: /tmp/digests
|
||||||
|
run: |
|
||||||
|
DOCKER_IMAGES="$(cat amd64/image.txt) $(cat arm64/image.txt)"
|
||||||
|
TAG_ARGS=$(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON")
|
||||||
|
docker buildx imagetools create $DOCKER_IMAGES $TAG_ARGS
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
name: PR Checker
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
- dev
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
validate-code:
|
||||||
|
name: Code Integrity Check
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Setup pnpm
|
||||||
|
uses: pnpm/action-setup@v4
|
||||||
|
|
||||||
|
- name: Setup Node.js
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: "lts/*"
|
||||||
|
cache: "pnpm"
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: pnpm install --frozen-lockfile
|
||||||
|
|
||||||
|
- name: Check code formatting
|
||||||
|
run: pnpm run format:check
|
||||||
|
continue-on-error: true
|
||||||
|
|
||||||
|
- name: Run Linter
|
||||||
|
run: pnpm run lint
|
||||||
|
continue-on-error: true
|
||||||
|
|
||||||
|
- name: Type Check
|
||||||
|
run: pnpm run typecheck
|
||||||
|
continue-on-error: true
|
||||||
|
|
||||||
|
- name: Run Tests
|
||||||
|
run: pnpm run test
|
||||||
|
continue-on-error: true
|
||||||
|
|
||||||
|
- name: Test Application Build
|
||||||
|
run: pnpm run build
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
name: Publish Docker image for release candidate
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- "*.*.*-rc*"
|
||||||
|
- "!*-*-rc*"
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
packages: write
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
docker_publish:
|
||||||
|
uses: ./.github/workflows/docker.yml
|
||||||
|
with:
|
||||||
|
add_latest: true
|
||||||
|
secrets:
|
||||||
|
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME_2 }}
|
||||||
|
DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD_2 }}
|
||||||
|
|
||||||
|
discord_notification:
|
||||||
|
needs: docker_publish
|
||||||
|
uses: ./.github/workflows/discord.yml
|
||||||
|
with:
|
||||||
|
discord_title: "New Release Candidate: ${{ github.ref_name }}"
|
||||||
|
discord_color: 2044800
|
||||||
|
discord_footer: "Portabase"
|
||||||
|
secrets:
|
||||||
|
DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }}
|
||||||
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
@@ -1,69 +1,45 @@
|
|||||||
name: Publish Docker image for release
|
name: Publish Docker image for release
|
||||||
|
|
||||||
on:
|
on:
|
||||||
release:
|
push:
|
||||||
types: [published]
|
tags:
|
||||||
|
- "*.*.*"
|
||||||
|
- "!*-*"
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
packages: write
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
push_to_registry:
|
docker_publish:
|
||||||
name: Push Docker image to Docker Hub
|
uses: ./.github/workflows/docker.yml
|
||||||
|
with:
|
||||||
|
add_latest: true
|
||||||
|
secrets:
|
||||||
|
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME_2 }}
|
||||||
|
DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD_2 }}
|
||||||
|
publish_release:
|
||||||
|
needs: docker_publish
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
permissions:
|
|
||||||
packages: write
|
|
||||||
contents: read
|
|
||||||
attestations: write
|
|
||||||
id-token: write
|
|
||||||
steps:
|
steps:
|
||||||
- name: Check out the repo
|
- name: Checkout code
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
- name: Log in to Docker Hub
|
- name: Publish GitHub Release
|
||||||
uses: docker/login-action@f4ef78c080cd8ba55a85445d5b36e214a81df20a
|
env:
|
||||||
with:
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
username: ${{ secrets.DOCKER_USERNAME }}
|
run: |
|
||||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
gh release edit ${{ github.ref_name }} --draft=false
|
||||||
|
|
||||||
- name: Extract metadata (tags, labels) for Docker
|
discord_notification:
|
||||||
id: meta
|
needs: docker_publish
|
||||||
uses: docker/metadata-action@9ec57ed1fcdbf14dcef7dfbe97b2010124a938b7
|
uses: ./.github/workflows/discord.yml
|
||||||
with:
|
with:
|
||||||
images: solucetechnologies/portabase
|
discord_title: "New Release: ${{ github.ref_name }}"
|
||||||
|
discord_color: 3066993
|
||||||
- name: Set tags
|
discord_footer: "Portabase"
|
||||||
id: set-tags
|
secrets:
|
||||||
run: |
|
DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }}
|
||||||
echo "RELEASE_TAG=${GITHUB_REF#refs/tags/}" >> $GITHUB_ENV
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
TAGS="solucetechnologies/portabase:${GITHUB_REF#refs/tags/}"
|
|
||||||
if [ "${{ github.event.release.target_commitish }}" = "main" ]; then
|
|
||||||
TAGS="$TAGS,solucetechnologies/portabase:latest"
|
|
||||||
fi
|
|
||||||
echo "tags=$TAGS" >> $GITHUB_OUTPUT
|
|
||||||
|
|
||||||
- name: Build and push Docker image
|
|
||||||
id: push
|
|
||||||
uses: docker/build-push-action@v6
|
|
||||||
with:
|
|
||||||
context: .
|
|
||||||
file: ./docker/dockerfile/Dockerfile
|
|
||||||
push: true
|
|
||||||
tags: ${{ steps.set-tags.outputs.tags }}
|
|
||||||
target: prod
|
|
||||||
|
|
||||||
# Do not delete
|
|
||||||
# - name: Build and push Docker image
|
|
||||||
# id: push
|
|
||||||
# uses: docker/build-push-action@v6
|
|
||||||
# with:
|
|
||||||
# context: .
|
|
||||||
# file: ./docker/dockerfile/Dockerfile
|
|
||||||
# push: true
|
|
||||||
# tags: ${{ steps.meta.outputs.tags }}
|
|
||||||
# labels: ${{ steps.meta.outputs.labels }}
|
|
||||||
# target: prod
|
|
||||||
|
|
||||||
# - name: Generate artifact attestation
|
|
||||||
# uses: actions/attest-build-provenance@v2
|
|
||||||
# with:
|
|
||||||
# subject-name: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME}}
|
|
||||||
# subject-digest: ${{ steps.push.outputs.digest }}
|
|
||||||
# push-to-registry: true
|
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ on:
|
|||||||
branches: [ main ]
|
branches: [ main ]
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
sca-deps: # Dependency & container scan
|
sca-deps:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
@@ -17,12 +17,16 @@ jobs:
|
|||||||
ignore-unfixed: true
|
ignore-unfixed: true
|
||||||
|
|
||||||
|
|
||||||
secrets-gitleaks: # Secrets exposure
|
secrets-gitleaks:
|
||||||
|
if: github.event.pull_request.head.repo.fork == false
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0 # Fetch full history for gitleaks
|
fetch-depth: 0
|
||||||
- uses: gitleaks/gitleaks-action@v2
|
- uses: gitleaks/gitleaks-action@v2
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
GITLEAKS_LICENSE: ${{ secrets.GITLEAKS_LICENSE }}
|
||||||
with:
|
with:
|
||||||
config-path: .gitleaks.toml # Optional, if you have a custom config
|
config-path: .gitleaks.toml
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||||
|
.idea
|
||||||
|
.vscode
|
||||||
# dependencies
|
# dependencies
|
||||||
/node_modules
|
/node_modules
|
||||||
/.pnp
|
/.pnp
|
||||||
@@ -39,5 +40,10 @@ public/uploads/*
|
|||||||
!public/uploads/
|
!public/uploads/
|
||||||
|
|
||||||
private/uploads/*
|
private/uploads/*
|
||||||
|
private/keys/*
|
||||||
|
|
||||||
/.env
|
/.env
|
||||||
|
|
||||||
|
seeds/pocket-id/*.db
|
||||||
|
|
||||||
|
certificates
|
||||||
|
|||||||
@@ -2,9 +2,7 @@ title = "Custom gitleaks config"
|
|||||||
|
|
||||||
[allowlist]
|
[allowlist]
|
||||||
# Global allowlist patterns (won’t be flagged)
|
# Global allowlist patterns (won’t be flagged)
|
||||||
regexes = [
|
regexes = []
|
||||||
"s3SecretAccessKey: env.S3_SECRET_KEY ?? null",
|
|
||||||
]
|
|
||||||
paths = [
|
paths = [
|
||||||
"src/utils/init.ts"
|
"src/utils/init.ts"
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -1,8 +0,0 @@
|
|||||||
# Default ignored files
|
|
||||||
/shelf/
|
|
||||||
/workspace.xml
|
|
||||||
# Editor-based HTTP Client requests
|
|
||||||
/httpRequests/
|
|
||||||
# Datasource local storage ignored files
|
|
||||||
/dataSources/
|
|
||||||
/dataSources.local.xml
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<project version="4">
|
|
||||||
<component name="ProjectModuleManager">
|
|
||||||
<modules>
|
|
||||||
<module fileurl="file://$PROJECT_DIR$/.idea/portabase.iml" filepath="$PROJECT_DIR$/.idea/portabase.iml" />
|
|
||||||
</modules>
|
|
||||||
</component>
|
|
||||||
</project>
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<module type="JAVA_MODULE" version="4">
|
|
||||||
<component name="NewModuleRootManager" inherit-compiler-output="true">
|
|
||||||
<exclude-output />
|
|
||||||
<content url="file://$MODULE_DIR$">
|
|
||||||
<sourceFolder url="file://$MODULE_DIR$/backend" isTestSource="false" />
|
|
||||||
<excludeFolder url="file://$MODULE_DIR$/venv" />
|
|
||||||
</content>
|
|
||||||
<orderEntry type="inheritedJdk" />
|
|
||||||
<orderEntry type="sourceFolder" forTests="false" />
|
|
||||||
</component>
|
|
||||||
</module>
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<project version="4">
|
|
||||||
<component name="VcsDirectoryMappings">
|
|
||||||
<mapping directory="" vcs="Git" />
|
|
||||||
</component>
|
|
||||||
</project>
|
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
{
|
||||||
|
"git": {
|
||||||
|
"commit": true,
|
||||||
|
"commitMessage": "chore: release v${version}",
|
||||||
|
"requireCleanWorkingDir": true,
|
||||||
|
"tag": true,
|
||||||
|
"tagName": "${version}",
|
||||||
|
"push": true
|
||||||
|
},
|
||||||
|
"github": {
|
||||||
|
"release": true,
|
||||||
|
"tokenRef": "GITHUB_TOKEN",
|
||||||
|
"draft": true,
|
||||||
|
"tagName": "${version}",
|
||||||
|
"push": true
|
||||||
|
},
|
||||||
|
"hooks": {
|
||||||
|
"after:git:push": "sleep 5"
|
||||||
|
},
|
||||||
|
"plugins": {
|
||||||
|
"@release-it/conventional-changelog": {
|
||||||
|
"preset": {
|
||||||
|
"name": "conventionalcommits",
|
||||||
|
"types": [
|
||||||
|
{
|
||||||
|
"type": "feat",
|
||||||
|
"section": "✨ Features"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "fix",
|
||||||
|
"section": "🐛 Bug Fixes"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "perf",
|
||||||
|
"section": "⚡️ Performance Improvements"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "revert",
|
||||||
|
"section": "⏪️ Reverts"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "docs",
|
||||||
|
"section": "📝 Documentation",
|
||||||
|
"hidden": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "chore",
|
||||||
|
"section": "🔧 Chores",
|
||||||
|
"hidden": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"@release-it/bumper": {
|
||||||
|
"out": {
|
||||||
|
"file": "CITATION.cff",
|
||||||
|
"path": "version",
|
||||||
|
"type": "text/yaml"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
{
|
|
||||||
"WillLuke.nextjs.hasPrompted": true
|
|
||||||
}
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
nodeLinker: node-modules
|
|
||||||
|
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
cff-version: 1.2.0
|
||||||
|
title: Portabase
|
||||||
|
message: If you use this software, please cite it as below.
|
||||||
|
type: software
|
||||||
|
authors:
|
||||||
|
- family-names: Gauthereau
|
||||||
|
given-names: Charles
|
||||||
|
- family-names: Larcher
|
||||||
|
given-names: Killian
|
||||||
|
repository-code: https://github.com/Portabase/portabase
|
||||||
|
url: https://portabase.io
|
||||||
|
abstract: >-
|
||||||
|
Portabase is a free, open-source, self-hosted solution for database
|
||||||
|
administration, providing backup and restore capabilities, scheduling,
|
||||||
|
retention policies, notifications, and support for multiple storage backends.
|
||||||
|
Its headless agent architecture enables connection to multiple database
|
||||||
|
instances securely and efficiently.
|
||||||
|
keywords:
|
||||||
|
- docker
|
||||||
|
- kubernetes
|
||||||
|
- backups
|
||||||
|
- postgresql
|
||||||
|
- mysql
|
||||||
|
- mariadb
|
||||||
|
- devops
|
||||||
|
- database
|
||||||
|
- monitoring
|
||||||
|
- s3
|
||||||
|
- self-hosted
|
||||||
|
- system-administration
|
||||||
|
- web-ui
|
||||||
|
- agent
|
||||||
|
license: Apache-2.0
|
||||||
|
version: 1.4.22
|
||||||
|
date-released: '2026-03-02'
|
||||||
@@ -186,7 +186,7 @@
|
|||||||
same "printed page" as the copyright notice for easier
|
same "printed page" as the copyright notice for easier
|
||||||
identification within third-party archives.
|
identification within third-party archives.
|
||||||
|
|
||||||
Copyright 2024 Soluce Technologies
|
Copyright 2024 Portabase
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
|||||||
@@ -1,4 +1,48 @@
|
|||||||
CLUSTER_SCRIPT=docker/entrypoints/app-dev-entrypoint.sh
|
CLUSTER_SCRIPT=docker/entrypoints/app-dev-entrypoint.sh
|
||||||
|
|
||||||
up:
|
up:
|
||||||
@bash $(CLUSTER_SCRIPT)
|
@bash $(CLUSTER_SCRIPT)
|
||||||
|
|
||||||
|
seed-keycloak:
|
||||||
|
@[ $$(ls -1 seeds/keycloak/*.json 2>/dev/null | wc -l) -gt 0 ] || (echo "No realm export found in seeds/keycloak. Add an export from Keycloak first."; exit 1)
|
||||||
|
@docker compose -f docker-compose.func.yml stop keycloak >/dev/null 2>&1 || true
|
||||||
|
@docker compose -f docker-compose.func.yml rm -f -s keycloak >/dev/null 2>&1 || true
|
||||||
|
@docker volume rm portabase-dev-func_keycloak-data >/dev/null 2>&1 || true
|
||||||
|
@docker compose -f docker-compose.func.yml up -d keycloak
|
||||||
|
@echo "Keycloak seed import triggered from seeds/keycloak/*.json"
|
||||||
|
|
||||||
|
seed-pocket:
|
||||||
|
@[ -f seeds/pocket-id/portabase.zip ] || (echo "No export found in seeds/pocket-id. Add an export from Pocket ID first."; exit 1)
|
||||||
|
@docker compose -f docker-compose.func.yml stop pocket-id >/dev/null 2>&1 || true
|
||||||
|
@docker compose -f docker-compose.func.yml rm -f -s pocket-id >/dev/null 2>&1 || true
|
||||||
|
@docker volume rm portabase-dev-func_pocket-id-data >/dev/null 2>&1 || true
|
||||||
|
@docker compose -f docker-compose.func.yml run --rm -v ./seeds/pocket-id/portabase.zip:/tmp/portabase.zip pocket-id ./pocket-id import --yes --path /tmp/portabase.zip >/dev/null
|
||||||
|
@docker compose -f docker-compose.func.yml up -d pocket-id
|
||||||
|
@sleep 2
|
||||||
|
@docker compose -f docker-compose.func.yml exec pocket-id ./pocket-id one-time-access-token admin
|
||||||
|
@echo "Pocket ID data restored from seeds/pocket-id/portabase.zip"
|
||||||
|
|
||||||
|
seed-auth: seed-keycloak seed-pocket
|
||||||
|
|
||||||
|
pocket-token:
|
||||||
|
@docker compose -f docker-compose.func.yml exec pocket-id ./pocket-id one-time-access-token admin
|
||||||
|
|
||||||
|
export-pocket:
|
||||||
|
@echo "Exporting Pocket ID data to seeds/pocket-id/portabase.zip..."
|
||||||
|
@mkdir -p ./seeds/pocket-id
|
||||||
|
@docker compose -f docker-compose.func.yml exec pocket-id ./pocket-id export --path /tmp/portabase-export.zip
|
||||||
|
@docker compose -f docker-compose.func.yml cp pocket-id:/tmp/portabase-export.zip ./seeds/pocket-id/portabase.zip
|
||||||
|
@docker compose -f docker-compose.func.yml exec pocket-id rm /tmp/portabase-export.zip
|
||||||
|
@echo "Pocket ID data exported to seeds/pocket-id/portabase.zip"
|
||||||
|
|
||||||
|
export-keycloak:
|
||||||
|
@echo "Exporting Keycloak configuration and users to seeds/keycloak/*.json..."
|
||||||
|
@mkdir -p ./seeds/keycloak
|
||||||
|
@rm -f ./seeds/keycloak/*.json
|
||||||
|
@docker compose -f docker-compose.func.yml stop keycloak >/dev/null 2>&1
|
||||||
|
@docker rm -f kc-exporter >/dev/null 2>&1 || true
|
||||||
|
@docker compose -f docker-compose.func.yml run --name kc-exporter keycloak export --dir /tmp/kc-export --users realm_file
|
||||||
|
@docker cp kc-exporter:/tmp/kc-export/. ./seeds/keycloak/
|
||||||
|
@docker rm -f kc-exporter >/dev/null 2>&1
|
||||||
|
@docker compose -f docker-compose.func.yml start keycloak >/dev/null 2>&1
|
||||||
|
@echo "Keycloak configuration and users exported to seeds/keycloak/*.json"
|
||||||
|
|||||||
@@ -1,222 +1,90 @@
|
|||||||
<br />
|
<br />
|
||||||
<div align="center">
|
<div align="center">
|
||||||
<a href="https://portabase.io">
|
<a href="https://portabase.io">
|
||||||
<img src="/public/images/logo.png" alt="Logo" width="80" height="80">
|
<img src="/.github/assets/logo.png" alt="Logo" width="80" height="80">
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<h3 align="center">Portabase</h3>
|
<h3 align="center">Portabase</h3>
|
||||||
|
|
||||||
<p align="center">
|
<p align="center" style="margin-top: 20px; font-style: italic;">
|
||||||
Easily backup and restore your database instances with Portabase
|
<i>Portabase is a tool designed to simplify the backup and restoration of your database instances. It integrates seamlessly with <a href="https://github.com/Portabase/agent-rust">Portabase agents</a> for managing operations securely and efficiently.</i>
|
||||||
<br />
|
|
||||||
<a href="https://portabase.io"><strong>Explore the Docs »</strong></a>
|
|
||||||
<br />
|
|
||||||
<br />
|
|
||||||
<a href="https://www.youtube.com/watch?v=D9uFrGxLc4s">View Demo</a>
|
|
||||||
·
|
|
||||||
<a href="https://github.com/Soluce-Technologies/portabase/issues/new?labels=bug&template=bug-report---.md">Report Bug</a>
|
|
||||||
·
|
|
||||||
<a href="https://github.com/Soluce-Technologies/portabase/issues/new?labels=enhancement&template=feature-request---.md">Request Feature</a>
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
|
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
|
||||||
|
[](LICENSE)
|
||||||
|
[](https://hub.docker.com/r/solucetechnologies/portabase)
|
||||||
|
[](https://github.com/Portabase/portabase)
|
||||||
|
[](https://www.buymeacoffee.com/portabase)
|
||||||
|
|
||||||
|
[](https://www.postgresql.org/)
|
||||||
|
[](https://www.mysql.com/)
|
||||||
|
[](https://mariadb.org/)
|
||||||
|
[](https://sqlite.org/)
|
||||||
|
[](https://www.mongodb.com/)
|
||||||
|
[](https://github.com/Portabase/portabase)
|
||||||
|
[](https://github.com/Portabase/portabase)
|
||||||
|
|
||||||
|
[![NextJS][NextJS]][NextJS-url]
|
||||||
|
[![BetterAuth][BetterAuth]][BetterAuth-url]
|
||||||
|
[![Drizzle][Drizzle]][Drizzle-url]
|
||||||
|
[![ShadcnUI][ShadcnUI]][ShadcnUI-url]
|
||||||
|
[![Docker][Docker]][Docker-url]
|
||||||
|
|
||||||
|
<p>
|
||||||
|
<strong>
|
||||||
|
<a href="https://portabase.io">Website</a> •
|
||||||
|
<a href="https://portabase.io/docs">Documentation</a> •
|
||||||
|
<a href="https://www.youtube.com/watch?v=nSTzT27GgAg">Demo</a> •
|
||||||
|
<a href="https://portabase.io/docs/dashboard/setup">Installation</a> •
|
||||||
|
<a href="https://github.com/Portabase/portabase/issues/new?labels=bug&template=bug-report---.md">Report Bug</a> •
|
||||||
|
<a href="https://github.com/Portabase/portabase/issues/new?labels=enhancement&template=feature-request---.md">Request Feature</a>
|
||||||
|
</strong>
|
||||||
|
</p>
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
You have 4 ways to install Portabase:
|
||||||
|
|
||||||
|
- Automated CLI (recommended) - [details](https://portabase.io/docs/dashboard/setup#cli)
|
||||||
|
- Docker Run - [details](https://portabase.io/docs/dashboard/setup#docker)
|
||||||
|
- Docker Compose setup - [details](https://portabase.io/docs/dashboard/setup#docker-compose)
|
||||||
|
- Kubernetes with Helm (soon)
|
||||||
|
- Development setup - [details](https://portabase.io/docs/dashboard/setup#development)
|
||||||
|
|
||||||
## 📚 Table of Contents
|
**Ensure Docker is installed on your machine before getting started.**
|
||||||
|
|
||||||
- [About The Project](#about-the-project)
|
## Contributors
|
||||||
- [Built With](#built-with)
|
|
||||||
- [Getting Started](#getting-started)
|
|
||||||
- [Prerequisites](#prerequisites)
|
|
||||||
- [Installation](#installation)
|
|
||||||
- [Usage](#usage)
|
|
||||||
- [Roadmap](#roadmap)
|
|
||||||
- [Contributing](#contributing)
|
|
||||||
- [License](#license)
|
|
||||||
- [Contact](#contact)
|
|
||||||
- [Acknowledgments](#acknowledgments)
|
|
||||||
|
|
||||||
---
|
[](https://github.com/Portabase/portabase/graphs/contributors)
|
||||||
|
|
||||||
## About The Project
|
[](https://www.buymeacoffee.com/portabase)
|
||||||
|
|
||||||
**Portabase** is a server dashboard tool designed to simplify the backup and restoration of your database instances. It integrates seamlessly with Portabase agents for managing operations securely and efficiently.
|
## License
|
||||||
|
|
||||||
GitHub Repository: [Portabase](https://github.com/Soluce-Technologies/portabase)
|
|
||||||
|
|
||||||
### 🔧 Built With
|
|
||||||
|
|
||||||
- [![NextJS][NextJS]][NextJS-url] (v15 with App Router)
|
|
||||||
- [![Drizzle][Drizzle]][Drizzle-url]
|
|
||||||
- [![ShadcnUI][ShadcnUI]][ShadcnUI-url]
|
|
||||||
- BetterAuth
|
|
||||||
- React Email
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🚀 Getting Started
|
|
||||||
|
|
||||||
### Prerequisites
|
|
||||||
|
|
||||||
Ensure Docker is installed on your machine before getting started.
|
|
||||||
|
|
||||||
### Installation
|
|
||||||
|
|
||||||
To run Portabase locally:
|
|
||||||
|
|
||||||
1. Clone the repository:
|
|
||||||
```bash
|
|
||||||
git clone https://github.com/Soluce-Technologies/portabase
|
|
||||||
cd portabase
|
|
||||||
```
|
|
||||||
2. Start the development environment:
|
|
||||||
```bash
|
|
||||||
docker compose up
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🛠️ Usage
|
|
||||||
|
|
||||||
Portabase provides a dashboard to manage database instances and backups. It supports:
|
|
||||||
|
|
||||||
- PostgreSQL (current)
|
|
||||||
- MongoDB (coming soon)
|
|
||||||
- MySQL (coming soon)
|
|
||||||
|
|
||||||
You can access the dashboard at `http://localhost:3000` after starting the project.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🗺️ Roadmap
|
|
||||||
|
|
||||||
- [ ] Add changelog
|
|
||||||
- [ ] Enhance documentation
|
|
||||||
- [ ] Implement testing procedures
|
|
||||||
- [ ] Include release file
|
|
||||||
- [ ] Improve security
|
|
||||||
- [ ] Enhance UX/UI
|
|
||||||
- [ ] Improve workspace management system
|
|
||||||
- [x] Migrate to Drizzle ORM
|
|
||||||
- [ ] Add multi-language support
|
|
||||||
- [ ] Extend multi-database support:
|
|
||||||
- [x] PostgreSQL
|
|
||||||
- [ ] MongoDB
|
|
||||||
- [ ] MySQL
|
|
||||||
|
|
||||||
Check out [open issues](https://github.com/Soluce-Technologies/portabase/issues) for more.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🤝 Contributing
|
|
||||||
|
|
||||||
Contributions are welcome and appreciated! Here's how to get started:
|
|
||||||
|
|
||||||
1. Fork the repository
|
|
||||||
2. Create a new branch:
|
|
||||||
```bash
|
|
||||||
git checkout -b feature/YourFeature
|
|
||||||
```
|
|
||||||
3. Commit your changes:
|
|
||||||
```bash
|
|
||||||
git commit -m "Add YourFeature"
|
|
||||||
```
|
|
||||||
4. Push to the branch:
|
|
||||||
```bash
|
|
||||||
git push origin feature/YourFeature
|
|
||||||
```
|
|
||||||
5. Open a pull request
|
|
||||||
|
|
||||||
Give the project a ⭐ if you like it!
|
|
||||||
|
|
||||||
### Top Contributors
|
|
||||||
|
|
||||||
[](https://github.com/Soluce-Technologies/portabase/graphs/contributors)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Developer Notes
|
|
||||||
|
|
||||||
### Environment Variables
|
|
||||||
|
|
||||||
```yml
|
|
||||||
# Environment
|
|
||||||
NODE_ENV=production
|
|
||||||
|
|
||||||
# Database
|
|
||||||
DATABASE_URL=postgresql://devuser:changeme@db:5432/devdb?schema=public
|
|
||||||
|
|
||||||
# Project Info
|
|
||||||
NEXT_PUBLIC_PROJECT_NAME="Portabase"
|
|
||||||
NEXT_PUBLIC_PROJECT_DESCRIPTION="Portabase is a powerful database manager"
|
|
||||||
NEXT_PUBLIC_PROJECT_URL=http://app.portabase.io
|
|
||||||
PROJECT_SECRET=
|
|
||||||
|
|
||||||
# SMTP (Email)
|
|
||||||
SMTP_HOST=
|
|
||||||
SMTP_PORT=587
|
|
||||||
SMTP_USER=
|
|
||||||
SMTP_PASSWORD=
|
|
||||||
SMTP_FROM=
|
|
||||||
|
|
||||||
# Google OAuth
|
|
||||||
AUTH_GOOGLE_ID=
|
|
||||||
AUTH_GOOGLE_SECRET=
|
|
||||||
|
|
||||||
# S3/MinIO Configuration
|
|
||||||
S3_ENDPOINT=http://app.s3.portabase.io
|
|
||||||
S3_ACCESS_KEY=
|
|
||||||
S3_SECRET_KEY=
|
|
||||||
S3_BUCKET_NAME=portabase
|
|
||||||
S3_PORT=9000
|
|
||||||
S3_USE_SSL=true
|
|
||||||
|
|
||||||
# Storage Backend: 'local' or 's3'
|
|
||||||
STORAGE_TYPE=local
|
|
||||||
```
|
|
||||||
|
|
||||||
### Semantic Versioning
|
|
||||||
|
|
||||||
Use the following format for Docker image versioning:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
major.minor.patch-rc.release
|
|
||||||
# Example: 1.0.0-rc.1
|
|
||||||
|
|
||||||
major.minor.patch-rc.release-tag
|
|
||||||
# Example: 1.0.0-rc.1-dev
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📄 License
|
|
||||||
|
|
||||||
Distributed under the Apache License. See `LICENSE.txt` for more details.
|
Distributed under the Apache License. See `LICENSE.txt` for more details.
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📬 Contact
|
[Docker]: https://img.shields.io/badge/Docker-2496ED?logo=docker&logoColor=fff&style=for-the-badge
|
||||||
|
|
||||||
- Killian Larcher - killian.larcher@soluce-technologies.com
|
|
||||||
- Charles Gauthereau - charles.gauthereau@soluce-technologies.com
|
|
||||||
- Project Link: [Portabase GitHub](https://github.com/Soluce-Technologies/portabase)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🙏 Acknowledgments
|
|
||||||
|
|
||||||
Thanks to all contributors and the open-source community!
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
[NextJS]: https://img.shields.io/badge/next.js-000000?style=for-the-badge&logo=nextdotjs&logoColor=white
|
[NextJS]: https://img.shields.io/badge/next.js-000000?style=for-the-badge&logo=nextdotjs&logoColor=white
|
||||||
|
|
||||||
|
[BetterAuth]: https://img.shields.io/badge/Better%20Auth-FFF?logo=betterauth&logoColor=000&style=for-the-badge
|
||||||
|
|
||||||
[Drizzle]: https://img.shields.io/badge/Drizzle-111?style=for-the-badge&logo=Drizzle&logoColor=c5f74f
|
[Drizzle]: https://img.shields.io/badge/Drizzle-111?style=for-the-badge&logo=Drizzle&logoColor=c5f74f
|
||||||
|
|
||||||
[ShadcnUI]: https://img.shields.io/badge/shadcn/ui-000000?style=for-the-badge&logo=shadcn/ui&logoColor=white
|
[ShadcnUI]: https://img.shields.io/badge/shadcn/ui-000000?style=for-the-badge&logo=shadcn/ui&logoColor=white
|
||||||
|
|
||||||
[NextJS-url]: https://nextjs.org/
|
[NextJS-url]: https://nextjs.org/
|
||||||
|
|
||||||
|
[BetterAuth-url]: https://www.better-auth.com/
|
||||||
|
|
||||||
[Drizzle-url]: https://orm.drizzle.team/
|
[Drizzle-url]: https://orm.drizzle.team/
|
||||||
|
|
||||||
[ShadcnUI-url]: https://ui.shadcn.com/
|
[ShadcnUI-url]: https://ui.shadcn.com/
|
||||||
|
|
||||||
|
[Docker-url]: https://www.docker.com/
|
||||||
|
|||||||
@@ -0,0 +1,29 @@
|
|||||||
|
import { CardContent, CardHeader } from "@/components/ui/card";
|
||||||
|
|
||||||
|
import { TooltipProvider } from "@/components/ui/tooltip";
|
||||||
|
import { ForgotPasswordForm } from "@/components/wrappers/auth/login/forgot-password-form/forgot-password-form";
|
||||||
|
import { env } from "@/env.mjs";
|
||||||
|
import { CardAuth } from "@/features/layout/card-auth";
|
||||||
|
import { redirect } from "next/navigation";
|
||||||
|
|
||||||
|
export default async function RoutePage(props: { searchParams: Promise<{ callbackUrl: string | undefined }> }) {
|
||||||
|
if (env.AUTH_EMAIL_PASSWORD_ENABLED !== "true") {
|
||||||
|
redirect("/login");
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TooltipProvider>
|
||||||
|
<CardAuth className="w-full">
|
||||||
|
<CardHeader>
|
||||||
|
<div className="grid gap-2 text-center mb-2">
|
||||||
|
<h1 className="text-3xl font-bold">Reset password</h1>
|
||||||
|
<p className="text-balance text-muted-foreground">Enter your email address and we'll send you a link to reset your password.</p>
|
||||||
|
</div>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<ForgotPasswordForm />
|
||||||
|
</CardContent>
|
||||||
|
</CardAuth>
|
||||||
|
</TooltipProvider>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
import {CardContent, CardHeader} from "@/components/ui/card";
|
||||||
|
import {TooltipProvider} from "@/components/ui/tooltip";
|
||||||
|
import {GuardForm} from "@/components/wrappers/auth/guard/guard-form";
|
||||||
|
import {cookies} from "next/headers";
|
||||||
|
import {redirect} from "next/navigation";
|
||||||
|
import {CardAuth} from "@/features/layout/card-auth";
|
||||||
|
|
||||||
|
export default async function GuardPage() {
|
||||||
|
|
||||||
|
const cookieStore = await cookies();
|
||||||
|
const token = cookieStore.get("better-auth.two_factor")?.value || cookieStore.get("__Secure-better-auth.two_factor")?.value;
|
||||||
|
|
||||||
|
if (!token) {
|
||||||
|
redirect("/login");
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TooltipProvider>
|
||||||
|
<CardAuth className="w-full">
|
||||||
|
<CardHeader>
|
||||||
|
<div className="grid gap-2 text-center mb-2">
|
||||||
|
<h1 className="text-3xl font-bold">Two-factor verification</h1>
|
||||||
|
<p className="text-balance text-muted-foreground">Please enter the verification code generated
|
||||||
|
by your authentication app.</p>
|
||||||
|
</div>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<GuardForm/>
|
||||||
|
</CardContent>
|
||||||
|
</CardAuth>
|
||||||
|
</TooltipProvider>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,54 +1,38 @@
|
|||||||
"use client"
|
import React, { Suspense } from "react";
|
||||||
import React, {useEffect, useState} from "react";
|
import { redirect } from "next/navigation";
|
||||||
import {LayoutAdmin} from "@/components/layout";
|
import { currentUser } from "@/lib/auth/current-user";
|
||||||
import Image from "next/image";
|
import { AuthLogoSection } from "@/components/wrappers/auth/auth-logo-section";
|
||||||
import {env} from "@/env.mjs";
|
import { env } from "@/env.mjs";
|
||||||
import {useTheme} from "next-themes";
|
import { Heart } from "lucide-react";
|
||||||
import {useSession} from "@/lib/auth/auth-client";
|
import { ErrorLayout } from "@/components/wrappers/common/error-layout";
|
||||||
import {useRouter} from "next/navigation";
|
|
||||||
|
|
||||||
|
export default async function Layout({
|
||||||
|
children,
|
||||||
|
}: {
|
||||||
|
children: React.ReactNode;
|
||||||
|
}) {
|
||||||
|
const user = await currentUser();
|
||||||
|
|
||||||
export default function Layout({children}: { children: React.ReactNode }) {
|
if (user && !user.banned && user.role !== "pending") {
|
||||||
|
redirect("/dashboard/home");
|
||||||
|
}
|
||||||
|
|
||||||
const { resolvedTheme } = useTheme();
|
return (
|
||||||
const [mounted, setMounted] = useState(false);
|
<div className="flex min-h-screen flex-col justify-between py-10 sm:px-6 lg:px-8 ">
|
||||||
const router = useRouter();
|
<div className="flex flex-col items-center justify-center flex-1">
|
||||||
const { data: session } = useSession();
|
<div className="mx-auto w-full max-w-md">
|
||||||
|
<AuthLogoSection />
|
||||||
if (session && session.user && !session.user.banned && session.user.role !== "pending") {
|
<div className="mt-4">{children}</div>
|
||||||
router.replace("/dashboard/home");
|
|
||||||
}
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setMounted(true);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
if (!mounted) return null;
|
|
||||||
|
|
||||||
const imageTheme = resolvedTheme === "dark" ? "/images/logo-white.png" : "/images/logo-black.png";
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="flex min-h-full flex-1 flex-col justify-center py-12 sm:px-6 lg:px-8 ">
|
|
||||||
<div className="mx-auto w-full max-w-md">
|
|
||||||
<div className="sm:mx-auto sm:w-full sm:max-w-md flex items-center justify-center space-x-2">
|
|
||||||
<img
|
|
||||||
className="p-12 text-black dark:text-white"
|
|
||||||
src={imageTheme}
|
|
||||||
// loading="eager"
|
|
||||||
alt="Logo"
|
|
||||||
// width={1024}
|
|
||||||
// height={1024}
|
|
||||||
/>
|
|
||||||
<span
|
|
||||||
className="text-sm text-muted-foreground -ml-12 -mb-12">
|
|
||||||
v{env.NEXT_PUBLIC_PROJECT_VERSION}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div>{children}</div>
|
|
||||||
</div>
|
|
||||||
<footer className="py-4 text-center text-xs justify-items-end text-muted-foreground">
|
|
||||||
Powered by <span className="font-medium">Soluce Technologies</span>
|
|
||||||
</footer>
|
|
||||||
</div>
|
</div>
|
||||||
)
|
</div>
|
||||||
}
|
<footer className="mt-8 text-center text-xs text-muted-foreground flex flex-col gap-1">
|
||||||
|
<p className="flex items-center justify-center gap-1">
|
||||||
|
Made with{" "}
|
||||||
|
<Heart className="size-3 fill-red-500 text-red-500 animate-pulse" />{" "}
|
||||||
|
by <span className="font-medium text-foreground">Portabase</span>
|
||||||
|
</p>
|
||||||
|
<p>v{env.NEXT_PUBLIC_PROJECT_VERSION}</p>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,32 +1,60 @@
|
|||||||
"use client"
|
import { LoginForm } from "@/components/wrappers/auth/login/login-form/login-form";
|
||||||
|
import { Metadata } from "next";
|
||||||
|
import { SUPPORTED_PROVIDERS } from "@/lib/auth/config";
|
||||||
|
import { SocialAuthButtons } from "@/components/wrappers/auth/social-buttons";
|
||||||
|
import { TooltipProvider } from "@/components/ui/tooltip";
|
||||||
|
import { CardContent, CardHeader } from "@/components/ui/card";
|
||||||
|
import Link from "next/link";
|
||||||
|
import { Separator } from "@/components/ui/separator";
|
||||||
|
import { CardAuth } from "@/features/layout/card-auth";
|
||||||
|
import { env } from "@/env.mjs";
|
||||||
|
import { en } from "zod/v4/locales";
|
||||||
|
|
||||||
import {useEffect, useRef, useState} from "react";
|
export const metadata: Metadata = {
|
||||||
import {useSearchParams} from "next/navigation";
|
title: "Login",
|
||||||
|
};
|
||||||
|
|
||||||
import {LoginForm} from "@/components/wrappers/auth/login/login-form/login-form";
|
export default async function SignInPage() {
|
||||||
import {toast} from "sonner";
|
return (
|
||||||
|
<TooltipProvider>
|
||||||
|
<CardAuth className="w-full">
|
||||||
|
<CardHeader>
|
||||||
|
<div className="grid gap-2 text-center mb-2">
|
||||||
|
<h1 className="text-3xl font-bold">Login</h1>
|
||||||
|
<p className="text-balance text-muted-foreground">
|
||||||
|
Fill your login informations
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-4">
|
||||||
|
{env.AUTH_EMAIL_PASSWORD_ENABLED === "true" && (
|
||||||
|
<LoginForm isPasskeyEnabled={env.AUTH_PASSKEY_ENABLED === "true"} />
|
||||||
|
)}
|
||||||
|
|
||||||
export default function SignInPage(props: {
|
{env.AUTH_EMAIL_PASSWORD_ENABLED === "true" &&
|
||||||
searchParams: Promise<{ callbackUrl: string | undefined }>
|
SUPPORTED_PROVIDERS.filter((p) => !p.isManual && p.isActive)
|
||||||
}) {
|
.length > 0 && (
|
||||||
|
<div className="relative my-4 flex items-center justify-center overflow-hidden">
|
||||||
|
<Separator />
|
||||||
|
<div className="px-2 text-center text-sm">OR</div>
|
||||||
|
<Separator />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
const [urlParams, setUrlParams] = useState<URLSearchParams>();
|
{SUPPORTED_PROVIDERS.filter((p) => !p.isManual && p.isActive).length >
|
||||||
|
0 && <SocialAuthButtons providers={SUPPORTED_PROVIDERS} />}
|
||||||
|
|
||||||
useEffect(() => {
|
{env.AUTH_SIGNUP_ENABLED !== "true" ||
|
||||||
const urlParams = new URLSearchParams(window.location.search);
|
(env.AUTH_EMAIL_PASSWORD_ENABLED === "true" && (
|
||||||
setUrlParams(urlParams);
|
<div className="mt-4 text-center text-sm">
|
||||||
const error = urlParams.get("error");
|
Don't have an account ?{" "}
|
||||||
console.log(urlParams.get("redirect"));
|
<Link href="/register" className="underline">
|
||||||
if (error?.includes("pending")) {
|
Sign up
|
||||||
toast.error("Your account is not active.");
|
</Link>
|
||||||
urlParams.delete("error");
|
</div>
|
||||||
window.history.replaceState({}, document.title, window.location.pathname + "?" + urlParams.toString());
|
))}
|
||||||
}
|
</CardContent>
|
||||||
}, []);
|
</CardAuth>
|
||||||
|
</TooltipProvider>
|
||||||
return (
|
);
|
||||||
<div className="mx-auto grid w-full gap-6">
|
}
|
||||||
<LoginForm/>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,10 +1,24 @@
|
|||||||
import {PageParams} from "@/types/next";
|
import { PageParams } from "@/types/next";
|
||||||
import {RegisterForm} from "@/components/wrappers/auth/register/register-form/register-form";
|
import { RegisterForm } from "@/components/wrappers/auth/register/register-form/register-form";
|
||||||
|
import { Metadata } from "next";
|
||||||
|
import { redirect } from "next/navigation";
|
||||||
|
import { env } from "@/env.mjs";
|
||||||
|
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
title: "Register",
|
||||||
|
};
|
||||||
|
|
||||||
export default async function RoutePage(props: PageParams<{}>) {
|
export default async function RoutePage(props: PageParams<{}>) {
|
||||||
return (
|
if (
|
||||||
<div className="mx-auto grid w-full gap-6">
|
env.AUTH_SIGNUP_ENABLED !== "true" ||
|
||||||
<RegisterForm/>
|
env.AUTH_EMAIL_PASSWORD_ENABLED !== "true"
|
||||||
</div>
|
) {
|
||||||
)
|
redirect("/login");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="mx-auto grid w-full gap-6">
|
||||||
|
<RegisterForm />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,64 @@
|
|||||||
|
import { CardContent, CardHeader } from "@/components/ui/card";
|
||||||
|
import { TooltipProvider } from "@/components/ui/tooltip";
|
||||||
|
import { ResetPasswordForm } from "@/components/wrappers/auth/login/reset-password-form/reset-password-form";
|
||||||
|
import { redirect } from "next/navigation";
|
||||||
|
import { auth } from "@/lib/auth/auth";
|
||||||
|
import { Avatar, AvatarImage, AvatarFallback } from "@radix-ui/react-avatar";
|
||||||
|
import { CardAuth } from "@/features/layout/card-auth";
|
||||||
|
import { env } from "@/env.mjs";
|
||||||
|
|
||||||
|
export default async function RoutePage(props: { searchParams: Promise<{ token: string | undefined }> }) {
|
||||||
|
if (env.AUTH_EMAIL_PASSWORD_ENABLED !== "true") {
|
||||||
|
return redirect("/login");
|
||||||
|
}
|
||||||
|
|
||||||
|
const { token } = await props.searchParams;
|
||||||
|
|
||||||
|
if (!token) {
|
||||||
|
return redirect(`/login?error=invalid_or_expired_token`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const verification = await (await auth.$context).internalAdapter.findVerificationValue(`reset-password:${token}`);
|
||||||
|
|
||||||
|
if (!verification || verification.expiresAt < new Date()) {
|
||||||
|
return redirect(`/login?error=invalid_or_expired_token`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const user = await (await auth.$context).internalAdapter.findUserById(verification.value);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TooltipProvider>
|
||||||
|
<CardAuth className="w-full">
|
||||||
|
<CardHeader className="space-y-4">
|
||||||
|
<div className="space-y-1 text-center">
|
||||||
|
<h1 className="text-2xl font-bold tracking-tight">Set a new password</h1>
|
||||||
|
<p className="text-sm text-muted-foreground text-balance">Please enter your new password below.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-col items-center space-y-2 text-center">
|
||||||
|
<Avatar className="relative flex h-16 w-16 shrink-0 overflow-hidden rounded-full border">
|
||||||
|
<AvatarImage src={user!.image ?? ""} alt={user!.name} className="aspect-square h-full w-full object-cover" />
|
||||||
|
<AvatarFallback className="flex h-full w-full items-center justify-center rounded-full bg-muted text-2xl font-medium text-muted-foreground">
|
||||||
|
{user!.name
|
||||||
|
.split(" ")
|
||||||
|
.map((n) => n[0])
|
||||||
|
.join("")
|
||||||
|
.toUpperCase()
|
||||||
|
.slice(0, 2)}
|
||||||
|
</AvatarFallback>
|
||||||
|
</Avatar>
|
||||||
|
|
||||||
|
<div className="flex flex-col items-center">
|
||||||
|
<div className="font-semibold text-lg">{user!.name}</div>
|
||||||
|
<div className="text-sm text-muted-foreground">{user!.email}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardHeader>
|
||||||
|
|
||||||
|
<CardContent>
|
||||||
|
<ResetPasswordForm />
|
||||||
|
</CardContent>
|
||||||
|
</CardAuth>
|
||||||
|
</TooltipProvider>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
import {notFound} from "next/navigation";
|
||||||
|
import {eq} from "drizzle-orm";
|
||||||
|
import {db} from "@/db";
|
||||||
|
import * as drizzleDb from "@/db";
|
||||||
|
import {PageParams} from "@/types/next";
|
||||||
|
import {Page} from "@/features/layout/page";
|
||||||
|
import {OrganizationManagement} from "@/components/wrappers/dashboard/admin/organizations/organization/organization-management";
|
||||||
|
import {buildOrganizationWithMembers} from "@/utils/common";
|
||||||
|
import {isUUID} from "@/utils/text";
|
||||||
|
import {user} from "@/db/schema/02_user";
|
||||||
|
import {invitation} from "@/db/schema/05_invitation";
|
||||||
|
import {member} from "@/db/schema/04_member";
|
||||||
|
import {organization} from "@/db/schema/03_organization";
|
||||||
|
import {user as drizzleUser} from "@/db/schema/02_user";
|
||||||
|
|
||||||
|
|
||||||
|
export default async function RoutePage(props: PageParams<{ organizationId: string }>) {
|
||||||
|
const {organizationId} = await props.params;
|
||||||
|
|
||||||
|
if (!organizationId) {
|
||||||
|
return notFound();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isUUID(organizationId)) {
|
||||||
|
return notFound();
|
||||||
|
}
|
||||||
|
|
||||||
|
const users = await db.select().from(drizzleUser);
|
||||||
|
|
||||||
|
const organizationData = await db
|
||||||
|
.select({organization, member, user, invitation})
|
||||||
|
.from(organization)
|
||||||
|
.leftJoin(member, eq(drizzleDb.schemas.organization.id, member.organizationId))
|
||||||
|
.leftJoin(invitation, eq(drizzleDb.schemas.invitation.id, invitation.organizationId))
|
||||||
|
.leftJoin(user, eq(drizzleDb.schemas.member.userId, user.id))
|
||||||
|
.where(eq(organization.id, organizationId));
|
||||||
|
|
||||||
|
const formattedData = buildOrganizationWithMembers(organizationData);
|
||||||
|
|
||||||
|
if (!formattedData) return notFound();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Page>
|
||||||
|
<OrganizationManagement organization={formattedData} users={users}/>
|
||||||
|
</Page>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
import {PageParams} from "@/types/next";
|
||||||
|
import {Page, PageActions, PageContent, PageHeader, PageTitle} from "@/features/layout/page";
|
||||||
|
import {db} from "@/db";
|
||||||
|
import {
|
||||||
|
AdminOrganizationAddModal
|
||||||
|
} from "@/components/wrappers/dashboard/admin/organizations/organization/admin-organization-add-modal";
|
||||||
|
import {AdminOrganizationList} from "@/components/wrappers/dashboard/admin/organizations/organization/admin-orgnization-list";
|
||||||
|
import {isNull} from "drizzle-orm";
|
||||||
|
|
||||||
|
export default async function RoutePage(props: PageParams<{}>) {
|
||||||
|
|
||||||
|
const organizations = await db.query.organization.findMany({
|
||||||
|
where: (fields) => isNull(fields.deletedAt),
|
||||||
|
with: {
|
||||||
|
members: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Page>
|
||||||
|
<PageHeader className="flex flex-col">
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<PageTitle className="mb-3">Active organizations</PageTitle>
|
||||||
|
<PageActions>
|
||||||
|
<AdminOrganizationAddModal/>
|
||||||
|
</PageActions>
|
||||||
|
</div>
|
||||||
|
</PageHeader>
|
||||||
|
<PageContent className="flex flex-col gap-5">
|
||||||
|
<AdminOrganizationList organizations={organizations}/>
|
||||||
|
</PageContent>
|
||||||
|
</Page>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
import {PageParams} from "@/types/next";
|
|
||||||
import {Page, PageContent, PageHeader, PageTitle} from "@/features/layout/page";
|
|
||||||
import {AdminTabs} from "@/components/wrappers/dashboard/admin/admin-tabs";
|
|
||||||
import {db} from "@/db";
|
|
||||||
import {isNull} from "drizzle-orm";
|
|
||||||
|
|
||||||
|
|
||||||
export default async function RoutePage(props: PageParams<{}>) {
|
|
||||||
|
|
||||||
const users = await db.query.user.findMany({
|
|
||||||
where: (fields) => isNull(fields.deletedAt),
|
|
||||||
with: {
|
|
||||||
accounts: true
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const settings = await db.query.setting.findFirst({
|
|
||||||
where: (fields, {eq}) => eq(fields.name, "system"),
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Page>
|
|
||||||
<PageHeader>
|
|
||||||
<PageTitle>Administration Panel</PageTitle>
|
|
||||||
</PageHeader>
|
|
||||||
<PageContent>
|
|
||||||
<AdminTabs settings={settings!} users={users}/>
|
|
||||||
</PageContent>
|
|
||||||
</Page>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
import {PageParams} from "@/types/next";
|
||||||
|
import {Page, PageContent, PageHeader, PageTitle} from "@/features/layout/page";
|
||||||
|
import {db} from "@/db";
|
||||||
|
import {notFound} from "next/navigation";
|
||||||
|
import {SettingsTabs} from "@/components/wrappers/dashboard/admin/settings/settings-tabs";
|
||||||
|
import {desc, isNull} from "drizzle-orm";
|
||||||
|
import * as drizzleDb from "@/db";
|
||||||
|
import {StorageChannelWith} from "@/db/schema/12_storage-channel";
|
||||||
|
|
||||||
|
export default async function RoutePage(props: PageParams<{}>) {
|
||||||
|
|
||||||
|
const settings = await db.query.setting.findFirst({
|
||||||
|
where: (fields, {eq}) => eq(fields.name, "system"),
|
||||||
|
});
|
||||||
|
|
||||||
|
const storageChannels = await db.query.storageChannel.findMany({
|
||||||
|
with: {
|
||||||
|
organizations: true
|
||||||
|
},
|
||||||
|
where: isNull(drizzleDb.schemas.storageChannel.organizationId),
|
||||||
|
orderBy: desc(drizzleDb.schemas.storageChannel.createdAt)
|
||||||
|
}) as StorageChannelWith[]
|
||||||
|
|
||||||
|
if (!settings || !storageChannels ) {
|
||||||
|
notFound()
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Page>
|
||||||
|
<PageHeader className="flex flex-col">
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<PageTitle className="mb-3">System settings</PageTitle>
|
||||||
|
</div>
|
||||||
|
</PageHeader>
|
||||||
|
<PageContent className="flex flex-col gap-5">
|
||||||
|
<SettingsTabs storageChannels={storageChannels} settings={settings} />
|
||||||
|
</PageContent>
|
||||||
|
</Page>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
import {PageParams} from "@/types/next";
|
||||||
|
import {Page, PageActions, PageContent, PageHeader, PageTitle} from "@/features/layout/page";
|
||||||
|
import {db} from "@/db";
|
||||||
|
import {desc, isNull} from "drizzle-orm";
|
||||||
|
import {AdminUserList} from "@/components/wrappers/dashboard/admin/users/admin-user-list";
|
||||||
|
import {AdminUserAddModal} from "@/components/wrappers/dashboard/admin/users/admin-user-add-modal";
|
||||||
|
import {SUPPORTED_PROVIDERS} from "@/lib/auth/config";
|
||||||
|
|
||||||
|
export default async function RoutePage(props: PageParams<{}>) {
|
||||||
|
|
||||||
|
const users = await db.query.user.findMany({
|
||||||
|
where: (fields) => isNull(fields.deletedAt),
|
||||||
|
with: {
|
||||||
|
accounts: true
|
||||||
|
},
|
||||||
|
orderBy: (fields) => desc(fields.createdAt),
|
||||||
|
|
||||||
|
});
|
||||||
|
const organizations = await db.query.organization.findMany({
|
||||||
|
with: {
|
||||||
|
members: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const credentialProvider = SUPPORTED_PROVIDERS.find(p => p.id === 'credential');
|
||||||
|
const isPasswordAuthEnabled = credentialProvider?.isActive || false;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Page>
|
||||||
|
<PageHeader className="flex flex-col">
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<PageTitle className="mb-3">Active users</PageTitle>
|
||||||
|
<PageActions>
|
||||||
|
<AdminUserAddModal organizations={organizations}/>
|
||||||
|
</PageActions>
|
||||||
|
</div>
|
||||||
|
</PageHeader>
|
||||||
|
<PageContent className="flex flex-col gap-5">
|
||||||
|
<AdminUserList users={users} isPasswordAuthEnabled={isPasswordAuthEnabled}/>
|
||||||
|
</PageContent>
|
||||||
|
</Page>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
import { PageParams } from "@/types/next";
|
|
||||||
import { Page, PageContent, PageHeader, PageTitle } from "@/features/layout/page";
|
|
||||||
import { AgentForm } from "@/components/wrappers/dashboard/agent/agent-form/agent-form";
|
|
||||||
import { notFound } from "next/navigation";
|
|
||||||
import { db } from "@/db";
|
|
||||||
|
|
||||||
export default async function RoutePage(
|
|
||||||
props: PageParams<{
|
|
||||||
agentId: string;
|
|
||||||
}>
|
|
||||||
) {
|
|
||||||
const { agentId } = await props.params;
|
|
||||||
|
|
||||||
const agent = await db.query.agent.findFirst({
|
|
||||||
where: (fields, { eq }) => eq(fields.id, agentId),
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!agent) {
|
|
||||||
notFound();
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Page>
|
|
||||||
<PageHeader>
|
|
||||||
<PageTitle>Edit {agent.name}</PageTitle>
|
|
||||||
</PageHeader>
|
|
||||||
<PageContent>
|
|
||||||
<AgentForm defaultValues={agent} agentId={agent.id} />
|
|
||||||
</PageContent>
|
|
||||||
</Page>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,124 +1,69 @@
|
|||||||
import Link from "next/link";
|
import { PageParams } from "@/types/next";
|
||||||
import {GearIcon} from "@radix-ui/react-icons";
|
import {
|
||||||
import {PageParams} from "@/types/next";
|
Page,
|
||||||
import {Page, PageActions, PageContent, PageDescription, PageTitle} from "@/features/layout/page";
|
PageContent,
|
||||||
import {formatDateLastContact} from "@/utils/date-formatting";
|
PageDescription,
|
||||||
import {buttonVariants} from "@/components/ui/button";
|
PageTitle,
|
||||||
import {Card, CardContent, CardHeader} from "@/components/ui/card";
|
} from "@/features/layout/page";
|
||||||
import {CardsWithPagination} from "@/components/wrappers/common/cards-with-pagination";
|
|
||||||
import {DatabaseCard} from "@/components/wrappers/dashboard/projects/project-card/project-database-card";
|
|
||||||
import {AgentCardKey} from "@/components/wrappers/dashboard/agent/agent-card-key/agent-card-key";
|
|
||||||
import { db } from "@/db";
|
import { db } from "@/db";
|
||||||
import * as drizzleDb from "@/db";
|
import * as drizzleDb from "@/db";
|
||||||
import {and, eq} from "drizzle-orm";
|
import { eq } from "drizzle-orm";
|
||||||
import {notFound} from "next/navigation";
|
import { notFound } from "next/navigation";
|
||||||
import {
|
import { ButtonDeleteAgent } from "@/components/wrappers/dashboard/agent/button-delete-agent/button-delete-agent";
|
||||||
ButtonDeleteProject
|
import { capitalizeFirstLetter } from "@/utils/text";
|
||||||
} from "@/components/wrappers/dashboard/projects/button-delete-project/button-delete-project";
|
import { generateEdgeKey } from "@/utils/edge_key";
|
||||||
import {ButtonDeleteAgent} from "@/components/wrappers/dashboard/agent/button-delete-agent/button-delete-agent";
|
import { getServerUrl } from "@/utils/get-server-url";
|
||||||
|
import { AgentContentPage } from "@/components/wrappers/dashboard/agent/agent-content";
|
||||||
|
import { AgentDialog } from "@/features/agents/components/agent.dialog";
|
||||||
|
import { AgentType } from "@/features/agents/agents.schema";
|
||||||
|
|
||||||
|
export default async function RoutePage(
|
||||||
|
props: PageParams<{ agentId: string }>,
|
||||||
|
) {
|
||||||
|
const { agentId } = await props.params;
|
||||||
|
|
||||||
export default async function RoutePage(props: PageParams<{ agentId: string }>) {
|
const agent = await db.query.agent.findFirst({
|
||||||
|
where: eq(drizzleDb.schemas.agent.id, agentId),
|
||||||
|
with: {
|
||||||
|
databases: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
const {agentId} = await props.params
|
if (!agent) {
|
||||||
|
notFound();
|
||||||
|
}
|
||||||
|
|
||||||
const agent = await db.query.agent.findFirst({
|
const edgeKey = await generateEdgeKey(getServerUrl(), agent.id);
|
||||||
where: eq(drizzleDb.schemas.agent.id, agentId),
|
|
||||||
with: {
|
|
||||||
databases: true
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Page>
|
||||||
if (!agent) {
|
<div className="justify-between gap-2 sm:flex">
|
||||||
notFound()
|
<PageTitle className="flex flex-col md:flex-row items-center justify-between w-full ">
|
||||||
}
|
<div className="min-w-full md:min-w-fit ">
|
||||||
//
|
{capitalizeFirstLetter(agent.name)}
|
||||||
// const databaseId = 'db-123';
|
</div>
|
||||||
//
|
<div className="flex items-center gap-2 md:justify-between w-full ">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<AgentDialog
|
||||||
|
agent={agent as AgentType & { id: string }}
|
||||||
|
typeTrigger={"edit"}
|
||||||
|
/>
|
||||||
|
|
||||||
// const totalBackupsResult = await db
|
|
||||||
// .select({ count: drizzleDb.schemas.backup.id })
|
|
||||||
// .from(drizzleDb.schemas.backup)
|
|
||||||
// .where(eq(drizzleDb.schemas.backup.databaseId, databaseId))
|
|
||||||
// .execute();
|
|
||||||
//
|
|
||||||
// const totalBackups = totalBackupsResult.length;
|
|
||||||
//
|
|
||||||
// const successfulBackupsResult = await db
|
|
||||||
// .select({ count: drizzleDb.schemas.backup.id })
|
|
||||||
// .from(drizzleDb.schemas.backup)
|
|
||||||
// .where(
|
|
||||||
// and(
|
|
||||||
// eq(drizzleDb.schemas.backup.databaseId, databaseId),
|
|
||||||
// eq(drizzleDb.schemas.backup.status, "success")
|
|
||||||
// )
|
|
||||||
// )
|
|
||||||
// .execute();
|
|
||||||
//
|
|
||||||
//
|
|
||||||
// const successfulBackups = successfulBackupsResult.length;
|
|
||||||
//
|
|
||||||
// const successRate =
|
|
||||||
// totalBackups > 0 ? (successfulBackups / totalBackups) * 100 : null;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Page>
|
|
||||||
<div className="justify-between gap-2 sm:flex">
|
|
||||||
<PageTitle className="flex items-center">
|
|
||||||
{agent.name}
|
|
||||||
<Link className={buttonVariants({variant: "outline"})}
|
|
||||||
href={`/dashboard/agents/${agent.id}/edit`}>
|
|
||||||
<GearIcon className="w-7 h-7"/>
|
|
||||||
</Link>
|
|
||||||
</PageTitle>
|
|
||||||
<PageActions className="justify-between">
|
|
||||||
<ButtonDeleteAgent agentId={agentId} text={"Delete Agent"} />
|
|
||||||
</PageActions>
|
|
||||||
</div>
|
</div>
|
||||||
<PageDescription className="mt-5 sm:mt-0">{agent.description}</PageDescription>
|
<div className="flex items-center gap-2">
|
||||||
<PageContent className="flex flex-col w-full h-full">
|
<ButtonDeleteAgent agentId={agentId} text={"Delete Agent"} />
|
||||||
<div className="flex flex-col sm:flex-row sm:justify-between gap-8 mb-6">
|
</div>
|
||||||
<Card className="w-full sm:w-auto flex-1">
|
</div>
|
||||||
<CardHeader className="font-bold text-xl">
|
</PageTitle>
|
||||||
Databases
|
</div>
|
||||||
</CardHeader>
|
|
||||||
<CardContent>{agent.databases.length}</CardContent>
|
|
||||||
</Card>
|
|
||||||
<Card className="w-full sm:w-auto flex-1">
|
|
||||||
<CardHeader className="font-bold text-xl">
|
|
||||||
Success rate
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
{/*{successRate ?? "Unavailable for now."}*/}
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
<Card className="w-full sm:w-auto flex-1">
|
|
||||||
<CardHeader className="font-bold text-xl">
|
|
||||||
Last contact
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
{formatDateLastContact(agent.lastContact)}
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
</div>
|
{agent.description && (
|
||||||
<Card className="w-full sm:w-auto flex-1 mb-4">
|
<PageDescription className="mt-5 sm:mt-0">
|
||||||
<CardHeader className="font-bold text-xl">
|
{agent.description}
|
||||||
Edge Key
|
</PageDescription>
|
||||||
</CardHeader>
|
)}
|
||||||
<CardContent>
|
<PageContent className="flex flex-col w-full h-full justify-between gap-6">
|
||||||
<AgentCardKey agent={agent}/>
|
<AgentContentPage agent={agent} edgeKey={edgeKey} />
|
||||||
</CardContent>
|
</PageContent>
|
||||||
</Card>
|
</Page>
|
||||||
<CardsWithPagination cardsPerPage={2} data={agent.databases} cardItem={DatabaseCard}/>
|
);
|
||||||
</PageContent>
|
}
|
||||||
</Page>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,16 +0,0 @@
|
|||||||
import { PageParams } from "@/types/next";
|
|
||||||
import { Page, PageContent, PageHeader, PageTitle } from "@/features/layout/page";
|
|
||||||
import { AgentForm } from "@/components/wrappers/dashboard/agent/agent-form/agent-form";
|
|
||||||
|
|
||||||
export default async function RoutePage(props: PageParams<{}>) {
|
|
||||||
return (
|
|
||||||
<Page>
|
|
||||||
<PageHeader>
|
|
||||||
<PageTitle>Create new agent</PageTitle>
|
|
||||||
</PageHeader>
|
|
||||||
<PageContent>
|
|
||||||
<AgentForm />
|
|
||||||
</PageContent>
|
|
||||||
</Page>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,26 +1,28 @@
|
|||||||
import { PageParams } from "@/types/next";
|
import {PageParams} from "@/types/next";
|
||||||
import { AgentCard } from "@/components/wrappers/dashboard/agent/agent-card/agent-card";
|
import {AgentCard} from "@/components/wrappers/dashboard/agent/agent-card/agent-card";
|
||||||
import { CardsWithPagination } from "@/components/wrappers/common/cards-with-pagination";
|
import {CardsWithPagination} from "@/components/wrappers/common/cards-with-pagination";
|
||||||
import { Button } from "@/components/ui/button";
|
import {Page, PageActions, PageContent, PageHeader, PageTitle} from "@/features/layout/page";
|
||||||
import Link from "next/link";
|
import {notFound} from "next/navigation";
|
||||||
import { Page, PageActions, PageContent, PageHeader, PageTitle } from "@/features/layout/page";
|
import {db} from "@/db";
|
||||||
import { notFound } from "next/navigation";
|
|
||||||
import { db } from "@/db";
|
|
||||||
import * as drizzleDb from "@/db";
|
import * as drizzleDb from "@/db";
|
||||||
|
import {desc, eq, not} from "drizzle-orm";
|
||||||
|
import {Metadata} from "next";
|
||||||
|
import {AgentDialog} from "@/features/agents/components/agent.dialog";
|
||||||
|
|
||||||
import {and, eq, not} from "drizzle-orm";
|
export const metadata: Metadata = {
|
||||||
import {Plus} from "lucide-react";
|
title: "Agents",
|
||||||
import {cn} from "@/lib/utils";
|
};
|
||||||
import {EmptyStatePlaceholder} from "@/components/wrappers/common/empty-state-placeholder";
|
|
||||||
export const dynamic = "force-dynamic";
|
|
||||||
|
|
||||||
export default async function RoutePage(props: PageParams<{}>) {
|
export default async function RoutePage(props: PageParams<{}>) {
|
||||||
|
|
||||||
const agents = await db.query.agent.findMany({
|
const agents = await db.query.agent.findMany({
|
||||||
where: not(eq(drizzleDb.schemas.agent.isArchived, true))
|
where: not(eq(drizzleDb.schemas.agent.isArchived, true)),
|
||||||
|
with: {
|
||||||
|
databases: true
|
||||||
|
},
|
||||||
|
orderBy: (fields) => desc(fields.lastContact),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
if (!agents) {
|
if (!agents) {
|
||||||
notFound();
|
notFound();
|
||||||
}
|
}
|
||||||
@@ -31,22 +33,17 @@ export default async function RoutePage(props: PageParams<{}>) {
|
|||||||
<PageTitle>Agents</PageTitle>
|
<PageTitle>Agents</PageTitle>
|
||||||
{agents.length > 0 && (
|
{agents.length > 0 && (
|
||||||
<PageActions>
|
<PageActions>
|
||||||
<Link href={"/dashboard/agents/new"}>
|
<AgentDialog typeTrigger={"create"} />
|
||||||
<Button>+ Create Agent</Button>
|
|
||||||
</Link>
|
|
||||||
</PageActions>
|
</PageActions>
|
||||||
)}
|
)}
|
||||||
</PageHeader>
|
</PageHeader>
|
||||||
<PageContent>
|
<PageContent>
|
||||||
{agents.length > 0 ? (
|
{agents.length > 0 ? (
|
||||||
<CardsWithPagination data={agents} cardItem={AgentCard} cardsPerPage={4} numberOfColumns={1} />
|
<CardsWithPagination data={agents} cardItem={AgentCard} cardsPerPage={4} numberOfColumns={1}/>
|
||||||
) : (
|
) : (
|
||||||
<EmptyStatePlaceholder
|
<AgentDialog typeTrigger="empty"/>
|
||||||
url={"/dashboard/agents/new"}
|
|
||||||
text={"Create new Agent"}
|
|
||||||
/>
|
|
||||||
)}
|
)}
|
||||||
</PageContent>
|
</PageContent>
|
||||||
</Page>
|
</Page>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
import {PageParams} from "@/types/next";
|
||||||
|
import {Page, PageActions, PageContent, PageHeader, PageTitle} from "@/features/layout/page";
|
||||||
|
import {Metadata} from "next";
|
||||||
|
import {db} from "@/db";
|
||||||
|
import {notificationChannel, NotificationChannelWith} from "@/db/schema/09_notification-channel";
|
||||||
|
import {desc, isNull} from "drizzle-orm";
|
||||||
|
import {ChannelsSection} from "@/components/wrappers/dashboard/admin/channels/channels-section";
|
||||||
|
import {ChannelAddEditModal} from "@/components/wrappers/dashboard/admin/channels/channel/channel-add-edit-modal";
|
||||||
|
import * as drizzleDb from "@/db";
|
||||||
|
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
title: "Notification Channels",
|
||||||
|
};
|
||||||
|
|
||||||
|
export default async function RoutePage(props: PageParams<{}>) {
|
||||||
|
|
||||||
|
const notificationChannels = await db.query.notificationChannel.findMany({
|
||||||
|
with: {
|
||||||
|
organizations: true
|
||||||
|
},
|
||||||
|
where: isNull(drizzleDb.schemas.notificationChannel.organizationId),
|
||||||
|
orderBy: desc(notificationChannel.createdAt)
|
||||||
|
}) as NotificationChannelWith[]
|
||||||
|
|
||||||
|
const organizations = await db.query.organization.findMany({
|
||||||
|
where: (fields) => isNull(fields.deletedAt),
|
||||||
|
with: {
|
||||||
|
members: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Page>
|
||||||
|
<PageHeader>
|
||||||
|
<PageTitle>Notification channels</PageTitle>
|
||||||
|
<PageActions>
|
||||||
|
<ChannelAddEditModal kind="notification" adminView={false}/>
|
||||||
|
</PageActions>
|
||||||
|
</PageHeader>
|
||||||
|
<PageContent>
|
||||||
|
<ChannelsSection kind="notification" organizations={organizations}
|
||||||
|
channels={notificationChannels}/>
|
||||||
|
</PageContent>
|
||||||
|
</Page>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
import {PageParams} from "@/types/next";
|
||||||
|
import {Page, PageContent, PageHeader, PageTitle} from "@/features/layout/page";
|
||||||
|
import {Metadata} from "next";
|
||||||
|
import {NotificationLogsList} from "@/components/wrappers/dashboard/admin/notifications/logs/notification-logs-list";
|
||||||
|
import {notFound} from "next/navigation";
|
||||||
|
import {getNotificationHistory} from "@/db/services/notification-log";
|
||||||
|
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
title: "Activity logs",
|
||||||
|
};
|
||||||
|
|
||||||
|
export default async function RoutePage(props: PageParams<{}>) {
|
||||||
|
|
||||||
|
const notificationLogs = await getNotificationHistory()
|
||||||
|
|
||||||
|
if (!notificationLogs) {
|
||||||
|
notFound();
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Page>
|
||||||
|
<PageHeader>
|
||||||
|
<PageTitle>Activity logs</PageTitle>
|
||||||
|
</PageHeader>
|
||||||
|
<PageContent>
|
||||||
|
<NotificationLogsList notificationLogs={notificationLogs} />
|
||||||
|
</PageContent>
|
||||||
|
</Page>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
import {PageParams} from "@/types/next";
|
||||||
|
import {Page, PageActions, PageContent, PageHeader, PageTitle} from "@/features/layout/page";
|
||||||
|
import {Metadata} from "next";
|
||||||
|
import {ChannelsSection} from "@/components/wrappers/dashboard/admin/channels/channels-section";
|
||||||
|
import {db} from "@/db";
|
||||||
|
import {desc, eq, isNull} from "drizzle-orm";
|
||||||
|
import * as drizzleDb from "@/db";
|
||||||
|
import {StorageChannelWith} from "@/db/schema/12_storage-channel";
|
||||||
|
import {ChannelAddEditModal} from "@/components/wrappers/dashboard/admin/channels/channel/channel-add-edit-modal";
|
||||||
|
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
title: "Storage Channels",
|
||||||
|
};
|
||||||
|
|
||||||
|
export default async function RoutePage(props: PageParams<{}>) {
|
||||||
|
|
||||||
|
const storageChannels = await db.query.storageChannel.findMany({
|
||||||
|
with: {
|
||||||
|
organizations: true
|
||||||
|
},
|
||||||
|
where: isNull(drizzleDb.schemas.storageChannel.organizationId),
|
||||||
|
orderBy: desc(drizzleDb.schemas.storageChannel.createdAt)
|
||||||
|
}) as StorageChannelWith[]
|
||||||
|
|
||||||
|
const organizations = await db.query.organization.findMany({
|
||||||
|
where: (fields) => isNull(fields.deletedAt),
|
||||||
|
with: {
|
||||||
|
members: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const settings = await db.query.setting.findFirst({
|
||||||
|
where: eq(drizzleDb.schemas.setting.name, "system"),
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Page>
|
||||||
|
<PageHeader>
|
||||||
|
<PageTitle>Storage channels</PageTitle>
|
||||||
|
<PageActions>
|
||||||
|
<ChannelAddEditModal kind={"storage"} adminView={false}/>
|
||||||
|
</PageActions>
|
||||||
|
</PageHeader>
|
||||||
|
<PageContent>
|
||||||
|
<ChannelsSection defaultStorageChannelId={settings?.defaultStorageChannelId} kind={"storage"} organizations={organizations} channels={storageChannels}/>
|
||||||
|
</PageContent>
|
||||||
|
</Page>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
import {PageParams} from "@/types/next";
|
|
||||||
import {Page, PageContent, PageHeader, PageTitle} from "@/features/layout/page";
|
|
||||||
import {notFound} from "next/navigation";
|
|
||||||
import {RestoreForm} from "@/components/wrappers/dashboard/database/restore-form";
|
|
||||||
import {currentUser} from "@/lib/auth/current-user";
|
|
||||||
|
|
||||||
import {db} from "@/db";
|
|
||||||
import {eq} from "drizzle-orm";
|
|
||||||
import * as drizzleDb from "@/db";
|
|
||||||
|
|
||||||
|
|
||||||
export default async function RoutePage(
|
|
||||||
props: PageParams<{
|
|
||||||
databaseId: string;
|
|
||||||
}>
|
|
||||||
) {
|
|
||||||
const {databaseId} = await props.params;
|
|
||||||
|
|
||||||
const user = await currentUser();
|
|
||||||
if (!user) notFound();
|
|
||||||
|
|
||||||
const [dbToRestore] = await db.select().from(drizzleDb.schemas.database).where(eq(drizzleDb.schemas.database.id, databaseId));
|
|
||||||
|
|
||||||
if (!dbToRestore) {
|
|
||||||
notFound();
|
|
||||||
}
|
|
||||||
|
|
||||||
const dbsOfSameType = await db.select().from(drizzleDb.schemas.database).where(eq(drizzleDb.schemas.database.dbms!, dbToRestore.dbms!));
|
|
||||||
const successfulBackups = await db.select().from(drizzleDb.schemas.backup).where(eq(drizzleDb.schemas.backup.status, "success"));
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Page>
|
|
||||||
<PageHeader>
|
|
||||||
<PageTitle>Restore {dbToRestore.name}</PageTitle>
|
|
||||||
</PageHeader>
|
|
||||||
<PageContent>
|
|
||||||
<RestoreForm databaseToRestore={dbToRestore} databases={dbsOfSameType} backups={successfulBackups}/>
|
|
||||||
</PageContent>
|
|
||||||
</Page>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
import { PageParams } from "@/types/next";
|
|
||||||
import { Page, PageContent, PageHeader, PageTitle } from "@/features/layout/page";
|
|
||||||
import { notFound } from "next/navigation";
|
|
||||||
import { DatabaseForm } from "@/components/wrappers/dashboard/database/database-form/database-form";
|
|
||||||
|
|
||||||
import { db } from "@/db";
|
|
||||||
import * as drizzleDb from "@/db";
|
|
||||||
|
|
||||||
import { eq } from "drizzle-orm";
|
|
||||||
|
|
||||||
export default async function RoutePage(props: PageParams<{ databaseId: string }>) {
|
|
||||||
const { databaseId } = await props.params;
|
|
||||||
|
|
||||||
const dbItem = await db.query.database.findFirst({
|
|
||||||
where: eq(drizzleDb.schemas.database.id, databaseId),
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!dbItem) {
|
|
||||||
notFound();
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Page>
|
|
||||||
<PageHeader>
|
|
||||||
<PageTitle>Edit {dbItem.name}</PageTitle>
|
|
||||||
</PageHeader>
|
|
||||||
<PageContent>
|
|
||||||
<DatabaseForm
|
|
||||||
databaseId={databaseId}
|
|
||||||
// @ts-ignore
|
|
||||||
defaultValues={{ ...dbItem, dbms: dbItem.dbms ?? "inactive", description: dbItem.description ?? undefined }}
|
|
||||||
/>
|
|
||||||
</PageContent>
|
|
||||||
</Page>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,19 +1,13 @@
|
|||||||
import {PageParams} from "@/types/next";
|
import {PageParams} from "@/types/next";
|
||||||
import {notFound, redirect} from "next/navigation";
|
import {notFound, redirect} from "next/navigation";
|
||||||
import {Page, PageActions, PageContent, PageDescription, PageTitle} from "@/features/layout/page";
|
import {Page} from "@/features/layout/page";
|
||||||
import {BackupButton} from "@/components/wrappers/dashboard/backup/backup-button/backup-button";
|
|
||||||
import {DatabaseTabs} from "@/components/wrappers/dashboard/projects/database/database-tabs";
|
|
||||||
import {DatabaseKpi} from "@/components/wrappers/dashboard/projects/database/database-kpi";
|
|
||||||
import {EditButton} from "@/components/wrappers/dashboard/database/edit-button/edit-button";
|
|
||||||
import {CronButton} from "@/components/wrappers/dashboard/database/cron-button/cron-button";
|
|
||||||
|
|
||||||
import {db} from "@/db";
|
import {db} from "@/db";
|
||||||
import {eq, and, inArray} from "drizzle-orm";
|
import {eq, and, inArray} from "drizzle-orm";
|
||||||
import * as drizzleDb from "@/db";
|
import * as drizzleDb from "@/db";
|
||||||
import {getOrganizationProjectDatabases} from "@/lib/services";
|
import {getOrganizationProjectDatabases} from "@/lib/services";
|
||||||
import {getOrganization} from "@/lib/auth/auth";
|
import {getActiveMember, getOrganization} from "@/lib/auth/auth";
|
||||||
import {RetentionPolicySheet} from "@/components/wrappers/dashboard/database/retention-policy/retention-policy-sheet";
|
import {BackupModalProvider} from "@/components/wrappers/dashboard/database/backup/backup-modal-context";
|
||||||
import {capitalizeFirstLetter} from "@/utils/text";
|
import {DatabaseContent} from "@/components/wrappers/dashboard/projects/database/database-content";
|
||||||
|
|
||||||
export default async function RoutePage(props: PageParams<{
|
export default async function RoutePage(props: PageParams<{
|
||||||
projectId: string;
|
projectId: string;
|
||||||
@@ -22,8 +16,9 @@ export default async function RoutePage(props: PageParams<{
|
|||||||
const {projectId, databaseId} = await props.params;
|
const {projectId, databaseId} = await props.params;
|
||||||
|
|
||||||
const organization = await getOrganization({});
|
const organization = await getOrganization({});
|
||||||
|
const activeMember = await getActiveMember()
|
||||||
|
|
||||||
if (!organization) {
|
if (!organization || !activeMember) {
|
||||||
notFound();
|
notFound();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -36,7 +31,9 @@ export default async function RoutePage(props: PageParams<{
|
|||||||
where: and(inArray(drizzleDb.schemas.backup.id, databasesProject.ids ?? []), eq(drizzleDb.schemas.database.id, databaseId), eq(drizzleDb.schemas.database.projectId, projectId)),
|
where: and(inArray(drizzleDb.schemas.backup.id, databasesProject.ids ?? []), eq(drizzleDb.schemas.database.id, databaseId), eq(drizzleDb.schemas.database.projectId, projectId)),
|
||||||
with: {
|
with: {
|
||||||
project: true,
|
project: true,
|
||||||
retentionPolicy: true
|
retentionPolicy: true,
|
||||||
|
alertPolicies: true,
|
||||||
|
storagePolicies: true
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -48,6 +45,11 @@ export default async function RoutePage(props: PageParams<{
|
|||||||
where: eq(drizzleDb.schemas.backup.databaseId, dbItem.id),
|
where: eq(drizzleDb.schemas.backup.databaseId, dbItem.id),
|
||||||
with: {
|
with: {
|
||||||
restorations: true,
|
restorations: true,
|
||||||
|
storages: {
|
||||||
|
with: {
|
||||||
|
storageChannel: true
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
orderBy: (b, {desc}) => [desc(b.createdAt)],
|
orderBy: (b, {desc}) => [desc(b.createdAt)],
|
||||||
});
|
});
|
||||||
@@ -57,7 +59,7 @@ export default async function RoutePage(props: PageParams<{
|
|||||||
orderBy: (r, {desc}) => [desc(r.createdAt)],
|
orderBy: (r, {desc}) => [desc(r.createdAt)],
|
||||||
});
|
});
|
||||||
|
|
||||||
const isAlreadyBackup = backups.some((b) => b.status === "waiting");
|
const isAlreadyBackup = backups.some((b) => b.status === "waiting" || b.status === "ongoing");
|
||||||
const isAlreadyRestore = restorations.some((r) => r.status === "waiting");
|
const isAlreadyRestore = restorations.some((r) => r.status === "waiting");
|
||||||
|
|
||||||
const totalBackups = await db.select({count: drizzleDb.schemas.backup.id})
|
const totalBackups = await db.select({count: drizzleDb.schemas.backup.id})
|
||||||
@@ -81,36 +83,29 @@ export default async function RoutePage(props: PageParams<{
|
|||||||
notFound();
|
notFound();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
const successRate = totalBackups > 0 ? (successfulBackups / totalBackups) * 100 : null;
|
const successRate = totalBackups > 0 ? (successfulBackups / totalBackups) * 100 : null;
|
||||||
|
|
||||||
|
const isMember = activeMember?.role === "member";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Page>
|
<Page>
|
||||||
<div className="justify-between gap-2 sm:flex">
|
<BackupModalProvider>
|
||||||
<PageTitle className="flex items-center justify-between w-full">
|
<DatabaseContent
|
||||||
{capitalizeFirstLetter(dbItem.name)}
|
activeMember={activeMember}
|
||||||
<div className="flex items-center gap-2 justify-between w-full">
|
settings={settings}
|
||||||
<div className="flex items-center gap-2">
|
database={dbItem}
|
||||||
<EditButton/>
|
isAlreadyRestore={isAlreadyRestore}
|
||||||
<RetentionPolicySheet database={dbItem}/>
|
restorations={restorations}
|
||||||
<CronButton database={dbItem}/>
|
backups={backups}
|
||||||
</div>
|
totalBackups={totalBackups}
|
||||||
<div className="flex items-center gap-2">
|
availableBackups={availableBackups}
|
||||||
<BackupButton disable={isAlreadyBackup} databaseId={databaseId}/>
|
successRate={successRate}
|
||||||
</div>
|
organizationId={organization.id}
|
||||||
</div>
|
activeOrganizationChannels={[]}
|
||||||
</PageTitle>
|
activeOrganizationStorageChannels={[]}
|
||||||
</div>
|
/>
|
||||||
|
</BackupModalProvider>
|
||||||
{dbItem.description && (
|
|
||||||
<PageDescription className="mt-5 sm:mt-0">{dbItem.description}</PageDescription>
|
|
||||||
)}
|
|
||||||
<PageContent className="flex flex-col w-full h-full">
|
|
||||||
<DatabaseKpi successRate={successRate} database={dbItem} availableBackups={availableBackups}
|
|
||||||
totalBackups={totalBackups}/>
|
|
||||||
<DatabaseTabs settings={settings} database={dbItem} isAlreadyRestore={isAlreadyRestore}
|
|
||||||
backups={backups}
|
|
||||||
restorations={restorations}/>
|
|
||||||
</PageContent>
|
|
||||||
</Page>
|
</Page>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -1,65 +0,0 @@
|
|||||||
import {notFound} from "next/navigation";
|
|
||||||
import {PageParams} from "@/types/next";
|
|
||||||
import {Page, PageContent, PageHeader, PageTitle} from "@/features/layout/page";
|
|
||||||
import {ProjectForm} from "@/components/wrappers/dashboard/projects/project-form/project-form";
|
|
||||||
import * as drizzleDb from "@/db";
|
|
||||||
|
|
||||||
import {db} from "@/db";
|
|
||||||
import {eq} from "drizzle-orm";
|
|
||||||
import {DatabaseWith} from "@/db/schema/07_database";
|
|
||||||
|
|
||||||
export default async function RoutePage(props: PageParams<{ projectId: string }>) {
|
|
||||||
const {projectId} = await props.params;
|
|
||||||
|
|
||||||
const proj = await db.query.project.findFirst({
|
|
||||||
where: eq(drizzleDb.schemas.project.id, projectId),
|
|
||||||
with: {
|
|
||||||
databases: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!proj) {
|
|
||||||
notFound();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
const org = await db.query.organization.findFirst({
|
|
||||||
where: eq(drizzleDb.schemas.organization.slug, "default"),
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!org) {
|
|
||||||
notFound();
|
|
||||||
}
|
|
||||||
|
|
||||||
const availableDatabases = (
|
|
||||||
await db.query.database.findMany({
|
|
||||||
where: (db, {or, eq, isNull}) => or(isNull(db.projectId), eq(db.projectId, proj.id)),
|
|
||||||
with: {
|
|
||||||
agent: true,
|
|
||||||
project: true,
|
|
||||||
backups: true,
|
|
||||||
restorations: true,
|
|
||||||
},
|
|
||||||
orderBy: (db, {desc}) => [desc(db.createdAt)],
|
|
||||||
})
|
|
||||||
)
|
|
||||||
// .filter((db) => db.project !== null) as DatabaseWith[];
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Page>
|
|
||||||
<PageHeader>
|
|
||||||
<PageTitle>Edit {proj.name}</PageTitle>
|
|
||||||
</PageHeader>
|
|
||||||
<PageContent>
|
|
||||||
<ProjectForm
|
|
||||||
organization={org}
|
|
||||||
databases={availableDatabases as DatabaseWith[]}
|
|
||||||
defaultValues={{...proj, databases: proj.databases.map((db) => db.id)}}
|
|
||||||
projectId={proj.id}
|
|
||||||
/>
|
|
||||||
</PageContent>
|
|
||||||
</Page>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,8 +1,5 @@
|
|||||||
import {PageParams} from "@/types/next";
|
import {PageParams} from "@/types/next";
|
||||||
import {Page, PageActions, PageContent, PageDescription, PageTitle} from "@/features/layout/page";
|
import {Page, PageContent, PageTitle} from "@/features/layout/page";
|
||||||
import {buttonVariants} from "@/components/ui/button";
|
|
||||||
import {GearIcon} from "@radix-ui/react-icons";
|
|
||||||
import Link from "next/link";
|
|
||||||
import {
|
import {
|
||||||
ButtonDeleteProject
|
ButtonDeleteProject
|
||||||
} from "@/components/wrappers/dashboard/projects/button-delete-project/button-delete-project";
|
} from "@/components/wrappers/dashboard/projects/button-delete-project/button-delete-project";
|
||||||
@@ -12,9 +9,14 @@ import {notFound, redirect} from "next/navigation";
|
|||||||
|
|
||||||
import {db} from "@/db";
|
import {db} from "@/db";
|
||||||
import {eq} from "drizzle-orm";
|
import {eq} from "drizzle-orm";
|
||||||
import {getOrganization} from "@/lib/auth/auth";
|
import {getActiveMember, getOrganization} from "@/lib/auth/auth";
|
||||||
import * as drizzleDb from "@/db";
|
import * as drizzleDb from "@/db";
|
||||||
import {capitalizeFirstLetter} from "@/utils/text";
|
import {capitalizeFirstLetter} from "@/utils/text";
|
||||||
|
import {ProjectDialog} from "@/features/projects/components/project.dialog";
|
||||||
|
import {DatabaseWith} from "@/db/schema/07_database";
|
||||||
|
import {ProjectWith} from "@/db/schema/06_project";
|
||||||
|
import {isUuidv4} from "@/utils/verify-uuid";
|
||||||
|
|
||||||
|
|
||||||
export default async function RoutePage(props: PageParams<{
|
export default async function RoutePage(props: PageParams<{
|
||||||
projectId: string
|
projectId: string
|
||||||
@@ -23,7 +25,13 @@ export default async function RoutePage(props: PageParams<{
|
|||||||
projectId
|
projectId
|
||||||
} = await props.params;
|
} = await props.params;
|
||||||
|
|
||||||
|
if (!isUuidv4(projectId)) {
|
||||||
|
notFound()
|
||||||
|
}
|
||||||
|
|
||||||
const organization = await getOrganization({});
|
const organization = await getOrganization({});
|
||||||
|
const activeMember = await getActiveMember()
|
||||||
|
|
||||||
if (!organization) {
|
if (!organization) {
|
||||||
notFound();
|
notFound();
|
||||||
}
|
}
|
||||||
@@ -48,23 +56,45 @@ export default async function RoutePage(props: PageParams<{
|
|||||||
redirect("/dashboard/projects");
|
redirect("/dashboard/projects");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const availableDatabases = (
|
||||||
|
await db.query.database.findMany({
|
||||||
|
where: (db, {or, eq, isNull}) => or(isNull(db.projectId), eq(db.projectId, proj.id)),
|
||||||
|
with: {
|
||||||
|
agent: true,
|
||||||
|
project: true,
|
||||||
|
backups: true,
|
||||||
|
restorations: true,
|
||||||
|
},
|
||||||
|
orderBy: (db, {desc}) => [desc(db.createdAt)],
|
||||||
|
})
|
||||||
|
) as DatabaseWith[];
|
||||||
|
|
||||||
|
const isMember = activeMember?.role === "member";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Page>
|
<Page>
|
||||||
<div className="justify-between gap-2 sm:flex">
|
<div className="justify-between gap-2 sm:flex">
|
||||||
<PageTitle className="flex items-center">
|
<PageTitle className="flex flex-col md:flex-row items-center justify-between w-full ">
|
||||||
{capitalizeFirstLetter(proj.name)}
|
<div className="min-w-full md:min-w-fit ">
|
||||||
<Link className={buttonVariants({variant: "outline"})} href={`/dashboard/projects/${proj.id}/edit`}>
|
{capitalizeFirstLetter(proj.name)}
|
||||||
<GearIcon className="w-7 h-7"/>
|
</div>
|
||||||
</Link>
|
{!isMember && (
|
||||||
|
<div className="flex items-center gap-2 md:justify-between w-full ">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<ProjectDialog
|
||||||
|
databases={availableDatabases}
|
||||||
|
organization={org}
|
||||||
|
project={proj as ProjectWith}
|
||||||
|
isEdit={true}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<ButtonDeleteProject projectId={projectId} text={"Delete Project"}/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</PageTitle>
|
</PageTitle>
|
||||||
<PageActions className="justify-between">
|
|
||||||
<ButtonDeleteProject projectId={projectId} text={"Delete Project"}/>
|
|
||||||
</PageActions>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<PageDescription>The list of associated databases</PageDescription>
|
|
||||||
|
|
||||||
<PageContent className="flex flex-col w-full h-full">
|
<PageContent className="flex flex-col w-full h-full">
|
||||||
{proj.databases.length > 0 ? (
|
{proj.databases.length > 0 ? (
|
||||||
<CardsWithPagination
|
<CardsWithPagination
|
||||||
@@ -72,8 +102,8 @@ export default async function RoutePage(props: PageParams<{
|
|||||||
organizationSlug={organization.slug}
|
organizationSlug={organization.slug}
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
cardItem={ProjectDatabaseCard}
|
cardItem={ProjectDatabaseCard}
|
||||||
cardsPerPage={4}
|
cardsPerPage={6}
|
||||||
numberOfColumns={1}
|
numberOfColumns={3}
|
||||||
extendedProps={proj}
|
extendedProps={proj}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
@@ -85,4 +115,4 @@ export default async function RoutePage(props: PageParams<{
|
|||||||
</PageContent>
|
</PageContent>
|
||||||
</Page>
|
</Page>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -1,49 +0,0 @@
|
|||||||
import {PageParams} from "@/types/next";
|
|
||||||
import {Page, PageContent, PageHeader, PageTitle} from "@/features/layout/page";
|
|
||||||
import {ProjectForm} from "@/components/wrappers/dashboard/projects/project-form/project-form";
|
|
||||||
import {notFound} from "next/navigation";
|
|
||||||
import {db} from "@/db";
|
|
||||||
import {eq} from "drizzle-orm";
|
|
||||||
import {getOrganization} from "@/lib/auth/auth";
|
|
||||||
import * as drizzleDb from "@/db";
|
|
||||||
import {DatabaseWith} from "@/db/schema/07_database";
|
|
||||||
|
|
||||||
export default async function RoutePage(props: PageParams<{ }>) {
|
|
||||||
|
|
||||||
const organization = await getOrganization({});
|
|
||||||
|
|
||||||
if (!organization ) {
|
|
||||||
notFound();
|
|
||||||
}
|
|
||||||
|
|
||||||
const availableDatabases = (
|
|
||||||
await db.query.database.findMany({
|
|
||||||
where: (db, {isNull}) => isNull(db.projectId),
|
|
||||||
with: {
|
|
||||||
agent: true,
|
|
||||||
project: true,
|
|
||||||
backups: true,
|
|
||||||
restorations: true,
|
|
||||||
},
|
|
||||||
orderBy: (db, {desc}) => [desc(db.createdAt)],
|
|
||||||
})
|
|
||||||
).filter((db) => db.project == null) as DatabaseWith[];
|
|
||||||
|
|
||||||
const org = await db.query.organization.findFirst({
|
|
||||||
where: eq(drizzleDb.schemas.organization.slug, organization.slug),
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!org) notFound();
|
|
||||||
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Page>
|
|
||||||
<PageHeader>
|
|
||||||
<PageTitle>Create new project</PageTitle>
|
|
||||||
</PageHeader>
|
|
||||||
<PageContent>
|
|
||||||
<ProjectForm databases={availableDatabases} organization={org}/>
|
|
||||||
</PageContent>
|
|
||||||
</Page>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,18 +1,23 @@
|
|||||||
import Link from "next/link";
|
|
||||||
|
|
||||||
import {PageParams} from "@/types/next";
|
import {PageParams} from "@/types/next";
|
||||||
import {CardsWithPagination} from "@/components/wrappers/common/cards-with-pagination";
|
import {CardsWithPagination} from "@/components/wrappers/common/cards-with-pagination";
|
||||||
import {Button} from "@/components/ui/button";
|
|
||||||
import {Page, PageActions, PageContent, PageHeader, PageTitle} from "@/features/layout/page";
|
import {Page, PageActions, PageContent, PageHeader, PageTitle} from "@/features/layout/page";
|
||||||
import {ProjectCard} from "@/components/wrappers/dashboard/projects/project-card/project-card";
|
import {ProjectCard} from "@/components/wrappers/dashboard/projects/project-card/project-card";
|
||||||
import {db} from "@/db";
|
import {db} from "@/db";
|
||||||
import {notFound} from "next/navigation";
|
import {notFound} from "next/navigation";
|
||||||
import {getOrganization} from "@/lib/auth/auth";
|
import {getActiveMember, getOrganization} from "@/lib/auth/auth";
|
||||||
import {EmptyStatePlaceholder} from "@/components/wrappers/common/empty-state-placeholder";
|
import {EmptyStatePlaceholder} from "@/components/wrappers/common/empty-state-placeholder";
|
||||||
|
import {Metadata} from "next";
|
||||||
|
import {ProjectDialog} from "@/features/projects/components/project.dialog";
|
||||||
|
import {DatabaseWith} from "@/db/schema/07_database";
|
||||||
|
|
||||||
export default async function RoutePage(props: PageParams<{ }>) {
|
export const metadata: Metadata = {
|
||||||
|
title: "Projects",
|
||||||
|
};
|
||||||
|
|
||||||
|
export default async function RoutePage(props: PageParams<{}>) {
|
||||||
|
|
||||||
const organization = await getOrganization({});
|
const organization = await getOrganization({});
|
||||||
|
const activeMember = await getActiveMember()
|
||||||
|
|
||||||
if (!organization) {
|
if (!organization) {
|
||||||
notFound();
|
notFound();
|
||||||
@@ -28,31 +33,48 @@ export default async function RoutePage(props: PageParams<{ }>) {
|
|||||||
databases: true,
|
databases: true,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
const isMember = activeMember?.role === "member";
|
||||||
|
|
||||||
|
const availableDatabases = (
|
||||||
|
await db.query.database.findMany({
|
||||||
|
where: (db, {isNull}) => isNull(db.projectId),
|
||||||
|
with: {
|
||||||
|
agent: true,
|
||||||
|
project: true,
|
||||||
|
backups: true,
|
||||||
|
restorations: true,
|
||||||
|
},
|
||||||
|
orderBy: (db, {desc}) => [desc(db.createdAt)],
|
||||||
|
})
|
||||||
|
).filter((db) => db.project == null) as DatabaseWith[];
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Page>
|
<Page>
|
||||||
<PageHeader>
|
<PageHeader>
|
||||||
<PageTitle>Projects</PageTitle>
|
<PageTitle>Projects</PageTitle>
|
||||||
{projects.length > 0 && (
|
{(projects.length > 0 && !isMember) && (
|
||||||
<PageActions>
|
<PageActions>
|
||||||
<Link href={`/dashboard/projects/new`}>
|
<ProjectDialog databases={availableDatabases} organization={organization}/>
|
||||||
<Button>+ Create Project</Button>
|
|
||||||
</Link>
|
|
||||||
</PageActions>
|
</PageActions>
|
||||||
)}
|
)}
|
||||||
</PageHeader>
|
</PageHeader>
|
||||||
|
|
||||||
<PageContent>
|
<PageContent>
|
||||||
{projects.length > 0 ? (
|
{projects.length > 0 ? (
|
||||||
<CardsWithPagination organizationSlug={organization.slug} data={projects} cardItem={ProjectCard}
|
<CardsWithPagination
|
||||||
cardsPerPage={4} numberOfColumns={1}/>
|
organizationSlug={organization.slug}
|
||||||
) : (
|
data={projects}
|
||||||
<EmptyStatePlaceholder
|
cardItem={ProjectCard}
|
||||||
url={"/dashboard/projects/new"}
|
cardsPerPage={9}
|
||||||
text={"Create new Project"}
|
numberOfColumns={3}
|
||||||
/>
|
/>
|
||||||
|
) : isMember ? (
|
||||||
|
<EmptyStatePlaceholder text="No project available"/>
|
||||||
|
) : (
|
||||||
|
<ProjectDialog databases={availableDatabases} organization={organization} isEmpty={true}/>
|
||||||
)}
|
)}
|
||||||
</PageContent>
|
</PageContent>
|
||||||
</Page>
|
</Page>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
import {PageParams} from "@/types/next";
|
|
||||||
import {Page, PageContent, PageHeader, PageTitle} from "@/features/layout/page";
|
|
||||||
import {notFound} from "next/navigation";
|
|
||||||
import {OrganizationForm} from "@/components/wrappers/dashboard/organization/organization-form/organization-form";
|
|
||||||
import {getOrganization} from "@/lib/auth/auth";
|
|
||||||
import {db} from "@/db";
|
|
||||||
import {isNull} from "drizzle-orm";
|
|
||||||
import {currentUser} from "@/lib/auth/current-user";
|
|
||||||
|
|
||||||
export default async function RoutePage(props: PageParams<{
|
|
||||||
|
|
||||||
}>) {
|
|
||||||
const user = await currentUser();
|
|
||||||
|
|
||||||
const organization = await getOrganization({});
|
|
||||||
|
|
||||||
const users = await db.query.user.findMany({
|
|
||||||
where: (fields) => isNull(fields.deletedAt)
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
if (!user || !users || !organization || organization.slug == "default") {
|
|
||||||
notFound();
|
|
||||||
}
|
|
||||||
|
|
||||||
return(
|
|
||||||
<Page>
|
|
||||||
<PageHeader>
|
|
||||||
<PageTitle>
|
|
||||||
Edit {organization.name}
|
|
||||||
</PageTitle>
|
|
||||||
</PageHeader>
|
|
||||||
<PageContent>
|
|
||||||
<OrganizationForm currentUser={user} users={users} defaultValues={organization}/>
|
|
||||||
</PageContent>
|
|
||||||
</Page>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,53 +1,95 @@
|
|||||||
import {PageParams} from "@/types/next";
|
import { PageParams } from "@/types/next";
|
||||||
import {Page, PageActions, PageContent, PageHeader, PageTitle} from "@/features/layout/page";
|
|
||||||
import {currentUser} from "@/lib/auth/current-user";
|
|
||||||
import {getActiveMember, getOrganization} from "@/lib/auth/auth";
|
|
||||||
import {notFound} from "next/navigation";
|
|
||||||
import {
|
import {
|
||||||
DeleteOrganizationButton
|
Page,
|
||||||
} from "@/components/wrappers/dashboard/organization/delete-organization/delete-organization-button";
|
PageActions,
|
||||||
import {EditButtonSettings} from "@/components/wrappers/dashboard/settings/edit-button-settings/edit-button-settings";
|
PageContent,
|
||||||
import {
|
PageHeader,
|
||||||
SettingsOrganizationMembersTable
|
PageTitle,
|
||||||
} from "@/components/wrappers/dashboard/settings/settings-organization-members-table";
|
} from "@/features/layout/page";
|
||||||
|
import { currentUser } from "@/lib/auth/current-user";
|
||||||
|
import { getActiveMember, getOrganization } from "@/lib/auth/auth";
|
||||||
|
import { notFound } from "next/navigation";
|
||||||
|
import { Metadata } from "next";
|
||||||
|
import { OrganizationTabs } from "@/components/wrappers/dashboard/organization/tabs/organization-tabs";
|
||||||
|
import { getOrganizationChannels } from "@/db/services/notification-channel";
|
||||||
|
import { computeOrganizationPermissions } from "@/lib/acl/organization-acl";
|
||||||
|
import { getOrganizationStorageChannels } from "@/db/services/storage-channel";
|
||||||
|
import { DeleteOrganizationButton } from "@/components/wrappers/dashboard/organization/delete-organization-button";
|
||||||
|
import { EditOrganizationDialog } from "@/features/organization/components/edit-organization.dialog";
|
||||||
|
import { db } from "@/db";
|
||||||
|
import { isNull } from "drizzle-orm";
|
||||||
|
import * as drizzleDb from "@/db";
|
||||||
|
import { eq } from "drizzle-orm";
|
||||||
|
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
title: "Settings",
|
||||||
|
};
|
||||||
|
|
||||||
export default async function RoutePage(props: PageParams<{ slug: string }>) {
|
export default async function RoutePage(props: PageParams<{ slug: string }>) {
|
||||||
const organization = await getOrganization({});
|
const organization = await getOrganization({});
|
||||||
const user = await currentUser();
|
const user = await currentUser();
|
||||||
const activeMember = await getActiveMember()
|
const activeMember = await getActiveMember();
|
||||||
|
|
||||||
if (!organization) {
|
if (!organization || !activeMember || !user) {
|
||||||
notFound();
|
notFound();
|
||||||
}
|
}
|
||||||
|
|
||||||
const isMember = activeMember?.role === "member";
|
const notificationChannels = await getOrganizationChannels(organization.id);
|
||||||
|
const storageChannels = await getOrganizationStorageChannels(organization.id);
|
||||||
|
const permissions = computeOrganizationPermissions(activeMember);
|
||||||
|
|
||||||
if (isMember) {
|
const users = await db.query.user.findMany({
|
||||||
notFound();
|
where: (fields) => isNull(fields.deletedAt),
|
||||||
}
|
});
|
||||||
|
|
||||||
return (
|
const organizationWithMembers = await db.query.organization.findFirst({
|
||||||
<Page>
|
where: eq(drizzleDb.schemas.organization.id, organization.id),
|
||||||
<PageHeader>
|
with: {
|
||||||
<PageTitle className="flex items-center">
|
members: {
|
||||||
Organization settings
|
with: {
|
||||||
{!isMember && organization.slug !== "default" && (
|
user: true,
|
||||||
<EditButtonSettings/>
|
},
|
||||||
)}
|
},
|
||||||
</PageTitle>
|
},
|
||||||
<PageActions>
|
});
|
||||||
{!isMember && organization.slug !== "default" && (
|
|
||||||
<DeleteOrganizationButton organizationSlug={organization.slug}/>
|
if (!organizationWithMembers) notFound();
|
||||||
)}
|
|
||||||
</PageActions>
|
return (
|
||||||
</PageHeader>
|
<Page>
|
||||||
{/*<PageDescription>*/}
|
<PageHeader>
|
||||||
{/* Manage your organization settings.*/}
|
<PageTitle className="flex flex-col md:flex-row items-center justify-between w-full ">
|
||||||
{/*</PageDescription>*/}
|
<div className="min-w-full md:min-w-fit ">Organization settings</div>
|
||||||
<PageContent>
|
<div className="flex items-center gap-2 md:justify-between w-full ">
|
||||||
<SettingsOrganizationMembersTable organization={organization}/>
|
<div className="flex items-center gap-2">
|
||||||
</PageContent>
|
{permissions.canManageSettings &&
|
||||||
</Page>
|
organization.slug !== "default" && (
|
||||||
)
|
<EditOrganizationDialog
|
||||||
}
|
organization={organizationWithMembers}
|
||||||
|
users={users}
|
||||||
|
currentUser={user}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
{permissions.canManageDangerZone &&
|
||||||
|
organization.slug !== "default" && (
|
||||||
|
<DeleteOrganizationButton
|
||||||
|
organizationSlug={organization.slug}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</PageTitle>
|
||||||
|
</PageHeader>
|
||||||
|
<PageContent>
|
||||||
|
<OrganizationTabs
|
||||||
|
activeMember={activeMember}
|
||||||
|
organization={organization}
|
||||||
|
notificationChannels={notificationChannels}
|
||||||
|
storageChannels={storageChannels}
|
||||||
|
/>
|
||||||
|
</PageContent>
|
||||||
|
</Page>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
@@ -8,7 +8,12 @@ import {db} from "@/db";
|
|||||||
import {and, asc, count, eq, inArray} from "drizzle-orm";
|
import {and, asc, count, eq, inArray} from "drizzle-orm";
|
||||||
import * as drizzleDb from "@/db";
|
import * as drizzleDb from "@/db";
|
||||||
import {getOrganization} from "@/lib/auth/auth";
|
import {getOrganization} from "@/lib/auth/auth";
|
||||||
import {DatabaseBackup, Folder, RefreshCcw} from "lucide-react";
|
import {Building2, DatabaseBackup, Folder, RefreshCcw} from "lucide-react";
|
||||||
|
import {Metadata} from "next";
|
||||||
|
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
title: "Statistics",
|
||||||
|
};
|
||||||
|
|
||||||
export default async function RoutePage(props: PageParams<{}>) {
|
export default async function RoutePage(props: PageParams<{}>) {
|
||||||
const organization = await getOrganization({});
|
const organization = await getOrganization({});
|
||||||
@@ -45,79 +50,6 @@ export default async function RoutePage(props: PageParams<{}>) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// const tomorrow = new Date();
|
|
||||||
// tomorrow.setDate(tomorrow.getDate() + 1);
|
|
||||||
//
|
|
||||||
// const before = new Date();
|
|
||||||
// before.setDate(before.getDate() - 1);
|
|
||||||
//
|
|
||||||
// const backupsEvolution = [
|
|
||||||
// {
|
|
||||||
// id: '22e84aa4-228c-45b3-82ec-846a639cd509',
|
|
||||||
// createdAt: new Date()
|
|
||||||
// },
|
|
||||||
// {
|
|
||||||
// id: '2c114dc3-1fa6-4ef1-972c-9765c65e9331',
|
|
||||||
// createdAt: new Date()
|
|
||||||
// },
|
|
||||||
// {
|
|
||||||
// id: '2c114dc3-1fa6-4ef1-972c-9765c65e9331',
|
|
||||||
// createdAt: new Date()
|
|
||||||
// },
|
|
||||||
// {
|
|
||||||
// id: '2c114dc3-1fa6-4ef1-972c-9765c65e9331',
|
|
||||||
// createdAt: new Date()
|
|
||||||
// },
|
|
||||||
// {
|
|
||||||
// id: '2c114dc3-1fa6-4ef1-972c-9765c65e9331',
|
|
||||||
// createdAt: new Date(before)
|
|
||||||
// },
|
|
||||||
// {
|
|
||||||
// id: '6a6106fe-7f45-48eb-a56f-0a1e734126a1',
|
|
||||||
// createdAt: new Date(before)
|
|
||||||
// },
|
|
||||||
// {
|
|
||||||
// id: 'a529d790-502e-4609-ad37-9b1c00c73477',
|
|
||||||
// createdAt: new Date()
|
|
||||||
// },
|
|
||||||
// {
|
|
||||||
// id: 'a8c105a8-3e29-423e-b7dd-d3218092cde1',
|
|
||||||
// createdAt: new Date()
|
|
||||||
// },
|
|
||||||
// {
|
|
||||||
// id: 'd33aaf4f-8525-4490-addb-12e3c8650d6d',
|
|
||||||
// createdAt: new Date()
|
|
||||||
// },
|
|
||||||
// {
|
|
||||||
// id: 'f1d5a4e2-1c33-41c4-932b-02456c2a6f1d',
|
|
||||||
// createdAt: new Date(tomorrow)
|
|
||||||
// },
|
|
||||||
// {
|
|
||||||
// id: 'f1d5a4e2-1c33-41c4-932b-02456c2a6f1d',
|
|
||||||
// createdAt: new Date(tomorrow)
|
|
||||||
// },
|
|
||||||
// {
|
|
||||||
// id: 'e88a588a-2353-4470-9976-8c3eb2ffc88d',
|
|
||||||
// createdAt: new Date()
|
|
||||||
// },
|
|
||||||
// {
|
|
||||||
// id: 'ee11441e-4b41-4c1b-9d91-929565b4204a',
|
|
||||||
// createdAt: new Date()
|
|
||||||
// },
|
|
||||||
//
|
|
||||||
// // Entries with tomorrow's date
|
|
||||||
// {
|
|
||||||
// id: 'f1d5a4e2-1c33-41c4-932b-02456c2a6f1d',
|
|
||||||
// createdAt: new Date(tomorrow)
|
|
||||||
// },
|
|
||||||
// {
|
|
||||||
// id: 'c0a8323d-9241-4896-9e64-01e905c24e51',
|
|
||||||
// createdAt: new Date(tomorrow)
|
|
||||||
// }
|
|
||||||
// ];
|
|
||||||
|
|
||||||
|
|
||||||
const backupsRate = await db
|
const backupsRate = await db
|
||||||
.select({
|
.select({
|
||||||
createdAt: drizzleDb.schemas.backup.createdAt,
|
createdAt: drizzleDb.schemas.backup.createdAt,
|
||||||
@@ -130,7 +62,6 @@ export default async function RoutePage(props: PageParams<{}>) {
|
|||||||
.orderBy(drizzleDb.schemas.backup.createdAt);
|
.orderBy(drizzleDb.schemas.backup.createdAt);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const restorationsCountResult = await db
|
const restorationsCountResult = await db
|
||||||
.select({
|
.select({
|
||||||
count: count(),
|
count: count(),
|
||||||
@@ -139,62 +70,64 @@ export default async function RoutePage(props: PageParams<{}>) {
|
|||||||
.where(inArray(drizzleDb.schemas.restoration.databaseId, databaseIds));
|
.where(inArray(drizzleDb.schemas.restoration.databaseId, databaseIds));
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const restorationsCount = restorationsCountResult[0]?.count ?? 0;
|
const restorationsCount = restorationsCountResult[0]?.count ?? 0;
|
||||||
const projectsCount = projects.length;
|
const projectsCount = projects.length;
|
||||||
const backupsEvolutionCount = backupsEvolution.length;
|
const backupsEvolutionCount = backupsEvolution.length;
|
||||||
|
|
||||||
|
|
||||||
const sortedBackupsEvolution = backupsEvolution.sort(
|
const sortedBackupsEvolution = backupsEvolution.sort(
|
||||||
(a, b) => new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime()
|
(a, b) => new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime()
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Page>
|
<Page>
|
||||||
<PageHeader>
|
<PageHeader>
|
||||||
<PageTitle>Statistics</PageTitle>
|
<PageTitle>Statistics Overview</PageTitle>
|
||||||
</PageHeader>
|
</PageHeader>
|
||||||
|
|
||||||
<PageContent className="flex flex-col gap-y-4">
|
<PageContent className="flex flex-col gap-y-4">
|
||||||
<div className="flex flex-col md:flex-row gap-4">
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||||
<Card className="w-full flex-1">
|
<Card className="w-full">
|
||||||
<CardHeader className="flex items-center gap-2">
|
<CardHeader className="flex flex-row items-center justify-between pb-2">
|
||||||
<Folder className="w-5 h-5 text-muted-foreground" />
|
<CardTitle className="text-sm font-medium">Projects</CardTitle>
|
||||||
<CardTitle>Projects</CardTitle>
|
<Building2 className="h-4 w-4 text-muted-foreground"/>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="text-3xl font-bold">{projectsCount}</CardContent>
|
<CardContent>
|
||||||
|
<div className="text-2xl font-bold">{projectsCount}</div>
|
||||||
|
<p className="text-xs text-muted-foreground">Active projects in this organization</p>
|
||||||
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
<Card className="w-full flex-1">
|
|
||||||
<CardHeader className="flex items-center gap-2">
|
<Card className="w-full">
|
||||||
<DatabaseBackup className="w-5 h-5 text-muted-foreground" />
|
<CardHeader className="flex flex-row items-center justify-between pb-2">
|
||||||
<CardTitle>Backups</CardTitle>
|
<CardTitle className="text-sm font-medium">Backups</CardTitle>
|
||||||
|
<DatabaseBackup className="h-4 w-4 text-muted-foreground"/>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="text-3xl font-bold">{backupsEvolutionCount}</CardContent>
|
<CardContent>
|
||||||
|
<div className="text-2xl font-bold">{backupsEvolutionCount}</div>
|
||||||
|
<p className="text-xs text-muted-foreground">Total backups executed across all databases</p>
|
||||||
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
<Card className="w-full flex-1">
|
|
||||||
<CardHeader className="flex items-center gap-2">
|
<Card className="w-full">
|
||||||
<RefreshCcw className="w-5 h-5 text-muted-foreground" />
|
<CardHeader className="flex flex-row items-center justify-between pb-2">
|
||||||
<CardTitle>Restorations</CardTitle>
|
<CardTitle className="text-sm font-medium">Restorations</CardTitle>
|
||||||
|
<RefreshCcw className="h-4 w-4 text-muted-foreground"/>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="text-3xl font-bold">{restorationsCount}</CardContent>
|
<CardContent>
|
||||||
|
<div className="text-2xl font-bold">{restorationsCount}</div>
|
||||||
|
<p className="text-xs text-muted-foreground">Total restoration operations performed</p>
|
||||||
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex flex-col md:flex-row gap-4">
|
<div className="flex flex-col md:flex-row gap-4">
|
||||||
<Card className="w-full">
|
<EvolutionLineChart
|
||||||
<CardHeader>
|
data={sortedBackupsEvolution}
|
||||||
<CardTitle>Evolution of the number of backups</CardTitle>
|
/>
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
<PercentageLineChart
|
||||||
<EvolutionLineChart data={sortedBackupsEvolution}/>
|
data={backupsRate}/>
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
<Card className="w-full">
|
|
||||||
<CardHeader>
|
|
||||||
<CardTitle>Success rate of backups</CardTitle>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
<PercentageLineChart data={backupsRate}/>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</div>
|
</div>
|
||||||
</PageContent>
|
</PageContent>
|
||||||
</Page>
|
</Page>
|
||||||
|
|||||||
@@ -1,16 +1,18 @@
|
|||||||
import {PageParams} from "@/types/next";
|
import {PageParams} from "@/types/next";
|
||||||
import {Page, PageContent, PageHeader, PageTitle} from "@/features/layout/page";
|
import {Page, PageContent, PageHeader, PageTitle} from "@/features/layout/page";
|
||||||
import {Card, CardContent, CardHeader, CardTitle} from "@/components/ui/card";
|
import {Card, CardContent, CardHeader, CardTitle} from "@/components/ui/card";
|
||||||
import {Building2, DatabaseBackup, Folder, RefreshCcw} from "lucide-react";
|
import {Building2, Database, DatabaseBackup, Folder, RefreshCcw, Server, Workflow} from "lucide-react";
|
||||||
import {EvolutionLineChart} from "@/components/wrappers/dashboard/statistics/charts/evolution-line-chart";
|
|
||||||
import {PercentageLineChart} from "@/components/wrappers/dashboard/statistics/charts/percentage-line-chart";
|
|
||||||
import {currentUser} from "@/lib/auth/current-user";
|
import {currentUser} from "@/lib/auth/current-user";
|
||||||
import {notFound} from "next/navigation";
|
import {notFound} from "next/navigation";
|
||||||
import {db} from "@/db";
|
import {db} from "@/db";
|
||||||
import {asc, eq, inArray} from "drizzle-orm";
|
import {asc, inArray} from "drizzle-orm";
|
||||||
import * as drizzleDb from "@/db";
|
import * as drizzleDb from "@/db";
|
||||||
import {auth, listOrganizations} from "@/lib/auth/auth";
|
import {listOrganizations} from "@/lib/auth/auth";
|
||||||
import {authClient} from "@/lib/auth/auth-client";
|
import {Metadata} from "next";
|
||||||
|
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
title: "Home",
|
||||||
|
};
|
||||||
|
|
||||||
export default async function RoutePage(props: PageParams<{}>) {
|
export default async function RoutePage(props: PageParams<{}>) {
|
||||||
|
|
||||||
@@ -21,6 +23,7 @@ export default async function RoutePage(props: PageParams<{}>) {
|
|||||||
|
|
||||||
const organizationIds = organizations.map(project => project.id);
|
const organizationIds = organizations.map(project => project.id);
|
||||||
|
|
||||||
|
const agents = await db.query.agent.findMany({});
|
||||||
|
|
||||||
const projects = await db.query.project.findMany({
|
const projects = await db.query.project.findMany({
|
||||||
where: inArray(drizzleDb.schemas.project.organizationId, organizationIds),
|
where: inArray(drizzleDb.schemas.project.organizationId, organizationIds),
|
||||||
@@ -39,50 +42,96 @@ export default async function RoutePage(props: PageParams<{}>) {
|
|||||||
columns: {
|
columns: {
|
||||||
id: true,
|
id: true,
|
||||||
createdAt: true,
|
createdAt: true,
|
||||||
|
deletedAt: true,
|
||||||
},
|
},
|
||||||
orderBy: [asc(drizzleDb.schemas.backup.id)],
|
orderBy: [asc(drizzleDb.schemas.backup.id)],
|
||||||
where: inArray(drizzleDb.schemas.backup.databaseId, databaseIds),
|
where: inArray(drizzleDb.schemas.backup.databaseId, databaseIds),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const availableBackups = backupsEvolution.filter(backup => backup.deletedAt == null);
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
||||||
<Page>
|
<Page>
|
||||||
<PageHeader>
|
<PageHeader>
|
||||||
<PageTitle>Dashboard</PageTitle>
|
<PageTitle>Dashboard</PageTitle>
|
||||||
</PageHeader>
|
</PageHeader>
|
||||||
<PageContent className="flex flex-col gap-y-4">
|
<PageContent className="flex flex-col gap-y-4">
|
||||||
<div className="flex flex-col md:flex-row gap-4">
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||||
<Card className="w-full flex-1">
|
<Card className="w-full">
|
||||||
<CardHeader className="flex items-center gap-2">
|
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||||
<Building2 className="w-5 h-5 text-muted-foreground"/>
|
<CardTitle className="text-sm font-medium">Organizations</CardTitle>
|
||||||
<CardTitle>Organizations</CardTitle>
|
<Building2 className="h-4 w-4 text-muted-foreground"/>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="text-3xl font-bold">{organizations.length}</CardContent>
|
<CardContent>
|
||||||
</Card>
|
<div className="text-2xl font-bold">{organizations.length}</div>
|
||||||
<Card className="w-full flex-1">
|
<p className="text-xs text-muted-foreground">Number of organizations</p>
|
||||||
<CardHeader className="flex items-center gap-2">
|
</CardContent>
|
||||||
<Folder className="w-5 h-5 text-muted-foreground"/>
|
|
||||||
<CardTitle>Projects</CardTitle>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent className="text-3xl font-bold">{projects.length}</CardContent>
|
|
||||||
</Card>
|
|
||||||
<Card className="w-full flex-1">
|
|
||||||
<CardHeader className="flex items-center gap-2">
|
|
||||||
<DatabaseBackup className="w-5 h-5 text-muted-foreground"/>
|
|
||||||
<CardTitle>Backups</CardTitle>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent className="text-3xl font-bold">{backupsEvolution.length}</CardContent>
|
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
|
<Card className="w-full">
|
||||||
|
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||||
|
<CardTitle className="text-sm font-medium">Projects</CardTitle>
|
||||||
|
<Folder className="h-4 w-4 text-muted-foreground"/>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="text-2xl font-bold">{projects.length}</div>
|
||||||
|
<p className="text-xs text-muted-foreground">Number of projects</p>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card className="w-full">
|
||||||
|
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||||
|
<CardTitle className="text-sm font-medium">Databases</CardTitle>
|
||||||
|
<Database className="h-4 w-4 text-muted-foreground"/>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="text-2xl font-bold">{databasesOfAllProjects.length}</div>
|
||||||
|
<p className="text-xs text-muted-foreground">Databases across all projects</p>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card className="w-full">
|
||||||
|
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||||
|
<CardTitle className="text-sm font-medium">Agents</CardTitle>
|
||||||
|
<Workflow className="h-4 w-4 text-muted-foreground"/>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="text-2xl font-bold">{agents.length}</div>
|
||||||
|
<p className="text-xs text-muted-foreground">Registered agents</p>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card className="w-full">
|
||||||
|
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||||
|
<CardTitle className="text-sm font-medium">Total Backups</CardTitle>
|
||||||
|
<DatabaseBackup className="h-4 w-4 text-muted-foreground"/>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="text-2xl font-bold">{backupsEvolution.length}</div>
|
||||||
|
<p className="text-xs text-muted-foreground">All backups recorded</p>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card className="w-full">
|
||||||
|
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||||
|
<CardTitle className="text-sm font-medium">Available Backups</CardTitle>
|
||||||
|
<RefreshCcw className="h-4 w-4 text-muted-foreground"/>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="text-2xl font-bold">{availableBackups.length}</div>
|
||||||
|
<p className="text-xs text-muted-foreground">Currently active backups</p>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-1 flex-col gap-4">
|
{/*Do not delete*/}
|
||||||
<div className="grid auto-rows-min gap-4 md:grid-cols-3">
|
{/*<div className="flex flex-1 flex-col gap-4">*/}
|
||||||
<div className="aspect-video rounded-xl bg-muted/50"/>
|
{/* <div className="grid auto-rows-min gap-4 md:grid-cols-3">*/}
|
||||||
<div className="aspect-video rounded-xl bg-muted/50"/>
|
{/* <div className="aspect-video rounded-xl bg-muted/50"/>*/}
|
||||||
<div className="aspect-video rounded-xl bg-muted/50"/>
|
{/* <div className="aspect-video rounded-xl bg-muted/50"/>*/}
|
||||||
</div>
|
{/* <div className="aspect-video rounded-xl bg-muted/50"/>*/}
|
||||||
</div>
|
{/* </div>*/}
|
||||||
|
{/*</div>*/}
|
||||||
</PageContent>
|
</PageContent>
|
||||||
</Page>
|
</Page>
|
||||||
|
|
||||||
|
|||||||
@@ -1,26 +1,26 @@
|
|||||||
import React from "react";
|
import { ReactNode } from "react";
|
||||||
import {redirect} from "next/navigation";
|
import { redirect } from "next/navigation";
|
||||||
|
|
||||||
import {SidebarInset, SidebarProvider} from "@/components/ui/sidebar";
|
import { SidebarInset, SidebarProvider } from "@/components/ui/sidebar";
|
||||||
import {AppSidebar} from "@/components/wrappers/dashboard/common/sidebar/app-sidebar";
|
import { AppSidebar } from "@/components/wrappers/dashboard/common/sidebar/app-sidebar";
|
||||||
import {Header} from "@/features/layout/Header";
|
import { Header } from "@/features/layout/Header";
|
||||||
import {currentUser} from "@/lib/auth/current-user";
|
import { currentUser } from "@/lib/auth/current-user";
|
||||||
|
import { ThemeMetaUpdater } from "@/features/browser/theme-meta-updater";
|
||||||
|
|
||||||
export default async function Layout({children}: { children: React.ReactNode }) {
|
export default async function Layout({ children }: { children: ReactNode }) {
|
||||||
const user = await currentUser();
|
const user = await currentUser();
|
||||||
if (!user) redirect("/login");
|
if (!user) redirect("/login");
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<SidebarProvider>
|
||||||
<SidebarProvider>
|
<div className="flex flex-col lg:flex-row w-full">
|
||||||
<div className="flex flex-col lg:flex-row w-full">
|
<ThemeMetaUpdater />
|
||||||
<AppSidebar/>
|
<AppSidebar />
|
||||||
<SidebarInset>
|
<SidebarInset>
|
||||||
<Header/>
|
<Header />
|
||||||
<main className="h-full">{children}</main>
|
<main className="h-full">{children}</main>
|
||||||
</SidebarInset>
|
</SidebarInset>
|
||||||
</div>
|
</div>
|
||||||
</SidebarProvider>
|
</SidebarProvider>
|
||||||
</>
|
);
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,62 +0,0 @@
|
|||||||
import {PageParams} from "@/types/next";
|
|
||||||
import {Page, PageContent, PageTitle} from "@/features/layout/page";
|
|
||||||
import {notFound} from "next/navigation";
|
|
||||||
import {UserForm} from "@/components/wrappers/dashboard/profile/user-form/user-form";
|
|
||||||
import {Badge} from "@/components/ui/badge";
|
|
||||||
import {ButtonDeleteAccount} from "@/components/wrappers/dashboard/profile/button-delete-account/button-delete-account";
|
|
||||||
import {AvatarWithUpload} from "@/components/wrappers/dashboard/profile/avatar/avatar-with-upload";
|
|
||||||
import {currentUser} from "@/lib/auth/current-user";
|
|
||||||
import {getAccounts, getSessions} from "@/lib/auth/auth";
|
|
||||||
|
|
||||||
export default async function RoutePage(props: PageParams<{}>) {
|
|
||||||
const user = await currentUser();
|
|
||||||
if (!user) {
|
|
||||||
return notFound();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (user.role !== "user" && user.role !== "admin" && user.role !== "superadmin") {
|
|
||||||
return notFound();
|
|
||||||
}
|
|
||||||
|
|
||||||
const sessions = await getSessions();
|
|
||||||
const accounts = await getAccounts();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Page>
|
|
||||||
<div className="justify-between gap-2 sm:flex">
|
|
||||||
<PageTitle className="flex items-center">
|
|
||||||
<AvatarWithUpload
|
|
||||||
user={{
|
|
||||||
...user,
|
|
||||||
image: user.image ?? null,
|
|
||||||
role: user.role ?? null,
|
|
||||||
banned: user.banned ?? null,
|
|
||||||
banReason: user.banReason ?? null,
|
|
||||||
banExpires: user.banExpires ?? null,
|
|
||||||
deletedAt: user.deletedAt ? new Date(user.deletedAt) : null,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
{user.name}
|
|
||||||
<Badge className="ml-3 hidden lg:block">{user.role}</Badge>
|
|
||||||
</PageTitle>
|
|
||||||
{/*<PageActions className="mt-2 hidden sm:block">*/}
|
|
||||||
{/* <ButtonDeleteAccount text="Delete my account"/>*/}
|
|
||||||
{/*</PageActions>*/}
|
|
||||||
</div>
|
|
||||||
<PageContent >
|
|
||||||
<UserForm
|
|
||||||
userId={user.id}
|
|
||||||
sessions={sessions} accounts={accounts}
|
|
||||||
defaultValues={{
|
|
||||||
name: user.name,
|
|
||||||
email: user.email,
|
|
||||||
role: user.role ?? undefined,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
{/*<div className="mt-4 sm:hidden ">*/}
|
|
||||||
{/* <ButtonDeleteAccount text="Delete my account"/>*/}
|
|
||||||
{/*</div>*/}
|
|
||||||
</PageContent>
|
|
||||||
</Page>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,2 +1,52 @@
|
|||||||
|
import { NextResponse } from "next/server";
|
||||||
|
import { eq } from "drizzle-orm";
|
||||||
|
import { db } from "@/db";
|
||||||
|
import * as drizzleDb from "@/db";
|
||||||
|
|
||||||
|
export function withAgentCheck(handler: Function) {
|
||||||
|
return async (request: Request, context: { params: Promise<{ agentId: string }> }) => {
|
||||||
|
try {
|
||||||
|
const agentId = (await context.params).agentId;
|
||||||
|
|
||||||
|
const agent = await db.query.agent.findFirst({
|
||||||
|
where: eq(drizzleDb.schemas.agent.id, agentId),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!agent) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: "Agent not found" },
|
||||||
|
{ status: 404 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return handler(request, { ...context, agent });
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Error in agent middleware:", err);
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: "Internal server error" },
|
||||||
|
{ status: 500 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export async function getDatabaseOrThrow(generatedId: string) {
|
||||||
|
const database = await db.query.database.findFirst({
|
||||||
|
where: eq(drizzleDb.schemas.database.agentDatabaseId, generatedId),
|
||||||
|
with: {
|
||||||
|
project: true,
|
||||||
|
alertPolicies: true,
|
||||||
|
storagePolicies: true
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!database) {
|
||||||
|
throw NextResponse.json(
|
||||||
|
{ error: "Database associated with generatedId not found" },
|
||||||
|
{ status: 404 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return database;
|
||||||
|
}
|
||||||
@@ -1,82 +1,63 @@
|
|||||||
import {NextResponse} from "next/server";
|
import {NextResponse} from "next/server";
|
||||||
import {isUuidv4} from "@/utils/verify-uuid";
|
|
||||||
import {uploadLocalPrivate, uploadS3Private} from "@/features/upload/private/upload.action";
|
|
||||||
import {v4 as uuidv4} from "uuid";
|
|
||||||
import {eventEmitter} from "../../../events/route";
|
|
||||||
import {db} from "@/db";
|
|
||||||
import {Backup} from "@/db/schema/07_database";
|
|
||||||
import {and, eq} from "drizzle-orm";
|
import {and, eq} from "drizzle-orm";
|
||||||
import * as drizzleDb from "@/db";
|
import * as drizzleDb from "@/db";
|
||||||
import {env} from "@/env.mjs";
|
import {db as dbClient, db} from "@/db";
|
||||||
|
import {getDatabaseOrThrow, withAgentCheck} from "./helpers";
|
||||||
|
import {Backup} from "@/db/schema/07_database";
|
||||||
import {withUpdatedAt} from "@/db/utils";
|
import {withUpdatedAt} from "@/db/utils";
|
||||||
|
import {eventEmitter} from "@/features/shared/event";
|
||||||
|
import {sendNotificationsBackupRestore} from "@/features/notifications/helpers";
|
||||||
|
import {EventKind} from "@/features/notifications/types";
|
||||||
|
|
||||||
export async function POST(
|
export type BodyPost = {
|
||||||
request: Request,
|
method: "manual" | "automatic"
|
||||||
{params}: { params: Promise<{ agentId: string }> }
|
generatedId: string
|
||||||
) {
|
}
|
||||||
|
|
||||||
|
export type BodyPatch = {
|
||||||
|
backupId: string
|
||||||
|
status: "success" | "failed"
|
||||||
|
size: number
|
||||||
|
generatedId: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export const POST = withAgentCheck(async (request: Request, {params, agent}: {
|
||||||
|
params: Promise<{ agentId: string }>,
|
||||||
|
agent: any
|
||||||
|
}) => {
|
||||||
try {
|
try {
|
||||||
const contentType = request.headers.get("Content-Type");
|
const body: BodyPost = await request.json();
|
||||||
|
const method = body.method
|
||||||
if (!contentType || !contentType.includes("multipart/form-data")) {
|
const database = await getDatabaseOrThrow(body.generatedId);
|
||||||
return NextResponse.json(
|
|
||||||
{error: "Unsupported or missing Content-Type"},
|
|
||||||
{status: 400}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
eventEmitter.emit('modification', {update: true});
|
|
||||||
|
|
||||||
const agentId = (await params).agentId;
|
|
||||||
const formData = await request.formData();
|
|
||||||
const generatedId = formData.get("generatedId") as string | null;
|
|
||||||
const method = formData.get("method") as string | null;
|
|
||||||
|
|
||||||
if (!generatedId || !isUuidv4(generatedId)) {
|
|
||||||
return NextResponse.json(
|
|
||||||
{error: "generatedId is not a valid UUID"},
|
|
||||||
{status: 400}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const agent = await db.query.agent.findFirst({
|
|
||||||
where: eq(drizzleDb.schemas.agent.id, agentId),
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!agent) {
|
|
||||||
return NextResponse.json(
|
|
||||||
{error: "Agent not found"},
|
|
||||||
{status: 404}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const database = await db.query.database.findFirst({
|
|
||||||
where: eq(drizzleDb.schemas.database.agentDatabaseId, generatedId),
|
|
||||||
with: {
|
|
||||||
project: true
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!database) {
|
|
||||||
return NextResponse.json(
|
|
||||||
{error: "Database associated with generatedId not found"},
|
|
||||||
{status: 404}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
let backup: Backup | null | undefined = null;
|
let backup: Backup | null | undefined = null;
|
||||||
|
|
||||||
if (method === "automatic") {
|
if (method === "automatic") {
|
||||||
[backup] = await db
|
|
||||||
.insert(drizzleDb.schemas.backup)
|
|
||||||
.values({
|
|
||||||
status: 'ongoing',
|
|
||||||
databaseId: database.id,
|
|
||||||
})
|
|
||||||
.returning();
|
|
||||||
|
|
||||||
|
const ongoingBackup = await db.query.backup.findFirst({
|
||||||
|
where: and(
|
||||||
|
eq(drizzleDb.schemas.backup.status, 'ongoing'),
|
||||||
|
eq(drizzleDb.schemas.backup.databaseId, database.id),
|
||||||
|
),
|
||||||
|
});
|
||||||
|
|
||||||
if (!backup) {
|
if (!ongoingBackup) {
|
||||||
|
[backup] = await db
|
||||||
|
.insert(drizzleDb.schemas.backup)
|
||||||
|
.values({
|
||||||
|
status: 'ongoing',
|
||||||
|
databaseId: database.id,
|
||||||
|
})
|
||||||
|
.returning();
|
||||||
|
if (!backup) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{error: "Unable to create an automatic backup"},
|
||||||
|
{status: 500}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{error: "Unable to create an automatic backup"},
|
{error: "A backup is already ongoing"},
|
||||||
{status: 500}
|
{status: 500}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -88,7 +69,6 @@ export async function POST(
|
|||||||
),
|
),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
if (!backup) {
|
if (!backup) {
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{error: "Unable to find the corresponding backup"},
|
{error: "Unable to find the corresponding backup"},
|
||||||
@@ -97,80 +77,78 @@ export async function POST(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const status = formData.get("status") as string | null;
|
|
||||||
|
|
||||||
if (status === "success") {
|
eventEmitter.emit('modification', {update: true});
|
||||||
const file = formData.get("file") as File | null;
|
|
||||||
|
|
||||||
if (!file) {
|
return NextResponse.json(
|
||||||
return NextResponse.json(
|
{
|
||||||
{error: "File is required for successful backup"},
|
message: "Init backup success",
|
||||||
{status: 400}
|
backup: backup,
|
||||||
);
|
},
|
||||||
}
|
{status: 200}
|
||||||
|
);
|
||||||
const uuid = uuidv4();
|
|
||||||
const fileName = `${uuid}.dump`;
|
|
||||||
const buffer = Buffer.from(await file.arrayBuffer());
|
|
||||||
|
|
||||||
const [settings] = await db.select().from(drizzleDb.schemas.setting).where(eq(drizzleDb.schemas.setting.name, "system")).limit(1);
|
|
||||||
if (!settings) {
|
|
||||||
throw new Error("System settings not found.");
|
|
||||||
}
|
|
||||||
|
|
||||||
let success: boolean, message: string, filePath: string;
|
|
||||||
|
|
||||||
const result =
|
|
||||||
settings.storage === "local"
|
|
||||||
? await uploadLocalPrivate(fileName, buffer)
|
|
||||||
: await uploadS3Private(`${database.project?.slug}/${fileName}`, buffer, env.S3_BUCKET_NAME!);
|
|
||||||
|
|
||||||
({success, message, filePath} = result);
|
|
||||||
|
|
||||||
if (!success) {
|
|
||||||
return NextResponse.json(
|
|
||||||
{error: message},
|
|
||||||
{status: 500}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
await db
|
|
||||||
.update(drizzleDb.schemas.backup)
|
|
||||||
.set(withUpdatedAt({
|
|
||||||
file: fileName,
|
|
||||||
status: 'success',
|
|
||||||
}))
|
|
||||||
.where(eq(drizzleDb.schemas.backup.id, backup.id));
|
|
||||||
|
|
||||||
eventEmitter.emit('modification', {update: true});
|
|
||||||
|
|
||||||
return NextResponse.json(
|
|
||||||
{
|
|
||||||
message: "Backup successfully uploaded",
|
|
||||||
},
|
|
||||||
{status: 200}
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
|
|
||||||
await db
|
|
||||||
.update(drizzleDb.schemas.backup)
|
|
||||||
.set(withUpdatedAt({status: 'failed'}))
|
|
||||||
.where(eq(drizzleDb.schemas.backup.id, backup.id));
|
|
||||||
|
|
||||||
eventEmitter.emit('modification', {update: true});
|
|
||||||
|
|
||||||
return NextResponse.json(
|
|
||||||
{
|
|
||||||
message: "Backup successfully updated with status failed",
|
|
||||||
},
|
|
||||||
{status: 200}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error in POST handler:", error);
|
console.error("Error in POST for INIT backup:", error);
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{error: "Internal server error"},
|
{error: "Internal server error"},
|
||||||
{status: 500}
|
{status: 500}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
|
|
||||||
|
export const PATCH = withAgentCheck(async (request: Request, {params, agent}: {
|
||||||
|
params: Promise<{ agentId: string }>,
|
||||||
|
agent: any
|
||||||
|
}) => {
|
||||||
|
try {
|
||||||
|
const body: BodyPatch = await request.json();
|
||||||
|
console.log("body", body);
|
||||||
|
|
||||||
|
const status = body.status
|
||||||
|
const backupId = body.backupId
|
||||||
|
const backupSize = body.size
|
||||||
|
|
||||||
|
const database = await getDatabaseOrThrow(body.generatedId);
|
||||||
|
|
||||||
|
const backup = await db.query.backup.findFirst({
|
||||||
|
where: eq(drizzleDb.schemas.backup.id, backupId),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!backup) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{error: "No backup found"},
|
||||||
|
{status: 500}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const [backupUpdated] = await dbClient
|
||||||
|
.update(drizzleDb.schemas.backup)
|
||||||
|
.set(withUpdatedAt({
|
||||||
|
status: status,
|
||||||
|
fileSize: backupSize
|
||||||
|
}))
|
||||||
|
.where(eq(drizzleDb.schemas.backup.id, backup.id))
|
||||||
|
.returning();
|
||||||
|
|
||||||
|
|
||||||
|
eventEmitter.emit('modification', {update: true});
|
||||||
|
await sendNotificationsBackupRestore(database, status == "failed" ? "error_backup" : "success_backup" as EventKind);
|
||||||
|
|
||||||
|
return NextResponse.json(
|
||||||
|
{
|
||||||
|
message: "Backup successfully updated",
|
||||||
|
backup: backupUpdated,
|
||||||
|
},
|
||||||
|
{status: 200}
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
|
||||||
|
|
||||||
|
console.error("Error in PATCH backup:", error);
|
||||||
|
return NextResponse.json(
|
||||||
|
{error: "Internal server error"},
|
||||||
|
{status: 500}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,76 @@
|
|||||||
|
import {NextResponse} from "next/server";
|
||||||
|
import {and, eq} from "drizzle-orm";
|
||||||
|
import * as drizzleDb from "@/db";
|
||||||
|
import {db} from "@/db";
|
||||||
|
import {getDatabaseOrThrow, withAgentCheck} from "../../helpers";
|
||||||
|
import {isUuidv4} from "@/utils/verify-uuid";
|
||||||
|
import {eventEmitter} from "@/features/shared/event";
|
||||||
|
|
||||||
|
export type Body = {
|
||||||
|
generatedId: string
|
||||||
|
storageChannelId: string
|
||||||
|
backupId: string
|
||||||
|
}
|
||||||
|
export const POST = withAgentCheck(async (request: Request, {params, agent}: {
|
||||||
|
params: Promise<{ agentId: string }>,
|
||||||
|
agent: any
|
||||||
|
}) => {
|
||||||
|
try {
|
||||||
|
const body: Body = await request.json();
|
||||||
|
|
||||||
|
console.log("body", body);
|
||||||
|
|
||||||
|
const generatedId = body.generatedId;
|
||||||
|
const storageChannelId = body.storageChannelId;
|
||||||
|
const backupId = body.backupId;
|
||||||
|
|
||||||
|
if (!generatedId || !isUuidv4(generatedId)) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{error: "generatedId is not a valid UUID"},
|
||||||
|
{status: 400}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const database = await getDatabaseOrThrow(generatedId);
|
||||||
|
|
||||||
|
const backup = await db.query.backup.findFirst({
|
||||||
|
where: and(
|
||||||
|
eq(drizzleDb.schemas.backup.id, backupId),
|
||||||
|
eq(drizzleDb.schemas.backup.databaseId, database.id),
|
||||||
|
),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!backup) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{error: "Unable to find the corresponding backup"},
|
||||||
|
{status: 404}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const [backupStorage] = await db
|
||||||
|
.insert(drizzleDb.schemas.backupStorage)
|
||||||
|
.values({
|
||||||
|
backupId: backup.id,
|
||||||
|
storageChannelId: storageChannelId,
|
||||||
|
status: "pending",
|
||||||
|
})
|
||||||
|
.returning();
|
||||||
|
|
||||||
|
eventEmitter.emit('modification', {update: true});
|
||||||
|
|
||||||
|
return NextResponse.json(
|
||||||
|
{
|
||||||
|
message: "Backup storage successfully created",
|
||||||
|
backupStorage: backupStorage
|
||||||
|
},
|
||||||
|
{status: 200}
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error in POST for INIT backup:", error);
|
||||||
|
return NextResponse.json(
|
||||||
|
{error: "Internal server error"},
|
||||||
|
{status: 500}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
@@ -0,0 +1,95 @@
|
|||||||
|
import {NextResponse} from "next/server";
|
||||||
|
import {and, eq} from "drizzle-orm";
|
||||||
|
import * as drizzleDb from "@/db";
|
||||||
|
import {db as dbClient, db} from "@/db";
|
||||||
|
import {withUpdatedAt} from "@/db/utils";
|
||||||
|
import {getDatabaseOrThrow, withAgentCheck} from "../../helpers";
|
||||||
|
import {eventEmitter} from "@/features/shared/event";
|
||||||
|
|
||||||
|
export type Body = {
|
||||||
|
generatedId: string
|
||||||
|
status: "success" | "failed"
|
||||||
|
backupStorageId: string
|
||||||
|
path: string
|
||||||
|
size: number
|
||||||
|
backupId: string
|
||||||
|
}
|
||||||
|
export const PATCH = withAgentCheck(async (request: Request, {params, agent}: {
|
||||||
|
params: Promise<{ agentId: string }>,
|
||||||
|
agent: any
|
||||||
|
}) => {
|
||||||
|
try {
|
||||||
|
const body: Body = await request.json();
|
||||||
|
const generatedId = body.generatedId;
|
||||||
|
const status = body.status;
|
||||||
|
const filePath = body.path;
|
||||||
|
const fileSize = body.size;
|
||||||
|
const backupStorageId = body.backupStorageId;
|
||||||
|
const backupId = body.backupId;
|
||||||
|
|
||||||
|
|
||||||
|
console.log("body", body);
|
||||||
|
|
||||||
|
const database = await getDatabaseOrThrow(generatedId);
|
||||||
|
|
||||||
|
const backup = await db.query.backup.findFirst({
|
||||||
|
where: and(
|
||||||
|
eq(drizzleDb.schemas.backup.id, backupId),
|
||||||
|
eq(drizzleDb.schemas.backup.databaseId, database.id),
|
||||||
|
),
|
||||||
|
with: {
|
||||||
|
storages: true
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!backup) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{error: "Unable to find the corresponding backup"},
|
||||||
|
{status: 404}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const [backupStorage] = await dbClient
|
||||||
|
.update(drizzleDb.schemas.backupStorage)
|
||||||
|
.set(withUpdatedAt({
|
||||||
|
status: status,
|
||||||
|
path: filePath,
|
||||||
|
size: fileSize
|
||||||
|
}))
|
||||||
|
.where(eq(drizzleDb.schemas.backupStorage.id, backupStorageId))
|
||||||
|
.returning();
|
||||||
|
|
||||||
|
|
||||||
|
if (backup.storages.length > 0) {
|
||||||
|
const hasSuccessfulStorage = backup.storages.some(
|
||||||
|
(storage) => storage.status === "success"
|
||||||
|
);
|
||||||
|
|
||||||
|
if (hasSuccessfulStorage && backup.status !== "success") {
|
||||||
|
await db
|
||||||
|
.update(drizzleDb.schemas.backup)
|
||||||
|
.set({
|
||||||
|
status: "success",
|
||||||
|
fileSize: fileSize,
|
||||||
|
})
|
||||||
|
.where(eq(drizzleDb.schemas.backup.id, backup.id));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
eventEmitter.emit('modification', {update: true});
|
||||||
|
|
||||||
|
return NextResponse.json({
|
||||||
|
message: "Backup status successfully updated",
|
||||||
|
backupStorage: backupStorage
|
||||||
|
},
|
||||||
|
{status: 200}
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error in POST for INIT backup:", error);
|
||||||
|
return NextResponse.json(
|
||||||
|
{error: "Internal server error"},
|
||||||
|
{status: 500}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
@@ -1,10 +1,10 @@
|
|||||||
import {NextResponse} from "next/server";
|
import {NextResponse} from "next/server";
|
||||||
import {isUuidv4} from "@/utils/verify-uuid";
|
import {isUuidv4} from "@/utils/verify-uuid";
|
||||||
import {eventEmitter} from "../../../events/route";
|
|
||||||
import * as drizzleDb from "@/db";
|
import * as drizzleDb from "@/db";
|
||||||
import {db} from "@/db";
|
import {db} from "@/db";
|
||||||
import {and, eq} from "drizzle-orm";
|
import {and, eq} from "drizzle-orm";
|
||||||
|
import {sendNotificationsBackupRestore} from "@/features/notifications/helpers";
|
||||||
|
import {eventEmitter} from "@/features/shared/event";
|
||||||
|
|
||||||
export type BodyResultRestore = {
|
export type BodyResultRestore = {
|
||||||
generatedId: string
|
generatedId: string
|
||||||
@@ -13,20 +13,16 @@ export type BodyResultRestore = {
|
|||||||
type RestorationStatus = 'waiting' | 'ongoing' | 'failed' | 'success';
|
type RestorationStatus = 'waiting' | 'ongoing' | 'failed' | 'success';
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
export async function POST(
|
export async function POST(
|
||||||
request: Request,
|
request: Request,
|
||||||
{params}: { params: Promise<{ agentId: string }> }
|
{params}: { params: Promise<{ agentId: string }> }
|
||||||
) {
|
) {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
eventEmitter.emit('modification', {update: true});
|
|
||||||
|
|
||||||
const agentId = (await params).agentId
|
const agentId = (await params).agentId
|
||||||
const body: BodyResultRestore = await request.json();
|
const body: BodyResultRestore = await request.json();
|
||||||
|
|
||||||
console.log(body)
|
|
||||||
|
|
||||||
|
|
||||||
if (!isUuidv4(body.generatedId)) {
|
if (!isUuidv4(body.generatedId)) {
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
@@ -43,8 +39,10 @@ export async function POST(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const database = await db.query.database.findFirst({
|
const database = await db.query.database.findFirst({
|
||||||
where: eq(drizzleDb.schemas.database.agentDatabaseId, body.generatedId)
|
where: eq(drizzleDb.schemas.database.agentDatabaseId, body.generatedId),
|
||||||
|
with: {
|
||||||
|
alertPolicies: true
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
if (!database) {
|
if (!database) {
|
||||||
@@ -59,22 +57,21 @@ export async function POST(
|
|||||||
return NextResponse.json({error: "Unable to fin the corresponding restoration"}, {status: 404})
|
return NextResponse.json({error: "Unable to fin the corresponding restoration"}, {status: 404})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
await db
|
await db
|
||||||
.update(drizzleDb.schemas.restoration)
|
.update(drizzleDb.schemas.restoration)
|
||||||
.set({ status: body.status as RestorationStatus })
|
.set({status: body.status as RestorationStatus})
|
||||||
.where(eq(drizzleDb.schemas.restoration.id, restoration.id));
|
.where(eq(drizzleDb.schemas.restoration.id, restoration.id));
|
||||||
|
|
||||||
|
await sendNotificationsBackupRestore(database, body.status == "failed" ? "error_restore" : "success_restore");
|
||||||
|
|
||||||
const response = {
|
const response = {
|
||||||
message: true,
|
message: true,
|
||||||
details: "Restoration successfully updated"
|
details: "Restoration successfully updated"
|
||||||
}
|
}
|
||||||
|
|
||||||
eventEmitter.emit('modification', {update: true});
|
|
||||||
|
|
||||||
|
|
||||||
return Response.json(response, {status: 200})
|
return Response.json(response, {status: 200})
|
||||||
|
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error in POST handler:', error);
|
console.error('Error in POST handler:', error);
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
|
|||||||
@@ -1,24 +1,25 @@
|
|||||||
import {NextResponse} from "next/server";
|
import {NextResponse} from "next/server";
|
||||||
import {Body} from "./route";
|
import {Body} from "./route";
|
||||||
import {isUuidv4} from "@/utils/verify-uuid";
|
import {isUuidv4} from "@/utils/verify-uuid";
|
||||||
import {getFileUrlPresignedLocal, getFileUrlPreSignedS3Action} from "@/features/upload/private/upload.action";
|
|
||||||
import {Agent} from "@/db/schema/08_agent";
|
import {Agent} from "@/db/schema/08_agent";
|
||||||
import {Database} from "@/db/schema/07_database";
|
import {Database, DatabaseWith} from "@/db/schema/07_database";
|
||||||
import * as drizzleDb from "@/db";
|
import * as drizzleDb from "@/db";
|
||||||
import {db as dbClient} from "@/db";
|
import {db, db as dbClient} from "@/db";
|
||||||
import {and, eq} from "drizzle-orm";
|
import {and, eq, inArray} from "drizzle-orm";
|
||||||
import {dbmsEnumSchema, EDbmsSchema} from "@/db/schema/types";
|
import {dbmsEnumSchema, EDbmsSchema} from "@/db/schema/types";
|
||||||
import {ServerActionResult} from "@/types/action-type";
|
|
||||||
import {SafeActionResult} from "next-safe-action";
|
|
||||||
import {ZodString} from "zod";
|
|
||||||
import {withUpdatedAt} from "@/db/utils";
|
import {withUpdatedAt} from "@/db/utils";
|
||||||
|
import type {StorageInput} from "@/features/storages/types";
|
||||||
|
import {dispatchStorage} from "@/features/storages/dispatch";
|
||||||
|
import {Setting} from "@/db/schema/01_setting";
|
||||||
|
|
||||||
export async function handleDatabases(body: Body, agent: Agent, lastContact: Date) {
|
export async function handleDatabases(body: Body, agent: Agent, lastContact: Date, settings: Setting) {
|
||||||
const databasesResponse = [];
|
const databasesResponse = [];
|
||||||
|
|
||||||
const formatDatabase = (database: Database, backupAction: boolean, restoreAction: boolean, UrlBackup: string) => ({
|
const formatDatabase = (database: DatabaseWith, backupAction: boolean, restoreAction: boolean, UrlBackup: string | null, storages: PingDatabaseStorageChannels[], urlMeta: string | null) => ({
|
||||||
generatedId: database.agentDatabaseId,
|
generatedId: database.agentDatabaseId,
|
||||||
dbms: database.dbms,
|
dbms: database.dbms,
|
||||||
|
storages: storages,
|
||||||
|
encrypt: settings.encryption,
|
||||||
data: {
|
data: {
|
||||||
backup: {
|
backup: {
|
||||||
action: backupAction,
|
action: backupAction,
|
||||||
@@ -27,6 +28,7 @@ export async function handleDatabases(body: Body, agent: Agent, lastContact: Dat
|
|||||||
restore: {
|
restore: {
|
||||||
action: restoreAction,
|
action: restoreAction,
|
||||||
file: UrlBackup,
|
file: UrlBackup,
|
||||||
|
metaFile: urlMeta
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -34,12 +36,16 @@ export async function handleDatabases(body: Body, agent: Agent, lastContact: Dat
|
|||||||
for (const db of body.databases) {
|
for (const db of body.databases) {
|
||||||
|
|
||||||
const existingDatabase = await dbClient.query.database.findFirst({
|
const existingDatabase = await dbClient.query.database.findFirst({
|
||||||
where: eq(drizzleDb.schemas.database.agentDatabaseId, db.generatedId)
|
where: eq(drizzleDb.schemas.database.agentDatabaseId, db.generatedId),
|
||||||
|
with: {
|
||||||
|
project: true
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
let backupAction: boolean = false
|
let backupAction: boolean = false
|
||||||
let restoreAction: boolean = false
|
let restoreAction: boolean = false
|
||||||
let urlBackup: string = ""
|
let urlBackup: string | null = null;
|
||||||
|
let urlMeta: string | null = null
|
||||||
|
|
||||||
if (!existingDatabase) {
|
if (!existingDatabase) {
|
||||||
if (!isUuidv4(db.generatedId)) {
|
if (!isUuidv4(db.generatedId)) {
|
||||||
@@ -48,6 +54,12 @@ export async function handleDatabases(body: Body, agent: Agent, lastContact: Dat
|
|||||||
{status: 500}
|
{status: 500}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!dbmsEnumSchema.safeParse(db.dbms).success) {
|
||||||
|
console.log(`Database type not available: ${db.dbms}`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
const [databaseCreated] = await dbClient
|
const [databaseCreated] = await dbClient
|
||||||
.insert(drizzleDb.schemas.database)
|
.insert(drizzleDb.schemas.database)
|
||||||
.values({
|
.values({
|
||||||
@@ -59,90 +71,95 @@ export async function handleDatabases(body: Body, agent: Agent, lastContact: Dat
|
|||||||
})
|
})
|
||||||
.returning();
|
.returning();
|
||||||
|
|
||||||
|
|
||||||
if (databaseCreated) {
|
if (databaseCreated) {
|
||||||
databasesResponse.push(formatDatabase(databaseCreated, backupAction, restoreAction, urlBackup));
|
const storages = await getDatabaseStorageChannels(databaseCreated.id)
|
||||||
|
|
||||||
|
databasesResponse.push(formatDatabase(databaseCreated, backupAction, restoreAction, urlBackup, storages, null));
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|
||||||
|
|
||||||
const [databaseUpdated] = await dbClient
|
const [databaseUpdated] = await dbClient
|
||||||
.update(drizzleDb.schemas.database)
|
.update(drizzleDb.schemas.database)
|
||||||
.set({lastContact: lastContact})
|
.set(withUpdatedAt({
|
||||||
|
name: db.name,
|
||||||
|
agentId: agent.id,
|
||||||
|
lastContact: lastContact
|
||||||
|
}))
|
||||||
.where(eq(drizzleDb.schemas.database.id, existingDatabase.id))
|
.where(eq(drizzleDb.schemas.database.id, existingDatabase.id))
|
||||||
.returning();
|
.returning();
|
||||||
|
|
||||||
|
const activeBackup = await dbClient.query.backup.findFirst({
|
||||||
const backup = await dbClient.query.backup.findFirst({
|
where: and(
|
||||||
where: and(eq(drizzleDb.schemas.backup.databaseId, databaseUpdated.id), eq(drizzleDb.schemas.backup.status, "waiting"))
|
eq(drizzleDb.schemas.backup.databaseId, databaseUpdated.id),
|
||||||
|
inArray(drizzleDb.schemas.backup.status, ["waiting", "ongoing"])
|
||||||
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
const restoration = await dbClient.query.restoration.findFirst({
|
const restoration = await dbClient.query.restoration.findFirst({
|
||||||
where: and(eq(drizzleDb.schemas.restoration.databaseId, databaseUpdated.id), eq(drizzleDb.schemas.restoration.status, "waiting"))
|
where: and(eq(drizzleDb.schemas.restoration.databaseId, databaseUpdated.id), eq(drizzleDb.schemas.restoration.status, "waiting")),
|
||||||
|
with: {
|
||||||
|
backupStorage: true
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
if (activeBackup && activeBackup.status == "waiting") {
|
||||||
if (backup) {
|
|
||||||
backupAction = true
|
backupAction = true
|
||||||
|
|
||||||
await dbClient
|
await dbClient
|
||||||
.update(drizzleDb.schemas.backup)
|
.update(drizzleDb.schemas.backup)
|
||||||
.set(withUpdatedAt({status: "ongoing"}))
|
.set(withUpdatedAt({status: "ongoing"}))
|
||||||
.where(eq(drizzleDb.schemas.backup.id, backup.id));
|
.where(eq(drizzleDb.schemas.backup.id, activeBackup.id));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (restoration) {
|
if (restoration) {
|
||||||
restoreAction = true
|
restoreAction = true
|
||||||
|
|
||||||
|
if (!restoration.backupStorage || restoration.backupStorage.status != "success" || !restoration.backupStorage.path) {
|
||||||
const backupToRestore = await dbClient.query.backup.findFirst({
|
restoreAction = false
|
||||||
where: eq(drizzleDb.schemas.backup.id, restoration.backupId),
|
continue;
|
||||||
with: {
|
|
||||||
database: {
|
|
||||||
with: {
|
|
||||||
project: true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const [settings] = await dbClient.select().from(drizzleDb.schemas.setting).where(eq(drizzleDb.schemas.setting.name, "system")).limit(1);
|
|
||||||
if (!settings) {
|
|
||||||
return NextResponse.json(
|
|
||||||
{error: "Unable to find settings"},
|
|
||||||
{status: 500}
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const input: StorageInput = {
|
||||||
|
action: "get",
|
||||||
|
data: {
|
||||||
|
path: restoration.backupStorage.path,
|
||||||
|
signedUrl: true,
|
||||||
|
},
|
||||||
|
metadata: {
|
||||||
|
storageId: restoration.backupStorage.storageChannelId,
|
||||||
|
fileKind: "backups"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const fileName = backupToRestore?.file
|
const inputMeta: StorageInput = {
|
||||||
|
action: "get",
|
||||||
|
data: {
|
||||||
|
path: `${restoration.backupStorage.path}.meta`,
|
||||||
|
signedUrl: true,
|
||||||
|
},
|
||||||
|
metadata: {
|
||||||
|
storageId: restoration.backupStorage.storageChannelId,
|
||||||
|
fileKind: "backups"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
let data: SafeActionResult<string, ZodString, readonly [], {
|
|
||||||
_errors?: string[] | undefined;
|
|
||||||
}, readonly [], ServerActionResult<string>, object> | undefined
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
const result = await dispatchStorage(input, undefined, restoration.backupStorage.storageChannelId);
|
||||||
|
const resultMeta = await dispatchStorage(inputMeta, undefined, restoration.backupStorage.storageChannelId);
|
||||||
|
|
||||||
if (settings.storage == "local") {
|
if (result.success) {
|
||||||
data = await getFileUrlPresignedLocal(fileName!)
|
urlBackup = result.url ?? null;
|
||||||
} else if (settings.storage == "s3") {
|
urlMeta = resultMeta.url ?? null
|
||||||
|
|
||||||
data = await getFileUrlPreSignedS3Action(`backups/${backupToRestore?.database.project?.slug}/${fileName}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
if (data?.data?.success) {
|
|
||||||
urlBackup = data.data.value ?? "";
|
|
||||||
} else {
|
} else {
|
||||||
await dbClient
|
await dbClient
|
||||||
.update(drizzleDb.schemas.restoration)
|
.update(drizzleDb.schemas.restoration)
|
||||||
.set({status: "failed"})
|
.set({status: "failed"})
|
||||||
.where(eq(drizzleDb.schemas.restoration.id, restoration.id));
|
.where(eq(drizzleDb.schemas.restoration.id, restoration.id));
|
||||||
|
|
||||||
// @ts-ignore
|
const errorMessage = "Failed to get backup URL";
|
||||||
const errorMessage = data?.data?.actionError?.message || "Failed to get presigned URL";
|
|
||||||
console.error("Restoration failed: ", errorMessage);
|
console.error("Restoration failed: ", errorMessage);
|
||||||
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -151,17 +168,81 @@ export async function handleDatabases(body: Body, agent: Agent, lastContact: Dat
|
|||||||
.update(drizzleDb.schemas.restoration)
|
.update(drizzleDb.schemas.restoration)
|
||||||
.set({status: "failed"})
|
.set({status: "failed"})
|
||||||
.where(eq(drizzleDb.schemas.restoration.id, restoration.id));
|
.where(eq(drizzleDb.schemas.restoration.id, restoration.id));
|
||||||
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
await dbClient
|
await dbClient
|
||||||
.update(drizzleDb.schemas.restoration)
|
.update(drizzleDb.schemas.restoration)
|
||||||
.set({status: "ongoing"})
|
.set({status: "ongoing"})
|
||||||
.where(eq(drizzleDb.schemas.restoration.id, restoration.id));
|
.where(eq(drizzleDb.schemas.restoration.id, restoration.id));
|
||||||
}
|
}
|
||||||
databasesResponse.push(formatDatabase(databaseUpdated, backupAction, restoreAction, urlBackup));
|
const storages = await getDatabaseStorageChannels(databaseUpdated.id)
|
||||||
|
databasesResponse.push(formatDatabase(databaseUpdated, backupAction, restoreAction, urlBackup, storages, urlMeta));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return databasesResponse;
|
return databasesResponse;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
type PingDatabaseStorageChannels = {
|
||||||
|
id: string;
|
||||||
|
config: any
|
||||||
|
provider: string
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getDatabaseStorageChannels(databaseId: string): Promise<PingDatabaseStorageChannels[]> {
|
||||||
|
|
||||||
|
const database = await db.query.database.findFirst({
|
||||||
|
where: eq(drizzleDb.schemas.database.id, databaseId),
|
||||||
|
with: {
|
||||||
|
project: true,
|
||||||
|
retentionPolicy: true,
|
||||||
|
alertPolicies: true,
|
||||||
|
storagePolicies: true
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!database) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
const settings = await db.query.setting.findFirst({
|
||||||
|
where: eq(drizzleDb.schemas.setting.name, "system"),
|
||||||
|
with: {storageChannel: true},
|
||||||
|
});
|
||||||
|
|
||||||
|
const defaultStorageChannel: PingDatabaseStorageChannels[] = settings?.storageChannel
|
||||||
|
? [{
|
||||||
|
id: settings.storageChannel.id,
|
||||||
|
provider: settings.storageChannel.provider,
|
||||||
|
config: settings.storageChannel.config,
|
||||||
|
}]
|
||||||
|
: [];
|
||||||
|
|
||||||
|
|
||||||
|
const enabledDatabaseStorageChannels = await Promise.all(
|
||||||
|
(database.storagePolicies ?? [])
|
||||||
|
.filter(p => p.enabled)
|
||||||
|
.map(async policy => {
|
||||||
|
const storageChannel = await db.query.storageChannel.findFirst({
|
||||||
|
where: eq(drizzleDb.schemas.storageChannel.id, policy.storageChannelId),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!storageChannel) return null;
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: storageChannel.id,
|
||||||
|
config: storageChannel.config,
|
||||||
|
provider: storageChannel.provider,
|
||||||
|
} as PingDatabaseStorageChannels;
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
const filteredChannels: PingDatabaseStorageChannels[] = enabledDatabaseStorageChannels.filter(
|
||||||
|
(c): c is PingDatabaseStorageChannels => c !== null
|
||||||
|
);
|
||||||
|
|
||||||
|
return filteredChannels.length > 0 ? filteredChannels : defaultStorageChannel;
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
import {NextResponse} from "next/server";
|
import {NextResponse} from "next/server";
|
||||||
import {getFileUrlPresignedLocal} from "@/features/upload/private/upload.action";
|
|
||||||
import {handleDatabases} from "./helpers";
|
import {handleDatabases} from "./helpers";
|
||||||
import {eventEmitter} from "../../../events/route";
|
|
||||||
import * as drizzleDb from "@/db";
|
import * as drizzleDb from "@/db";
|
||||||
import {db} from "@/db";
|
import {db} from "@/db";
|
||||||
import {EDbmsSchema} from "@/db/schema/types";
|
import {EDbmsSchema} from "@/db/schema/types";
|
||||||
import {eq} from "drizzle-orm";
|
import {eq} from "drizzle-orm";
|
||||||
import {isUuidv4} from "@/utils/verify-uuid";
|
import {isUuidv4} from "@/utils/verify-uuid";
|
||||||
|
import {withUpdatedAt} from "@/db/utils";
|
||||||
|
import {eventEmitter} from "@/features/shared/event";
|
||||||
|
import {notFound} from "next/navigation";
|
||||||
|
|
||||||
export type databaseAgent = {
|
export type databaseAgent = {
|
||||||
name: string,
|
name: string,
|
||||||
@@ -15,16 +16,10 @@ export type databaseAgent = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export type Body = {
|
export type Body = {
|
||||||
|
version: string,
|
||||||
databases: databaseAgent[]
|
databases: databaseAgent[]
|
||||||
}
|
}
|
||||||
|
|
||||||
// Function to test the get file url presigned local
|
|
||||||
export async function GET(request: Request) {
|
|
||||||
const url = await getFileUrlPresignedLocal("d4a7fa35-2506-4d01-a612-a8ef2e2cc1c5.dump")
|
|
||||||
return Response.json({
|
|
||||||
message: url
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function POST(
|
export async function POST(
|
||||||
request: Request,
|
request: Request,
|
||||||
@@ -32,34 +27,45 @@ export async function POST(
|
|||||||
) {
|
) {
|
||||||
try {
|
try {
|
||||||
const agentId = (await params).agentId
|
const agentId = (await params).agentId
|
||||||
|
console.log(agentId)
|
||||||
const body: Body = await request.json();
|
const body: Body = await request.json();
|
||||||
const lastContact = new Date();
|
const lastContact = new Date();
|
||||||
|
let message: string
|
||||||
|
|
||||||
if (!isUuidv4(agentId)) {
|
if (!isUuidv4(agentId)) {
|
||||||
|
message = "agentId is not a valid uuid"
|
||||||
|
console.error(message)
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{error: "agentId is not a valid uuid"},
|
{error: "agentId is not a valid uuid"},
|
||||||
{status: 500}
|
{status: 500}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const agent = await db.query.agent.findFirst({
|
const agent = await db.query.agent.findFirst({
|
||||||
where: eq(drizzleDb.schemas.agent.id, agentId),
|
where: eq(drizzleDb.schemas.agent.id, agentId),
|
||||||
})
|
})
|
||||||
|
|
||||||
if (!agent) {
|
if (!agent) {
|
||||||
return NextResponse.json({error: "Agent not found"}, {status: 404})
|
message = "Agent not found"
|
||||||
|
return NextResponse.json({error: message}, {status: 404})
|
||||||
}
|
}
|
||||||
const databasesResponse = await handleDatabases(body, agent, lastContact)
|
|
||||||
|
|
||||||
|
const [settings] = await db.select().from(drizzleDb.schemas.setting).where(eq(drizzleDb.schemas.setting.name, "system")).limit(1);
|
||||||
|
if (!settings) {
|
||||||
|
return NextResponse.json({error: "An error occured"}, {status: 404})
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const databasesResponse = await handleDatabases(body, agent, lastContact, settings)
|
||||||
|
|
||||||
await db
|
await db
|
||||||
.update(drizzleDb.schemas.agent)
|
.update(drizzleDb.schemas.agent)
|
||||||
.set({ lastContact: lastContact })
|
.set(withUpdatedAt({
|
||||||
|
version: body.version,
|
||||||
|
lastContact: lastContact
|
||||||
|
}))
|
||||||
.where(eq(drizzleDb.schemas.agent.id, agentId));
|
.where(eq(drizzleDb.schemas.agent.id, agentId));
|
||||||
|
|
||||||
eventEmitter.emit('modification', {update: true});
|
|
||||||
|
|
||||||
const response = {
|
const response = {
|
||||||
agent: {
|
agent: {
|
||||||
@@ -68,7 +74,8 @@ export async function POST(
|
|||||||
},
|
},
|
||||||
databases: databasesResponse
|
databases: databasesResponse
|
||||||
}
|
}
|
||||||
console.log(response)
|
|
||||||
|
|
||||||
return Response.json(response)
|
return Response.json(response)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error in POST handler:', error);
|
console.error('Error in POST handler:', error);
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import { NextResponse } from "next/server";
|
||||||
|
|
||||||
|
export async function GET() {
|
||||||
|
return NextResponse.json({
|
||||||
|
PROJECT_URL: process.env.PROJECT_URL,
|
||||||
|
PROJECT_NAME: process.env.PROJECT_NAME,
|
||||||
|
PROJECT_DESCRIPTION: process.env.PROJECT_DESCRIPTION,
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -1,15 +1,22 @@
|
|||||||
import { EventEmitter } from 'events';
|
import {auth} from "@/lib/auth/auth";
|
||||||
|
import {headers} from "next/headers";
|
||||||
export const eventEmitter = new EventEmitter();
|
import {NextResponse} from "next/server";
|
||||||
|
import {eventEmitter} from "@/features/shared/event";
|
||||||
|
|
||||||
export async function GET(request: Request) {
|
export async function GET(request: Request) {
|
||||||
console.log('GET request received');
|
|
||||||
|
const session = await auth.api.getSession({
|
||||||
|
headers: await headers(),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!session) {
|
||||||
|
return NextResponse.json({error: "Unauthorized"}, {status: 403});
|
||||||
|
}
|
||||||
|
|
||||||
return new Response(
|
return new Response(
|
||||||
new ReadableStream({
|
new ReadableStream({
|
||||||
start(controller) {
|
start(controller) {
|
||||||
console.log('Stream started');
|
console.log('Stream started');
|
||||||
|
|
||||||
const handleModification = (data: any) => {
|
const handleModification = (data: any) => {
|
||||||
console.log('Modification event triggered:', data);
|
console.log('Modification event triggered:', data);
|
||||||
controller.enqueue(`event: modification\n`);
|
controller.enqueue(`event: modification\n`);
|
||||||
@@ -18,7 +25,6 @@ export async function GET(request: Request) {
|
|||||||
|
|
||||||
eventEmitter.on('modification', handleModification);
|
eventEmitter.on('modification', handleModification);
|
||||||
|
|
||||||
// Handle client disconnect
|
|
||||||
request.signal.addEventListener('abort', () => {
|
request.signal.addEventListener('abort', () => {
|
||||||
console.log('Client disconnected');
|
console.log('Client disconnected');
|
||||||
controller.close();
|
controller.close();
|
||||||
@@ -35,15 +41,4 @@ export async function GET(request: Request) {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
|
||||||
|
|
||||||
export async function POST(request: Request) {
|
|
||||||
console.log('POST request received');
|
|
||||||
const data = await request.json();
|
|
||||||
console.log('Data received:', data);
|
|
||||||
|
|
||||||
// Emit the event to all connected clients
|
|
||||||
eventEmitter.emit('modification', data);
|
|
||||||
|
|
||||||
return new Response('Event sent', { status: 200 });
|
|
||||||
}
|
}
|
||||||
@@ -1,29 +1,46 @@
|
|||||||
import * as fs from "node:fs";
|
|
||||||
import {NextResponse} from "next/server";
|
import {NextResponse} from "next/server";
|
||||||
import path from "path";
|
import path from "path";
|
||||||
|
import type {StorageInput} from "@/features/storages/types";
|
||||||
|
import {dispatchStorage} from "@/features/storages/dispatch";
|
||||||
|
import {Readable} from "node:stream";
|
||||||
|
|
||||||
export async function GET(
|
export async function GET(
|
||||||
request: Request,
|
request: Request,
|
||||||
{params}: { params: Promise<{ fileName: string }> }
|
|
||||||
) {
|
) {
|
||||||
|
|
||||||
const {searchParams} = new URL(request.url);
|
const {searchParams} = new URL(request.url);
|
||||||
const token = searchParams.get('token');
|
const token = searchParams.get('token');
|
||||||
const expires = searchParams.get('expires');
|
const expires = searchParams.get('expires');
|
||||||
const fileName = (await params).fileName
|
const pathFromUrl = searchParams.get('path');
|
||||||
|
const storageId = searchParams.get('storageId');
|
||||||
|
|
||||||
const privateLocalDir = "private/uploads/files/";
|
if (!pathFromUrl || !storageId) {
|
||||||
const filePath = path.join(privateLocalDir, fileName);
|
return NextResponse.json({error: "Missing search params"}, {status: 404})
|
||||||
|
|
||||||
const crypto = require('crypto');
|
|
||||||
|
|
||||||
if (!fs.existsSync(filePath)) {
|
|
||||||
return NextResponse.json(
|
|
||||||
{error: 'File not found'},
|
|
||||||
{status: 404}
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const input: StorageInput = {
|
||||||
|
action: "get",
|
||||||
|
data: {
|
||||||
|
path: pathFromUrl,
|
||||||
|
signedUrl: true,
|
||||||
|
},
|
||||||
|
metadata: {
|
||||||
|
storageId: storageId,
|
||||||
|
fileKind: "backups",
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
console.debug(input);
|
||||||
|
|
||||||
|
const result = await dispatchStorage(input, undefined, storageId);
|
||||||
|
|
||||||
|
if (!result.success) {
|
||||||
|
return NextResponse.json({error: "Enable to get file from provided storage channel, an error occurred !"})
|
||||||
|
}
|
||||||
|
|
||||||
|
const fileName = path.basename(pathFromUrl);
|
||||||
|
|
||||||
|
const crypto = require('crypto');
|
||||||
const expectedToken = crypto.createHash('sha256').update(`${fileName}${expires}`).digest('hex');
|
const expectedToken = crypto.createHash('sha256').update(`${fileName}${expires}`).digest('hex');
|
||||||
if (token !== expectedToken) {
|
if (token !== expectedToken) {
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
@@ -31,8 +48,8 @@ export async function GET(
|
|||||||
{status: 403}
|
{status: 403}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
//@ts-ignore
|
|
||||||
const expiresAt = parseInt(expires, 10);
|
const expiresAt = parseInt(expires!, 10);
|
||||||
if (Date.now() > expiresAt) {
|
if (Date.now() > expiresAt) {
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{error: 'Signed token expired'},
|
{error: 'Signed token expired'},
|
||||||
@@ -40,7 +57,15 @@ export async function GET(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const fileStream = fs.createReadStream(filePath);
|
if (!result.file || !(result.file instanceof Readable)) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{error: "Invalid file payload"},
|
||||||
|
{status: 500}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const fileStream = Readable.from(result.file as Readable);
|
||||||
|
|
||||||
const stream = new ReadableStream({
|
const stream = new ReadableStream({
|
||||||
start(controller) {
|
start(controller) {
|
||||||
fileStream.on('data', (chunk) => controller.enqueue(chunk));
|
fileStream.on('data', (chunk) => controller.enqueue(chunk));
|
||||||
@@ -0,0 +1,88 @@
|
|||||||
|
import {NextResponse} from "next/server";
|
||||||
|
import {auth} from "@/lib/auth/auth";
|
||||||
|
import {headers} from "next/headers";
|
||||||
|
import {db} from "@/db";
|
||||||
|
import * as drizzleDb from "@/db";
|
||||||
|
import {eq} from "drizzle-orm";
|
||||||
|
import {StorageInput} from "@/features/storages/types";
|
||||||
|
import {dispatchStorage} from "@/features/storages/dispatch";
|
||||||
|
import {Readable} from "node:stream";
|
||||||
|
|
||||||
|
export async function GET(
|
||||||
|
req: Request,
|
||||||
|
{params}: { params: Promise<{ fileName: string }> }
|
||||||
|
) {
|
||||||
|
const {searchParams} = new URL(req.url);
|
||||||
|
const fileName = (await params).fileName;
|
||||||
|
const storageId = searchParams.get('storageId');
|
||||||
|
|
||||||
|
if (!fileName) return NextResponse.json({error: "Missing file parameter"}, {status: 400});
|
||||||
|
|
||||||
|
const session = await auth.api.getSession({headers: await headers()});
|
||||||
|
if (!session) return NextResponse.json({error: "Unauthorized"}, {status: 403});
|
||||||
|
|
||||||
|
|
||||||
|
if (!storageId) {
|
||||||
|
return NextResponse.json({error: "Missing storageId in search params"}, {status: 404})
|
||||||
|
}
|
||||||
|
|
||||||
|
const ext = fileName.split(".").pop()?.toLowerCase();
|
||||||
|
const contentType =
|
||||||
|
ext === "png"
|
||||||
|
? "image/png"
|
||||||
|
: ext === "jpg" || ext === "jpeg"
|
||||||
|
? "image/jpeg"
|
||||||
|
: ext === "gif"
|
||||||
|
? "image/gif"
|
||||||
|
: ext === "webp"
|
||||||
|
? "image/webp"
|
||||||
|
: "application/octet-stream";
|
||||||
|
|
||||||
|
try {
|
||||||
|
|
||||||
|
const path = `images/${fileName}`;
|
||||||
|
|
||||||
|
const input: StorageInput = {
|
||||||
|
action: "get",
|
||||||
|
data: {
|
||||||
|
path: path,
|
||||||
|
},
|
||||||
|
metadata: {
|
||||||
|
storageId: storageId,
|
||||||
|
fileKind: "images"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await dispatchStorage(input, undefined, storageId);
|
||||||
|
|
||||||
|
if (!result.file || !(result.file instanceof Readable)) {
|
||||||
|
console.error(`An error occurred while getting file :`, result);
|
||||||
|
return NextResponse.json(
|
||||||
|
{error: "Invalid file payload"},
|
||||||
|
{status: 500}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const fileStream = Readable.from(result.file as Readable);
|
||||||
|
|
||||||
|
const stream = new ReadableStream({
|
||||||
|
start(controller) {
|
||||||
|
fileStream.on('data', (chunk) => controller.enqueue(chunk));
|
||||||
|
fileStream.on('end', () => controller.close());
|
||||||
|
fileStream.on('error', (err) => controller.error(err));
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return new NextResponse(stream, {
|
||||||
|
headers: {
|
||||||
|
'Content-Disposition': `inline; filename="${fileName}"`,
|
||||||
|
"Cache-Control": "no-store",
|
||||||
|
"Content-Type": contentType,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Error streaming image:", err);
|
||||||
|
return NextResponse.json({error: "Error fetching file"}, {status: 500});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
export async function GET(request: Request) {
|
||||||
|
const url = new URL(request.url);
|
||||||
|
const code = url.searchParams.get("code");
|
||||||
|
|
||||||
|
const success = Boolean(code);
|
||||||
|
|
||||||
|
return new Response(
|
||||||
|
`
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<body>
|
||||||
|
<h1>${success ? "Success" : "Failed"}</h1>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
`,
|
||||||
|
{
|
||||||
|
status: success ? 200 : 400,
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "text/html; charset=utf-8",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
import path from "path";
|
|
||||||
import fs from "fs/promises";
|
|
||||||
import { NextResponse } from "next/server";
|
|
||||||
|
|
||||||
export async function GET(
|
|
||||||
request: Request,
|
|
||||||
{params}: { params: Promise<{ fileName: string }> }
|
|
||||||
) {
|
|
||||||
|
|
||||||
try {
|
|
||||||
const fileName = (await params).fileName;
|
|
||||||
|
|
||||||
|
|
||||||
console.log("fileName", fileName);
|
|
||||||
|
|
||||||
const filePath = path.join(process.cwd(), "private/uploads/images", fileName);
|
|
||||||
|
|
||||||
// Check if the file exists
|
|
||||||
try {
|
|
||||||
await fs.access(filePath); // Ensures the file exists
|
|
||||||
} catch {
|
|
||||||
return NextResponse.json({ error: "File not found" }, { status: 404 });
|
|
||||||
}
|
|
||||||
|
|
||||||
// Read the file
|
|
||||||
const fileContent = await fs.readFile(filePath); // Returns a Buffer
|
|
||||||
|
|
||||||
return new NextResponse(fileContent, {
|
|
||||||
headers: {
|
|
||||||
"Content-Disposition": `attachment; filename="${fileName}"`,
|
|
||||||
"Content-Type": "application/octet-stream", // Adjust MIME type as needed
|
|
||||||
},
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error reading file:", error);
|
|
||||||
return NextResponse.json({ error: "Internal Server Error" }, { status: 500 });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
import { NextResponse } from "next/server";
|
|
||||||
|
|
||||||
export type BodyInit = {
|
|
||||||
initialize: boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
export async function POST(request: Request) {
|
|
||||||
try {
|
|
||||||
const body: BodyInit = await request.json();
|
|
||||||
|
|
||||||
console.log(body);
|
|
||||||
|
|
||||||
return NextResponse.json(
|
|
||||||
{
|
|
||||||
message: "Initialization successfully done!",
|
|
||||||
},
|
|
||||||
{ status: 200 }
|
|
||||||
);
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error in POST initialization:", error);
|
|
||||||
return NextResponse.json({ error: "Internal server error" }, { status: 500 });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
import {NextResponse} from "next/server";
|
||||||
|
import fs from "fs";
|
||||||
|
import path from "path";
|
||||||
|
import {env} from "@/env.mjs";
|
||||||
|
|
||||||
|
|
||||||
|
export async function POST(request: Request) {
|
||||||
|
try {
|
||||||
|
const body = await request.json();
|
||||||
|
const event = body.Event
|
||||||
|
const headers = event.HTTPRequest.Header
|
||||||
|
const uploadLength = headers["X-File-Size"]?.[0];
|
||||||
|
const uploadOffset = headers["Upload-Offset"]?.[0];
|
||||||
|
const status = headers["X-Status"]?.[0];
|
||||||
|
|
||||||
|
console.log(`Upload ID : ${event.Upload.ID} (${uploadOffset}/${uploadLength})`);
|
||||||
|
|
||||||
|
if (status === "success") {
|
||||||
|
if (
|
||||||
|
body.Type === "post-receive" &&
|
||||||
|
event.Upload.SizeIsDeferred === false &&
|
||||||
|
event.Upload.Offset === event.Upload.Size
|
||||||
|
) {
|
||||||
|
const id = event.Upload.ID;
|
||||||
|
const fileName = headers["X-File-Name"]?.[0];
|
||||||
|
const filePath = headers["X-File-Path"]?.[0];
|
||||||
|
|
||||||
|
if (!filePath) {
|
||||||
|
return NextResponse.json({error: "Missing X-File-Path"}, {status: 500});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// const uploadDir = path.join(process.cwd(), "/private/uploads/");
|
||||||
|
const uploadDir = path.join(env.PRIVATE_PATH, "/uploads/");
|
||||||
|
|
||||||
|
const oldFilePath = path.join(uploadDir, "tmp", id);
|
||||||
|
const newFilePath = path.join(uploadDir, filePath);
|
||||||
|
|
||||||
|
fs.mkdirSync(path.dirname(newFilePath), {recursive: true});
|
||||||
|
|
||||||
|
let retries = 10;
|
||||||
|
while (!fs.existsSync(oldFilePath)) {
|
||||||
|
if (retries-- === 0) {
|
||||||
|
return NextResponse.json({error: `Upload file not found: ${oldFilePath}`}, {status: 500});
|
||||||
|
}
|
||||||
|
await new Promise(r => setTimeout(r, 200));
|
||||||
|
}
|
||||||
|
|
||||||
|
fs.renameSync(oldFilePath, newFilePath);
|
||||||
|
|
||||||
|
const infoFilePath = `${oldFilePath}.info`;
|
||||||
|
if (fs.existsSync(infoFilePath)) {
|
||||||
|
fs.unlinkSync(infoFilePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
const metadataHeaderB64 = headers["Upload-Metadata"]?.[0];
|
||||||
|
|
||||||
|
if (metadataHeaderB64) {
|
||||||
|
const metadataHeader = Buffer.from(metadataHeaderB64, "base64").toString("utf-8");
|
||||||
|
if (metadataHeader) {
|
||||||
|
const tomlContent = metadataHeader
|
||||||
|
.split(",")
|
||||||
|
.map((pair) => {
|
||||||
|
const [key, value] = pair.split(" ");
|
||||||
|
const escapedValue = value.replace(/"/g, '\\"');
|
||||||
|
return `${key} = "${escapedValue}"`;
|
||||||
|
})
|
||||||
|
.join("\n");
|
||||||
|
const metaFilePath = `${newFilePath}.meta`;
|
||||||
|
fs.writeFileSync(metaFilePath, tomlContent, "utf-8");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return NextResponse.json({});
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Hook error:", error);
|
||||||
|
return NextResponse.json({error: "Internal server error"}, {status: 500});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 6.4 KiB |
@@ -1,85 +1,33 @@
|
|||||||
// "use client"
|
"use client";
|
||||||
//
|
|
||||||
// import {useSearchParams} from "next/navigation"
|
|
||||||
//
|
|
||||||
// enum Error {
|
|
||||||
// Configuration = "Configuration",
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// const errorMap = {
|
|
||||||
// [Error.Configuration]: (
|
|
||||||
// <p>
|
|
||||||
// There was a problem when trying to authenticate. Please contact us if this
|
|
||||||
// error persists. Unique error code:{" "}
|
|
||||||
// <code className="rounded-sm bg-slate-100 p-1 text-xs">Configuration</code>
|
|
||||||
// </p>
|
|
||||||
// ),
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// export default function AuthErrorPage() {
|
|
||||||
// const search = useSearchParams()
|
|
||||||
// const error = search.get("error") as Error
|
|
||||||
//
|
|
||||||
// return (
|
|
||||||
// <div className="flex h-screen w-full flex-col items-center justify-center">
|
|
||||||
// <a
|
|
||||||
// href="#"
|
|
||||||
// className="block max-w-sm rounded-lg border border-gray-200 bg-white p-6 text-center shadow-sm hover:bg-gray-100 dark:border-gray-700 dark:bg-gray-800 dark:hover:bg-gray-700"
|
|
||||||
// >
|
|
||||||
// <h5 className="mb-2 flex flex-row items-center justify-center gap-2 text-xl font-bold tracking-tight text-gray-900 dark:text-white">
|
|
||||||
// Something went wrong
|
|
||||||
// </h5>
|
|
||||||
// <div className="font-normal text-gray-700 dark:text-gray-400">
|
|
||||||
// {errorMap[error] || "Please contact us if this error persists."}
|
|
||||||
// </div>
|
|
||||||
// </a>
|
|
||||||
// </div>
|
|
||||||
// )
|
|
||||||
// }
|
|
||||||
"use client"
|
|
||||||
|
|
||||||
import { useSearchParams } from "next/navigation"
|
import { Loader2 } from "lucide-react";
|
||||||
import React, { Suspense } from "react"
|
import { useRouter, useSearchParams } from "next/navigation";
|
||||||
|
import { useEffect } from "react";
|
||||||
|
import { useSession } from "@/lib/auth/auth-client";
|
||||||
|
|
||||||
enum Error {
|
export default function ErrorPage() {
|
||||||
Configuration = "Configuration",
|
const router = useRouter();
|
||||||
|
const searchParams = useSearchParams();
|
||||||
|
const { data: session, isPending } = useSession();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isPending) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const error = searchParams.get("error");
|
||||||
|
const params = new URLSearchParams();
|
||||||
|
if (error) {
|
||||||
|
params.set("error", error);
|
||||||
|
}
|
||||||
|
|
||||||
|
const destination = session ? "/dashboard/home" : "/login";
|
||||||
|
router.replace(`${destination}?${params.toString()}`);
|
||||||
|
}, [isPending, session, router, searchParams]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex h-screen w-full items-center justify-center">
|
||||||
|
<Loader2 className="h-8 w-8 animate-spin" />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const errorMap = {
|
|
||||||
[Error.Configuration]: (
|
|
||||||
<p>
|
|
||||||
There was a problem when trying to authenticate. Please contact us if this
|
|
||||||
error persists. Unique error code:{" "}
|
|
||||||
<code className="rounded-sm bg-slate-100 p-1 text-xs">Configuration</code>
|
|
||||||
</p>
|
|
||||||
),
|
|
||||||
}
|
|
||||||
|
|
||||||
function AuthErrorPageContent() {
|
|
||||||
const search = useSearchParams()
|
|
||||||
const error = search.get("error") as Error
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="flex h-screen w-full flex-col items-center justify-center">
|
|
||||||
<a
|
|
||||||
href="#"
|
|
||||||
className="block max-w-sm rounded-lg border border-gray-200 bg-white p-6 text-center shadow-sm hover:bg-gray-100 dark:border-gray-700 dark:bg-gray-800 dark:hover:bg-gray-700"
|
|
||||||
>
|
|
||||||
<h5 className="mb-2 flex flex-row items-center justify-center gap-2 text-xl font-bold tracking-tight text-gray-900 dark:text-white">
|
|
||||||
Something went wrong
|
|
||||||
</h5>
|
|
||||||
<div className="font-normal text-gray-700 dark:text-gray-400">
|
|
||||||
{errorMap[error] || "Please contact us if this error persists."}
|
|
||||||
</div>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function AuthErrorPage() {
|
|
||||||
return (
|
|
||||||
<Suspense fallback={<div>Loading...</div>}>
|
|
||||||
<AuthErrorPageContent />
|
|
||||||
</Suspense>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
@@ -6,8 +6,11 @@
|
|||||||
@theme inline {
|
@theme inline {
|
||||||
--color-background: var(--background);
|
--color-background: var(--background);
|
||||||
--color-foreground: var(--foreground);
|
--color-foreground: var(--foreground);
|
||||||
--font-sans: var(--font-geist-sans);
|
--font-sans: var(--font-poppins), ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
|
||||||
|
--font-title: var(--font-poppins), var(--font-poppins), sans-serif;
|
||||||
--font-mono: var(--font-geist-mono);
|
--font-mono: var(--font-geist-mono);
|
||||||
|
--font-author: var(--font-author);
|
||||||
|
--font-poppins: var(--font-poppins);
|
||||||
--color-sidebar-ring: var(--sidebar-ring);
|
--color-sidebar-ring: var(--sidebar-ring);
|
||||||
--color-sidebar-border: var(--sidebar-border);
|
--color-sidebar-border: var(--sidebar-border);
|
||||||
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
||||||
@@ -71,7 +74,7 @@
|
|||||||
--card-foreground: oklch(0.145 0 0);
|
--card-foreground: oklch(0.145 0 0);
|
||||||
--popover: oklch(1 0 0);
|
--popover: oklch(1 0 0);
|
||||||
--popover-foreground: oklch(0.145 0 0);
|
--popover-foreground: oklch(0.145 0 0);
|
||||||
--primary: oklch(0.205 0 0);
|
--primary: oklch(0.611 0.204 42.149);
|
||||||
--primary-foreground: oklch(0.985 0 0);
|
--primary-foreground: oklch(0.985 0 0);
|
||||||
--secondary: oklch(0.97 0 0);
|
--secondary: oklch(0.97 0 0);
|
||||||
--secondary-foreground: oklch(0.205 0 0);
|
--secondary-foreground: oklch(0.205 0 0);
|
||||||
@@ -82,7 +85,7 @@
|
|||||||
--destructive: oklch(0.577 0.245 27.325);
|
--destructive: oklch(0.577 0.245 27.325);
|
||||||
--border: oklch(0.922 0 0);
|
--border: oklch(0.922 0 0);
|
||||||
--input: oklch(0.922 0 0);
|
--input: oklch(0.922 0 0);
|
||||||
--ring: oklch(0.708 0 0);
|
--ring: oklch(0.611 0.204 42.149);
|
||||||
--chart-1: oklch(0.646 0.222 41.116);
|
--chart-1: oklch(0.646 0.222 41.116);
|
||||||
--chart-2: oklch(0.6 0.118 184.704);
|
--chart-2: oklch(0.6 0.118 184.704);
|
||||||
--chart-3: oklch(0.398 0.07 227.392);
|
--chart-3: oklch(0.398 0.07 227.392);
|
||||||
@@ -90,12 +93,12 @@
|
|||||||
--chart-5: oklch(0.769 0.188 70.08);
|
--chart-5: oklch(0.769 0.188 70.08);
|
||||||
--sidebar: oklch(0.985 0 0);
|
--sidebar: oklch(0.985 0 0);
|
||||||
--sidebar-foreground: oklch(0.145 0 0);
|
--sidebar-foreground: oklch(0.145 0 0);
|
||||||
--sidebar-primary: oklch(0.205 0 0);
|
--sidebar-primary: oklch(0.611 0.204 42.149);
|
||||||
--sidebar-primary-foreground: oklch(0.985 0 0);
|
--sidebar-primary-foreground: oklch(0.985 0 0);
|
||||||
--sidebar-accent: oklch(0.97 0 0);
|
--sidebar-accent: oklch(0.97 0 0);
|
||||||
--sidebar-accent-foreground: oklch(0.205 0 0);
|
--sidebar-accent-foreground: oklch(0.205 0 0);
|
||||||
--sidebar-border: oklch(0.922 0 0);
|
--sidebar-border: oklch(0.922 0 0);
|
||||||
--sidebar-ring: oklch(0.708 0 0);
|
--sidebar-ring: oklch(0.611 0.204 42.149);
|
||||||
}
|
}
|
||||||
|
|
||||||
.dark {
|
.dark {
|
||||||
@@ -105,8 +108,8 @@
|
|||||||
--card-foreground: oklch(0.985 0 0);
|
--card-foreground: oklch(0.985 0 0);
|
||||||
--popover: oklch(0.205 0 0);
|
--popover: oklch(0.205 0 0);
|
||||||
--popover-foreground: oklch(0.985 0 0);
|
--popover-foreground: oklch(0.985 0 0);
|
||||||
--primary: oklch(0.922 0 0);
|
--primary: oklch(0.704 0.191 47.132);
|
||||||
--primary-foreground: oklch(0.205 0 0);
|
--primary-foreground: oklch(0.985 0 0);
|
||||||
--secondary: oklch(0.269 0 0);
|
--secondary: oklch(0.269 0 0);
|
||||||
--secondary-foreground: oklch(0.985 0 0);
|
--secondary-foreground: oklch(0.985 0 0);
|
||||||
--muted: oklch(0.269 0 0);
|
--muted: oklch(0.269 0 0);
|
||||||
@@ -116,7 +119,7 @@
|
|||||||
--destructive: oklch(0.704 0.191 22.216);
|
--destructive: oklch(0.704 0.191 22.216);
|
||||||
--border: oklch(1 0 0 / 10%);
|
--border: oklch(1 0 0 / 10%);
|
||||||
--input: oklch(1 0 0 / 15%);
|
--input: oklch(1 0 0 / 15%);
|
||||||
--ring: oklch(0.556 0 0);
|
--ring: oklch(0.704 0.191 47.132);
|
||||||
--chart-1: oklch(0.488 0.243 264.376);
|
--chart-1: oklch(0.488 0.243 264.376);
|
||||||
--chart-2: oklch(0.696 0.17 162.48);
|
--chart-2: oklch(0.696 0.17 162.48);
|
||||||
--chart-3: oklch(0.769 0.188 70.08);
|
--chart-3: oklch(0.769 0.188 70.08);
|
||||||
@@ -124,12 +127,12 @@
|
|||||||
--chart-5: oklch(0.645 0.246 16.439);
|
--chart-5: oklch(0.645 0.246 16.439);
|
||||||
--sidebar: oklch(0.205 0 0);
|
--sidebar: oklch(0.205 0 0);
|
||||||
--sidebar-foreground: oklch(0.985 0 0);
|
--sidebar-foreground: oklch(0.985 0 0);
|
||||||
--sidebar-primary: oklch(0.488 0.243 264.376);
|
--sidebar-primary: oklch(0.704 0.191 47.132);
|
||||||
--sidebar-primary-foreground: oklch(0.985 0 0);
|
--sidebar-primary-foreground: oklch(0.985 0 0);
|
||||||
--sidebar-accent: oklch(0.269 0 0);
|
--sidebar-accent: oklch(0.269 0 0);
|
||||||
--sidebar-accent-foreground: oklch(0.985 0 0);
|
--sidebar-accent-foreground: oklch(0.985 0 0);
|
||||||
--sidebar-border: oklch(1 0 0 / 10%);
|
--sidebar-border: oklch(1 0 0 / 10%);
|
||||||
--sidebar-ring: oklch(0.556 0 0);
|
--sidebar-ring: oklch(0.704 0.191 47.132);
|
||||||
}
|
}
|
||||||
|
|
||||||
@layer base {
|
@layer base {
|
||||||
@@ -137,6 +140,18 @@
|
|||||||
@apply border-border outline-ring/50;
|
@apply border-border outline-ring/50;
|
||||||
}
|
}
|
||||||
body {
|
body {
|
||||||
@apply bg-background text-foreground;
|
@apply bg-background text-foreground font-sans;
|
||||||
|
}
|
||||||
|
h1, h2, h3, h4, h5, h6 {
|
||||||
|
@apply font-title;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scrollbar-hide {
|
||||||
|
-ms-overflow-style: none;
|
||||||
|
scrollbar-width: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scrollbar-hide::-webkit-scrollbar {
|
||||||
|
display: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 90 KiB After Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 7.7 KiB After Width: | Height: | Size: 3.9 KiB |
@@ -1,32 +1,42 @@
|
|||||||
import React from "react";
|
import type { Metadata } from "next";
|
||||||
import type {Metadata} from "next";
|
import type React from "react";
|
||||||
import {Inter} from "next/font/google";
|
|
||||||
import "./globals.css";
|
import "./globals.css";
|
||||||
import {Providers} from "./providers";
|
import { ConsoleSilencer } from "@/components/wrappers/common/console-silencer";
|
||||||
import {cn} from "@/lib/utils";
|
import { author, geistMono, poppins } from "@/fonts/fonts";
|
||||||
import {ConsoleSilencer} from "@/components/wrappers/common/console-silencer";
|
import { cn } from "@/lib/utils";
|
||||||
|
import { Providers } from "./providers";
|
||||||
|
|
||||||
const inter = Inter({subsets: ["latin"]});
|
const title = process.env.PROJECT_NAME ?? "App Title";
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: process.env.NEXT_PUBLIC_PROJECT_NAME ?? "App Title",
|
title: {
|
||||||
description: process.env.NEXT_PUBLIC_PROJECT_DESCRIPTION ?? undefined,
|
default: title,
|
||||||
|
template: `%s - ${title}`,
|
||||||
|
},
|
||||||
|
description: process.env.PROJECT_DESCRIPTION ?? undefined,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function RootLayout({
|
export default async function RootLayout({
|
||||||
children,
|
children,
|
||||||
}: Readonly<{
|
}: Readonly<{
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
}>) {
|
}>) {
|
||||||
return (
|
return (
|
||||||
<html lang="en" suppressHydrationWarning>
|
<html lang="en" suppressHydrationWarning>
|
||||||
<head>
|
<head>
|
||||||
<meta name="apple-mobile-web-app-title" content="Portabase"/>
|
<meta name="apple-mobile-web-app-title" content={title} />
|
||||||
</head>
|
</head>
|
||||||
<body className={cn(inter.className, "h-full")}>
|
<body
|
||||||
<ConsoleSilencer/>
|
className={cn(
|
||||||
|
poppins.variable,
|
||||||
|
author.variable,
|
||||||
|
geistMono.variable,
|
||||||
|
"font-sans h-full",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<ConsoleSilencer />
|
||||||
<Providers>{children}</Providers>
|
<Providers>{children}</Providers>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,19 @@
|
|||||||
|
import BackButton from "@/components/wrappers/common/button/back-button";
|
||||||
|
|
||||||
export default async function NotFound() {
|
export default async function NotFound() {
|
||||||
return(
|
return (
|
||||||
<div className="flex items-center min-h-screen px-4 py-12 sm:px-6 md:px-8 lg:px-12 xl:px-16">
|
<div className="flex items-center min-h-screen px-4 py-12 sm:px-6 md:px-8 lg:px-12 xl:px-16">
|
||||||
<div className="w-full space-y-6 text-center">
|
<div className="w-full space-y-6 text-center">
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<h1 className="scroll-m-20 text-4xl font-extrabold tracking-tight lg:text-5xl">Not found</h1>
|
<h1 className="scroll-m-20 text-4xl font-extrabold tracking-tight lg:text-5xl">
|
||||||
<p className="leading-7 [&:not(:first-child)]:mt-6">The content you are trying to view is not available.</p>
|
Not found
|
||||||
</div>
|
</h1>
|
||||||
</div>
|
<p className="leading-7 not-first:mt-6">
|
||||||
|
The content you are trying to view is not available.
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
)
|
<BackButton>Go home</BackButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,23 +1,28 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { PropsWithChildren, Suspense } from "react";
|
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||||
import { ThemeProvider } from "@/features/theme/theme-provider";
|
import { type PropsWithChildren, Suspense } from "react";
|
||||||
|
|
||||||
import { Toaster } from "@/components/ui/sonner";
|
import { Toaster } from "@/components/ui/sonner";
|
||||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
import { ErrorLayout } from "@/components/wrappers/common/error-layout";
|
||||||
|
import { ThemeMetaUpdaterRoot } from "@/features/browser/theme-meta-updater-root";
|
||||||
|
import { ThemeProvider } from "@/features/theme/theme-provider";
|
||||||
|
|
||||||
export type ProviderProps = PropsWithChildren<{}>;
|
export type ProviderProps = PropsWithChildren<{}>;
|
||||||
const queryClient = new QueryClient();
|
const queryClient = new QueryClient();
|
||||||
|
|
||||||
export const Providers = (props: ProviderProps) => {
|
export const Providers = (props: ProviderProps) => {
|
||||||
return (
|
return (
|
||||||
<Suspense fallback={null}>
|
<Suspense fallback={null}>
|
||||||
<ThemeProvider attribute="class" defaultTheme="system" enableSystem>
|
<ThemeProvider attribute="class" defaultTheme="system" enableSystem>
|
||||||
<QueryClientProvider client={queryClient}>
|
<ThemeMetaUpdaterRoot />
|
||||||
<Toaster />
|
<QueryClientProvider client={queryClient}>
|
||||||
{props.children}
|
<ErrorLayout>
|
||||||
</QueryClientProvider>
|
<Toaster />
|
||||||
</ThemeProvider>
|
{props.children}
|
||||||
</Suspense>
|
</ErrorLayout>
|
||||||
);
|
</QueryClientProvider>
|
||||||
|
</ThemeProvider>
|
||||||
|
</Suspense>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -10,11 +10,15 @@
|
|||||||
"cssVariables": true,
|
"cssVariables": true,
|
||||||
"prefix": ""
|
"prefix": ""
|
||||||
},
|
},
|
||||||
|
"iconLibrary": "radix",
|
||||||
"aliases": {
|
"aliases": {
|
||||||
"components": "@/components",
|
"components": "@/components",
|
||||||
"utils": "@/lib/utils",
|
"utils": "@/lib/utils",
|
||||||
"ui": "@/components/ui",
|
"ui": "@/components/ui",
|
||||||
"lib": "@/lib",
|
"lib": "@/lib",
|
||||||
"hooks": "@/hooks"
|
"hooks": "@/hooks"
|
||||||
|
},
|
||||||
|
"registries": {
|
||||||
|
"@reui": "https://reui.io/r/{name}.json"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,35 @@
|
|||||||
|
name: portabase-dev-func
|
||||||
|
|
||||||
|
services:
|
||||||
|
keycloak:
|
||||||
|
image: quay.io/keycloak/keycloak:latest
|
||||||
|
command: start-dev --import-realm
|
||||||
|
# environment:
|
||||||
|
# KC_BOOTSTRAP_ADMIN_USERNAME: admin
|
||||||
|
# KC_BOOTSTRAP_ADMIN_PASSWORD: admin
|
||||||
|
ports:
|
||||||
|
- "3056:8080"
|
||||||
|
volumes:
|
||||||
|
- keycloak-data:/opt/keycloak/data
|
||||||
|
- ./seeds/keycloak:/opt/keycloak/data/import:ro
|
||||||
|
- ./export:/tmp/export
|
||||||
|
pocket-id:
|
||||||
|
image: ghcr.io/pocket-id/pocket-id
|
||||||
|
restart: unless-stopped
|
||||||
|
environment:
|
||||||
|
- APP_URL=http://localhost:3055
|
||||||
|
- ENCRYPTION_KEY=QwHyjbZvSsDUAcjpdmSPsuYxaH6vET6OeBaeLwXccCb43L6Om3W1AoU5pKIJTzYr
|
||||||
|
ports:
|
||||||
|
- 3055:1411
|
||||||
|
volumes:
|
||||||
|
- pocket-id-data:/app/data
|
||||||
|
- ./seeds/pocket-id:/seed:ro
|
||||||
|
healthcheck:
|
||||||
|
test: "curl -f http://localhost:1411/healthz"
|
||||||
|
interval: 1m30s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 2
|
||||||
|
start_period: 10s
|
||||||
|
volumes:
|
||||||
|
keycloak-data:
|
||||||
|
pocket-id-data:
|
||||||
@@ -1,24 +1,23 @@
|
|||||||
name: portabase-prod
|
|
||||||
|
|
||||||
services:
|
services:
|
||||||
|
|
||||||
app:
|
app:
|
||||||
build:
|
# build:
|
||||||
context: .
|
# context: .
|
||||||
dockerfile: docker/dockerfile/Dockerfile
|
# dockerfile: docker/dockerfile/Dockerfile
|
||||||
target: prod
|
# target: prod
|
||||||
|
image: portabase/portabase:1.4.0
|
||||||
ports:
|
ports:
|
||||||
- '8887:80'
|
- '8887:80'
|
||||||
|
environment:
|
||||||
|
TZ: "Europe/Paris"
|
||||||
env_file:
|
env_file:
|
||||||
- .env
|
- .env
|
||||||
|
volumes:
|
||||||
|
- portabase-data:/data
|
||||||
depends_on:
|
depends_on:
|
||||||
db:
|
db:
|
||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
|
|
||||||
container_name: portabase-app-prod
|
container_name: portabase-app-prod
|
||||||
|
|
||||||
|
|
||||||
db:
|
db:
|
||||||
image: postgres:16-alpine
|
image: postgres:16-alpine
|
||||||
ports:
|
ports:
|
||||||
@@ -35,20 +34,6 @@ services:
|
|||||||
timeout: 5s
|
timeout: 5s
|
||||||
retries: 5
|
retries: 5
|
||||||
|
|
||||||
|
|
||||||
s3:
|
|
||||||
image: docker.io/bitnami/minio:latest
|
|
||||||
ports:
|
|
||||||
- '9000:9000'
|
|
||||||
- '9001:9001'
|
|
||||||
volumes:
|
|
||||||
- minio_data:/data
|
|
||||||
environment:
|
|
||||||
- MINIO_ROOT_USER=${S3_ACCESS_KEY}
|
|
||||||
- MINIO_ROOT_PASSWORD=${S3_SECRET_KEY}
|
|
||||||
- MINIO_DEFAULT_BUCKETS=${S3_BUCKET_NAME}
|
|
||||||
|
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
|
portabase-data:
|
||||||
postgres-data:
|
postgres-data:
|
||||||
minio_data:
|
|
||||||
|
|||||||
@@ -1,73 +1,36 @@
|
|||||||
name: portabase-dev
|
name: portabase-dev
|
||||||
|
|
||||||
services:
|
services:
|
||||||
# app:
|
db:
|
||||||
# build:
|
image: postgres:17-alpine
|
||||||
# context: .
|
ports:
|
||||||
# dockerfile: docker/dockerfile/Dockerfile
|
- "5433:5432"
|
||||||
# target: dev
|
volumes:
|
||||||
# ports:
|
- postgres-data:/var/lib/postgresql/data
|
||||||
# - "8887:8887"
|
environment:
|
||||||
# environment:
|
- POSTGRES_DB=devdb
|
||||||
# - TIME_ZONE="Europe/Paris"
|
- POSTGRES_USER=devuser
|
||||||
# - NODE_ENV=development
|
- POSTGRES_PASSWORD=changeme
|
||||||
# depends_on:
|
healthcheck:
|
||||||
# db:
|
test: ["CMD-SHELL", "pg_isready -U devuser -d devdb"]
|
||||||
# condition: service_healthy
|
interval: 10s
|
||||||
# volumes:
|
timeout: 5s
|
||||||
# - .:/app
|
retries: 5
|
||||||
# - /app/node_modules
|
|
||||||
# container_name: portabase-app
|
|
||||||
#
|
|
||||||
|
|
||||||
db:
|
tusd:
|
||||||
image: postgres:16-alpine
|
image: tusproject/tusd:v2.8.0
|
||||||
ports:
|
ports:
|
||||||
- "5433:5432"
|
- "1080:8080"
|
||||||
volumes:
|
command: >
|
||||||
- postgres-data:/var/lib/postgresql/data
|
-upload-dir /data/uploads/tmp
|
||||||
environment:
|
-hooks-http http://localhost:8887/api/tus/hooks
|
||||||
- POSTGRES_DB=devdb
|
-max-size 21474836480
|
||||||
- POSTGRES_USER=devuser
|
-base-path /tus/files/
|
||||||
- POSTGRES_PASSWORD=changeme
|
extra_hosts:
|
||||||
healthcheck:
|
- "localhost:host-gateway"
|
||||||
test: ["CMD-SHELL", "pg_isready -U devuser -d devdb"]
|
volumes:
|
||||||
interval: 10s
|
- ./private/uploads/tmp:/data/uploads/tmp
|
||||||
timeout: 5s
|
user: "1000:1000"
|
||||||
retries: 5
|
|
||||||
|
|
||||||
# db-pgadmin:
|
|
||||||
# image: dpage/pgadmin4
|
|
||||||
# environment:
|
|
||||||
# PGADMIN_DEFAULT_EMAIL: "devuser@devuser.devuser"
|
|
||||||
# PGADMIN_DEFAULT_PASSWORD: "changeme"
|
|
||||||
# PGADMIN_CONFIG_SERVER_MODE: "False"
|
|
||||||
# POSTGRES_USER: "devuser"
|
|
||||||
# POSTGRES_PASSWORD: "changeme"
|
|
||||||
# volumes:
|
|
||||||
# - pgadmin-data:/var/lib/pgadmin
|
|
||||||
# ports:
|
|
||||||
# - "8080:80"
|
|
||||||
# restart: unless-stopped
|
|
||||||
# depends_on:
|
|
||||||
# - db
|
|
||||||
|
|
||||||
s3:
|
|
||||||
container_name: s3-portabase-dev
|
|
||||||
image: docker.io/bitnami/minio:latest
|
|
||||||
ports:
|
|
||||||
- "9000:9000"
|
|
||||||
- "9001:9001"
|
|
||||||
volumes:
|
|
||||||
- minio_data:/data
|
|
||||||
|
|
||||||
environment:
|
|
||||||
- MINIO_ROOT_USER=${S3_ACCESS_KEY}
|
|
||||||
- MINIO_ROOT_PASSWORD=${S3_SECRET_KEY}
|
|
||||||
- MINIO_DEFAULT_BUCKETS=${S3_BUCKET_NAME}
|
|
||||||
- MINIO_BROWSER=on
|
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
postgres-data:
|
postgres-data:
|
||||||
minio_data:
|
|
||||||
# pgadmin-data:
|
|
||||||
|
|||||||
@@ -1,114 +1,124 @@
|
|||||||
FROM node:22-alpine AS base
|
FROM --platform=$BUILDPLATFORM node:22-bullseye AS base
|
||||||
|
|
||||||
ENV YARN_VERSION=4.9.1
|
RUN apt-get update && apt-get install -y \
|
||||||
|
curl \
|
||||||
RUN apk add --update --no-cache \
|
ca-certificates \
|
||||||
build-base \
|
bash \
|
||||||
libc6-compat \
|
tzdata \
|
||||||
openssl \
|
libc6 \
|
||||||
make \
|
build-essential \
|
||||||
g++ \
|
g++ \
|
||||||
jpeg-dev \
|
make \
|
||||||
cairo-dev \
|
|
||||||
giflib-dev \
|
|
||||||
pango-dev \
|
|
||||||
libtool \
|
|
||||||
autoconf \
|
autoconf \
|
||||||
automake \
|
automake \
|
||||||
libpng
|
libtool \
|
||||||
|
git \
|
||||||
RUN corepack enable && corepack prepare yarn@${YARN_VERSION}
|
nginx \
|
||||||
RUN yarn set version ${YARN_VERSION}
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
|
||||||
FROM base AS deps
|
RUN mkdir -p /etc/apt/keyrings \
|
||||||
|
&& curl -fsSL https://www.postgresql.org/media/keys/ACCC4CF8.asc \
|
||||||
|
| gpg --dearmor -o /etc/apt/keyrings/postgresql.gpg \
|
||||||
|
&& echo "deb [signed-by=/etc/apt/keyrings/postgresql.gpg] http://apt.postgresql.org/pub/repos/apt bullseye-pgdg main" \
|
||||||
|
> /etc/apt/sources.list.d/pgdg.list
|
||||||
|
|
||||||
|
RUN apt-get update && apt-get install -y \
|
||||||
|
postgresql-18 \
|
||||||
|
postgresql-client-18 \
|
||||||
|
postgresql-contrib-18 \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
ENV PATH="/usr/lib/postgresql/18/bin:${PATH}"
|
||||||
|
|
||||||
|
|
||||||
|
FROM --platform=$BUILDPLATFORM golang:1.23-bookworm AS tusd-base
|
||||||
|
|
||||||
|
ARG TUSD_VERSION=2.8.0
|
||||||
|
|
||||||
|
RUN apt-get update && apt-get install -y --no-install-recommends git ca-certificates \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
WORKDIR /build
|
||||||
|
|
||||||
|
RUN git clone https://github.com/tus/tusd.git . \
|
||||||
|
&& git checkout v${TUSD_VERSION}
|
||||||
|
|
||||||
|
RUN CGO_ENABLED=0 GOOS=linux GOARCH=${TARGETARCH} \
|
||||||
|
go build -ldflags="-s -w" -o /tusddist/tusd ./cmd/tusd
|
||||||
|
|
||||||
|
FROM scratch AS tusd-dist
|
||||||
|
COPY --from=tusd-base /tusddist/tusd /tusd
|
||||||
|
|
||||||
|
|
||||||
|
FROM base AS build-env
|
||||||
|
|
||||||
|
RUN corepack enable && corepack prepare pnpm@latest --activate
|
||||||
|
|
||||||
|
FROM build-env AS deps
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
COPY .yarn ./.yarn
|
COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./
|
||||||
COPY .yarnrc.yml ./
|
RUN pnpm i --frozen-lockfile
|
||||||
COPY package.json yarn.lock* package-lock.json* pnpm-lock.yaml* ./
|
|
||||||
|
|
||||||
|
FROM build-env AS dev
|
||||||
RUN \
|
|
||||||
if [ -f yarn.lock ]; then yarn; \
|
|
||||||
elif [ -f package-lock.json ]; then npm ci; \
|
|
||||||
elif [ -f pnpm-lock.yaml ]; then corepack enable pnpm && pnpm i --frozen-lockfile; \
|
|
||||||
else echo "Lockfile not found." && exit 1; \
|
|
||||||
fi
|
|
||||||
|
|
||||||
|
|
||||||
FROM base AS dev
|
|
||||||
|
|
||||||
WORKDIR /app
|
|
||||||
|
|
||||||
COPY --from=deps /app/node_modules ./node_modules
|
COPY --from=deps /app/node_modules ./node_modules
|
||||||
|
COPY --from=tusd-base /usr/local/bin/tusd /usr/local/bin/tusd
|
||||||
|
WORKDIR /app
|
||||||
COPY . .
|
COPY . .
|
||||||
|
|
||||||
USER root
|
USER root
|
||||||
|
|
||||||
RUN chmod +x /app/docker/entrypoints/app-dev-entrypoint.sh
|
RUN chmod +x /app/docker/entrypoints/app-dev-entrypoint.sh
|
||||||
|
|
||||||
ENTRYPOINT ["sh","/app/docker/entrypoints/app-dev-entrypoint.sh"]
|
ENTRYPOINT ["sh","/app/docker/entrypoints/app-dev-entrypoint.sh"]
|
||||||
|
|
||||||
|
FROM build-env AS builder
|
||||||
# Rebuild the source code only when needed
|
|
||||||
FROM base AS builder
|
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
COPY --from=deps /app/node_modules ./node_modules
|
COPY --from=deps /app/node_modules ./node_modules
|
||||||
COPY . .
|
COPY . .
|
||||||
|
|
||||||
ENV NEXT_TELEMETRY_DISABLED=1
|
ENV NEXT_TELEMETRY_DISABLED=1
|
||||||
|
RUN pnpm run build
|
||||||
RUN \
|
|
||||||
if [ -f yarn.lock ]; then yarn run build; \
|
|
||||||
elif [ -f package-lock.json ]; then npm run build; \
|
|
||||||
elif [ -f pnpm-lock.yaml ]; then corepack enable pnpm && pnpm run build; \
|
|
||||||
else echo "Lockfile not found." && exit 1; \
|
|
||||||
fi
|
|
||||||
|
|
||||||
|
|
||||||
FROM base AS prod
|
FROM base AS prod
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
ENV NODE_ENV=production
|
ENV NODE_ENV=production
|
||||||
ENV NEXT_TELEMETRY_DISABLED=1
|
ENV NEXT_TELEMETRY_DISABLED=1
|
||||||
|
ENV PORT=80
|
||||||
|
ENV HOSTNAME="0.0.0.0"
|
||||||
|
ENV PGDATA=/data/postgres
|
||||||
|
ENV PRIVATE_PATH=/data/private
|
||||||
|
|
||||||
|
|
||||||
RUN addgroup --system --gid 1001 nodejs
|
RUN addgroup --system --gid 1001 nodejs
|
||||||
RUN adduser --system --uid 1001 nextjs
|
RUN adduser --system --uid 1001 nextjs
|
||||||
|
|
||||||
COPY --from=builder /app/public ./public
|
COPY --from=builder /app/public ./public
|
||||||
|
|
||||||
RUN mkdir .next
|
|
||||||
RUN chown nextjs:nodejs .next
|
|
||||||
RUN mkdir -p /app/private/uploads
|
|
||||||
RUN chown -R nextjs:nodejs /app/private
|
|
||||||
RUN chown -R nextjs:nodejs /app/public
|
|
||||||
|
|
||||||
|
|
||||||
COPY --from=builder /app/next.config.ts ./
|
COPY --from=builder /app/next.config.ts ./
|
||||||
COPY --from=builder /app/portabase.config.ts ./
|
COPY --from=builder /app/portabase.config.ts ./
|
||||||
COPY --from=builder /app/drizzle.config.ts ./
|
COPY --from=builder /app/drizzle.config.ts ./
|
||||||
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
|
COPY --from=builder --chown=1001:1001 /app/.next/standalone ./
|
||||||
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
|
COPY --from=builder --chown=1001:1001 /app/.next/static ./.next/static
|
||||||
COPY --chown=nextjs:nodejs src/db ./src/db
|
COPY --chown=1001:1001 src/db ./src/db
|
||||||
|
COPY --from=deps /app/node_modules ./node_modules
|
||||||
|
|
||||||
|
COPY --from=tusd-dist /tusd /usr/local/bin/tusd
|
||||||
|
RUN chmod +x /usr/local/bin/tusd
|
||||||
|
|
||||||
|
COPY docker/nginx/nginx.conf /etc/nginx/nginx.conf
|
||||||
|
|
||||||
|
|
||||||
|
RUN mkdir -p .next /data/private/uploads \
|
||||||
|
&& chown -R nextjs:nodejs .next /data/private /app/public
|
||||||
|
|
||||||
|
|
||||||
USER root
|
USER root
|
||||||
|
|
||||||
COPY ./docker/entrypoints/app-prod-entrypoint.sh /app/app-prod-entrypoint.sh
|
COPY ./docker/entrypoints/app-prod-entrypoint.sh /app/app-prod-entrypoint.sh
|
||||||
RUN chmod +x /app/app-prod-entrypoint.sh
|
RUN chmod +x /app/app-prod-entrypoint.sh
|
||||||
|
|
||||||
EXPOSE 80
|
EXPOSE 80
|
||||||
|
|
||||||
ENV PORT=80
|
|
||||||
|
|
||||||
ENV HOSTNAME="0.0.0.0"
|
|
||||||
|
|
||||||
USER nextjs
|
|
||||||
|
|
||||||
ENTRYPOINT ["sh","/app/app-prod-entrypoint.sh"]
|
ENTRYPOINT ["sh","/app/app-prod-entrypoint.sh"]
|
||||||
|
|
||||||
|
|||||||
@@ -1,18 +1,12 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
#
|
|
||||||
#npx drizzle-kit generate
|
|
||||||
#npx drizzle-kit migrate
|
|
||||||
#
|
|
||||||
#npm run dev
|
|
||||||
#
|
|
||||||
#exec "$@"
|
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
echo "▶ Running Drizzle codegen..."
|
echo "▶ Running Drizzle codegen..."
|
||||||
npx drizzle-kit generate
|
pnpm drizzle-kit generate
|
||||||
|
|
||||||
echo "▶ Applying migrations..."
|
echo "▶ Applying migrations..."
|
||||||
npx drizzle-kit migrate
|
pnpm drizzle-kit migrate
|
||||||
|
|
||||||
echo "▶ Starting Next.js dev server..."
|
echo "▶ Starting Next.js dev server..."
|
||||||
exec npm run dev
|
exec pnpm dev
|
||||||
@@ -1,14 +1,83 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
|
||||||
#echo " ____ __ __ "
|
if [ -n "$TZ" ]; then
|
||||||
#echo " / __ \____ _____/ /_____ _/ /_ ____ _________ "
|
echo "[INFO] Application timezone set to $TZ (environment only)"
|
||||||
#echo " / /_/ / __ \/ ___/ __/ __ / __ \/ __ / ___/ _ \ "
|
export TZ="$TZ"
|
||||||
#echo " / ____/ /_/ / / / /_/ /_/ / /_/ / /_/ (__ ) __/ "
|
else
|
||||||
#echo " /_/ \____/_/ \__/\__,_/_.___/\__,_/____/\___/ "
|
echo "[WARN] No TZ provided, using default container timezone"
|
||||||
#echo " "
|
fi
|
||||||
#echo " Community Edition v1.1.1 "
|
|
||||||
#echo " "
|
POSTGRES_BIN=$(ls -d /usr/lib/postgresql/*/bin | head -n 1)
|
||||||
|
|
||||||
|
if [ -z "$POSTGRES_BIN" ]; then
|
||||||
|
echo "PostgreSQL binaries not found"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
export PATH="$POSTGRES_BIN:$PATH"
|
||||||
|
|
||||||
|
if [ -z "$DATABASE_URL" ]; then
|
||||||
|
echo "[INFO] No DATABASE_URL provided, starting internal Postgres..."
|
||||||
|
|
||||||
|
mkdir -p "$PGDATA"
|
||||||
|
chown -R postgres:postgres "$PGDATA"
|
||||||
|
|
||||||
|
if [ ! -f "$PGDATA/PG_VERSION" ]; then
|
||||||
|
echo "[INFO] Initializing database cluster..."
|
||||||
|
if ! su postgres -c "initdb -D '$PGDATA'" > /dev/null 2>&1; then
|
||||||
|
echo "[ERROR] initdb failed"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! su postgres -c "pg_ctl -D '$PGDATA' \
|
||||||
|
-o \"-c listen_addresses='localhost' -c logging_collector=on\" \
|
||||||
|
-l $PGDATA/postgres.log -w start" > /dev/null 2>&1; then
|
||||||
|
echo "[ERROR] PostgreSQL failed to start"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
until su postgres -c "pg_isready -h 127.0.0.1 -p 5432" > /dev/null 2>&1; do
|
||||||
|
sleep 1
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "[INFO] PostgreSQL server is up and accepting connections"
|
||||||
|
|
||||||
|
DB_USER="${POSTGRES_USER:-portabase_user}"
|
||||||
|
DB_PASS="${POSTGRES_PASSWORD:-JaB6b1SUtIWYvt7srnOt}"
|
||||||
|
DB_NAME="${POSTGRES_DB:-portabase_db}"
|
||||||
|
|
||||||
|
USER_EXISTS=$(su postgres -c "psql -tAc \"SELECT 1 FROM pg_roles WHERE rolname='$DB_USER'\"" 2>/dev/null)
|
||||||
|
if [ "$USER_EXISTS" != "1" ]; then
|
||||||
|
if ! su postgres -c "psql -c \"CREATE USER $DB_USER WITH PASSWORD '$DB_PASS';\"" > /dev/null 2>&1; then
|
||||||
|
echo "[ERROR] Failed creating user"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
DB_EXISTS=$(su postgres -c "psql -tAc \"SELECT 1 FROM pg_database WHERE datname='$DB_NAME'\"" 2>/dev/null)
|
||||||
|
if [ "$DB_EXISTS" != "1" ]; then
|
||||||
|
if ! su postgres -c "psql -c \"CREATE DATABASE $DB_NAME OWNER $DB_USER;\"" > /dev/null 2>&1; then
|
||||||
|
echo "[ERROR] Failed creating database"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
export DATABASE_URL="postgres://$DB_USER:$DB_PASS@127.0.0.1:5432/$DB_NAME"
|
||||||
|
|
||||||
|
echo "[SUCCESS] Internal PostgreSQL started successfully"
|
||||||
|
echo "[SUCCESS] Database: $DB_NAME | User: $DB_USER | Host: 127.0.0.1:5432"
|
||||||
|
fi
|
||||||
|
|
||||||
|
|
||||||
|
mkdir -p /data/private/uploads/tmp
|
||||||
|
echo "▶ Starting tusd server..."
|
||||||
|
tusd --base-path /tus/files/ --upload-dir /data/private/uploads/tmp --hooks-http http://127.0.0.1:3000/api/tus/hooks --port 1080 --max-size 21474836480 &
|
||||||
|
|
||||||
|
echo "▶ Starting Next.js server..."
|
||||||
|
PORT=3000 node server.js &
|
||||||
|
|
||||||
|
echo "▶ Starting nginx..."
|
||||||
|
exec nginx -g "daemon off;"
|
||||||
|
|
||||||
node server.js
|
|
||||||
|
|
||||||
exec "$@"
|
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
events {}
|
||||||
|
|
||||||
|
http {
|
||||||
|
client_max_body_size 20G;
|
||||||
|
ignore_invalid_headers off;
|
||||||
|
|
||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
|
||||||
|
location /tus/ {
|
||||||
|
proxy_pass http://127.0.0.1:1080/tus/;
|
||||||
|
|
||||||
|
proxy_pass_request_headers on;
|
||||||
|
|
||||||
|
proxy_request_buffering off;
|
||||||
|
proxy_buffering off;
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
|
||||||
|
proxy_set_header Host $http_host;
|
||||||
|
proxy_set_header X-Forwarded-Host $http_host;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
|
||||||
|
proxy_set_header Upgrade $http_upgrade;
|
||||||
|
proxy_set_header Connection "upgrade";
|
||||||
|
}
|
||||||
|
|
||||||
|
location / {
|
||||||
|
proxy_pass http://127.0.0.1:3000;
|
||||||
|
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
proxy_set_header Upgrade $http_upgrade;
|
||||||
|
proxy_set_header Connection "upgrade";
|
||||||
|
|
||||||
|
proxy_set_header Host $http_host;
|
||||||
|
proxy_set_header X-Forwarded-Host $http_host;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
import { Button as ReactEmailButton } from "@react-email/components";
|
|
||||||
import { ComponentPropsWithoutRef } from "react";
|
|
||||||
|
|
||||||
export const Button = (props: ComponentPropsWithoutRef<typeof ReactEmailButton>) => {
|
|
||||||
return <ReactEmailButton className="block w-52 rounded bg-blue-600 py-3.5 text-center text-sm font-normal text-white no-underline " {...props} />;
|
|
||||||
};
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
import { Body, Container, Head, Html, Img, Preview, Section, Tailwind } from "@react-email/components";
|
|
||||||
import * as React from "react";
|
|
||||||
import { PropsWithChildren } from "react";
|
|
||||||
import { getServerUrl } from "@/utils/get-server-url";
|
|
||||||
|
|
||||||
const baseUrl = getServerUrl();
|
|
||||||
|
|
||||||
export const EmailLayout = ({ children, preview }: PropsWithChildren<{ preview?: string }>) => {
|
|
||||||
return (
|
|
||||||
<Tailwind>
|
|
||||||
<Html>
|
|
||||||
<Head />
|
|
||||||
{preview ? <Preview>{preview}</Preview> : <Preview>Please check your mails</Preview>}
|
|
||||||
<Body className="bg-gray-100 py-4" style={{ fontFamily: "Arial, sans-serif" }}>
|
|
||||||
<Container className="bg-white border border-gray-200 p-12">
|
|
||||||
<Img src={`${baseUrl}/logo-title-black.png`} width="200" height="auto" alt="Logo" />
|
|
||||||
<Section>{children}</Section>
|
|
||||||
</Container>
|
|
||||||
</Body>
|
|
||||||
</Html>
|
|
||||||
</Tailwind>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default EmailLayout;
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
import { Button } from "@react-email/button";
|
|
||||||
import { Html } from "@react-email/html";
|
|
||||||
import * as React from "react";
|
|
||||||
|
|
||||||
export default function HelloEmail() {
|
|
||||||
return (
|
|
||||||
<Html>
|
|
||||||
<Button href="https://example.com" style={{ background: "#000", color: "#fff" }}>
|
|
||||||
Hello,
|
|
||||||
</Button>
|
|
||||||
</Html>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
import { Text } from "@react-email/components";
|
|
||||||
import * as React from "react";
|
|
||||||
import EmailLayout from "./EmailLayout";
|
|
||||||
import { Button } from "@react-email/button";
|
|
||||||
|
|
||||||
export const TestEmailSettings = () => {
|
|
||||||
return (
|
|
||||||
<EmailLayout preview="Email Setup">
|
|
||||||
<Text className="text-base font-light leading-8 text-green-800 ">Hi, your email settings are setup !</Text>
|
|
||||||
<Text className="text-base font-light leading-8 text-green-800 ">Best regard,</Text>{" "}
|
|
||||||
<Text className="text-base font-light leading-8 text-green-800 ">Portabase</Text>
|
|
||||||
</EmailLayout>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default TestEmailSettings;
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
import { Text as ReactEmailText } from "@react-email/components";
|
|
||||||
import { ComponentPropsWithoutRef } from "react";
|
|
||||||
|
|
||||||
export const Text = (props: ComponentPropsWithoutRef<typeof ReactEmailText>) => {
|
|
||||||
return <ReactEmailText className="text-base font-light leading-8 text-green-800 " {...props} />;
|
|
||||||
};
|
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
import {defineConfig, globalIgnores} from 'eslint/config'
|
||||||
|
import nextVitals from 'eslint-config-next/core-web-vitals'
|
||||||
|
|
||||||
|
const eslintConfig = defineConfig([
|
||||||
|
...nextVitals,
|
||||||
|
globalIgnores([
|
||||||
|
'.next/**',
|
||||||
|
'out/**',
|
||||||
|
'build/**',
|
||||||
|
'next-env.d.ts',
|
||||||
|
]),
|
||||||
|
])
|
||||||
|
|
||||||
|
export default eslintConfig
|
||||||
@@ -1,86 +0,0 @@
|
|||||||
import { NextRequest, NextResponse } from "next/server";
|
|
||||||
import { loggingMiddleware } from "@/middleware/loggingMiddleware";
|
|
||||||
import { errorHandler } from "@/middleware/errorHandler";
|
|
||||||
import { auth } from "@/lib/auth/auth";
|
|
||||||
import { headers } from "next/headers";
|
|
||||||
import { signOut } from "@/lib/auth/auth-client";
|
|
||||||
|
|
||||||
export async function middleware(request: NextRequest) {
|
|
||||||
const url = request.nextUrl.clone();
|
|
||||||
const redirectUrl = encodeURIComponent(request.nextUrl.pathname)
|
|
||||||
|
|
||||||
if (url.pathname.startsWith("/dashboard")) {
|
|
||||||
const session = await auth.api.getSession({
|
|
||||||
headers: await headers(),
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!session) {
|
|
||||||
return NextResponse.redirect(new URL(`/login?redirect=${redirectUrl}`, request.url));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (session.user.banned) {
|
|
||||||
signOut();
|
|
||||||
return NextResponse.redirect(new URL("/login?error=banned", request.url));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (session.user.role === "pending") {
|
|
||||||
signOut();
|
|
||||||
return NextResponse.redirect(new URL(`/login?error=pending?redirect=${redirectUrl}`, request.url));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (url.pathname === "/dashboard") {
|
|
||||||
return NextResponse.redirect(new URL(`/dashboard/home`, request.url));
|
|
||||||
}
|
|
||||||
|
|
||||||
return NextResponse.next();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Exclude `/api/auth` and its subpaths
|
|
||||||
if (url.pathname.startsWith("/api/auth")) {
|
|
||||||
return NextResponse.next();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (url.pathname.startsWith("/api")) {
|
|
||||||
const routeExists = checkRouteExists(url.pathname);
|
|
||||||
// If the route does not exist, return a 404 JSON response
|
|
||||||
if (!routeExists) {
|
|
||||||
return new NextResponse(JSON.stringify({ message: "This API route does not exist.", status: 404 }), {
|
|
||||||
status: 404,
|
|
||||||
headers: { "Content-Type": "application/json" },
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
loggingMiddleware(request);
|
|
||||||
} catch (err) {
|
|
||||||
errorHandler(err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Function to check if the route exists (supports dynamic routes)
|
|
||||||
function checkRouteExists(pathname: string) {
|
|
||||||
// Define static and dynamic routes with patterns
|
|
||||||
const routePatterns = [
|
|
||||||
//do not delete
|
|
||||||
// /^\/api\/auth\/\d+$/, // Dynamic route with a number as a parameter (e.g., /api/dynamic/123)
|
|
||||||
// /^\/api\/auth\/\w+$/, // Dynamic route with a number as a parameter (e.g., /api/dynamic/123)
|
|
||||||
// /^\/api\/agent\/healthcheck\/\w+$/, // Dynamic route with an alphanumeric parameter (e.g., /api/user/username)
|
|
||||||
/^\/api\/agent\/[^/]+\/status\/?$/, // Dynamic route for /api/agent/[id]/status
|
|
||||||
/^\/api\/agent\/[^/]+\/backup\/?$/,
|
|
||||||
/^\/api\/agent\/[^/]+\/restore\/?$/,
|
|
||||||
/^\/api\/files\/[^/]+\/?$/,
|
|
||||||
/^\/api\/images\/[^/]+\/?$/,
|
|
||||||
/^\/api\/events\/?$/,
|
|
||||||
/^\/api\/init\/?$/,
|
|
||||||
];
|
|
||||||
return routePatterns.some((pattern) => pattern.test(pathname));
|
|
||||||
}
|
|
||||||
|
|
||||||
export const config = {
|
|
||||||
runtime: "nodejs",
|
|
||||||
matcher: [
|
|
||||||
// '/api/agent/:path*',
|
|
||||||
"/api/:path*",
|
|
||||||
"/dashboard/:path*",
|
|
||||||
"/dashboard",
|
|
||||||
],
|
|
||||||
};
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
/// <reference types="next" />
|
/// <reference types="next" />
|
||||||
/// <reference types="next/image-types/global" />
|
/// <reference types="next/image-types/global" />
|
||||||
/// <reference path="./.next/types/routes.d.ts" />
|
import "./.next/dev/types/routes.d.ts";
|
||||||
|
|
||||||
// NOTE: This file should not be edited
|
// NOTE: This file should not be edited
|
||||||
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
|
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import type {NextConfig} from "next";
|
import type {NextConfig} from "next";
|
||||||
import {PORTABASE_DEFAULT_SETTINGS} from "./portabase.config";
|
import {PORTABASE_DEFAULT_SETTINGS} from "./portabase.config";
|
||||||
|
|
||||||
|
const isDev = process.env.NODE_ENV === "development";
|
||||||
|
|
||||||
|
|
||||||
function buildCSPHeader(): string {
|
function buildCSPHeader(): string {
|
||||||
const {CSP} = PORTABASE_DEFAULT_SETTINGS.SECURITY;
|
const {CSP} = PORTABASE_DEFAULT_SETTINGS.SECURITY;
|
||||||
@@ -41,9 +43,23 @@ const nextConfig: NextConfig = {
|
|||||||
typescript: {
|
typescript: {
|
||||||
ignoreBuildErrors: true,
|
ignoreBuildErrors: true,
|
||||||
},
|
},
|
||||||
eslint: {
|
experimental: {
|
||||||
ignoreDuringBuilds: true,
|
serverActions: {
|
||||||
|
bodySizeLimit: "10gb",
|
||||||
|
},
|
||||||
|
proxyClientMaxBodySize: '10gb',
|
||||||
},
|
},
|
||||||
|
async rewrites() {
|
||||||
|
if (!isDev) return [];
|
||||||
|
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
source: "/tus/:path*",
|
||||||
|
destination: "http://localhost:1080/tus/:path*",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
},
|
||||||
|
|
||||||
async headers() {
|
async headers() {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
|
|||||||