mirror of
https://github.com/Portabase/portabase.git
synced 2026-07-14 11:16:13 +02:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
31cbd52703 | ||
|
|
dd85f7a25d | ||
|
|
203c25aabe | ||
|
|
41b1c89283 | ||
|
|
180ea21041 | ||
|
|
e1c0fc6917 | ||
|
|
057c192314 | ||
|
|
aff782aa28 | ||
|
|
2e729cb79c | ||
|
|
d8677d74a7 | ||
|
|
2f77714994 | ||
|
|
bc8050c4df | ||
|
|
a9c69b112d | ||
|
|
04785c323b | ||
|
|
1d209ceb28 | ||
|
|
941a9256f0 | ||
|
|
9098012426 | ||
|
|
8e12ff59a4 | ||
|
|
0f8a267861 | ||
|
|
da53a00fd8 | ||
|
|
47e3a93e42 | ||
|
|
0262a1c176 | ||
|
|
7aecd911c0 | ||
|
|
595ccadfb3 | ||
|
|
bded273f94 | ||
|
|
c095d393cf | ||
|
|
c3ae82fde7 | ||
|
|
a8e11ebef7 | ||
|
|
61b1a2e724 | ||
|
|
7a4af4e1e0 | ||
|
|
a9d6481193 | ||
|
|
fb97963cc3 | ||
|
|
0cc0ba989c | ||
|
|
26eff02943 | ||
|
|
67246efafc | ||
|
|
260b3ba1f9 | ||
|
|
90f8885e73 | ||
|
|
0bacd6a65c | ||
|
|
2846deb4d0 | ||
|
|
ae6dbcab25 | ||
|
|
d823ec2e82 | ||
|
|
b0ce5273ce | ||
|
|
b576862024 | ||
|
|
dc2496ee18 | ||
|
|
87db207833 | ||
|
|
b1ccf6ec5f | ||
|
|
ca3f63af81 | ||
|
|
29b0ca5bf5 | ||
|
|
da50193e8c | ||
|
|
1829c1fc1c | ||
|
|
79a17407bd | ||
|
|
dd1e3283d5 | ||
|
|
d1baedd092 | ||
|
|
9e504cefc1 | ||
|
|
3cc5ab70fa | ||
|
|
0441af78a1 | ||
|
|
3c615a4fd7 | ||
|
|
8681e16fde | ||
|
|
918c6c448c | ||
|
|
1e2b4f16b3 | ||
|
|
f0d02eb8b3 | ||
|
|
96274206c5 | ||
|
|
7ae7ea6f79 | ||
|
|
487e7ad83d | ||
|
|
c9dabd39fe | ||
|
|
30ab689c6e | ||
|
|
8143af18af | ||
|
|
471c65cc48 | ||
|
|
9162d5afcc | ||
|
|
9236c664c2 | ||
|
|
767ed69020 | ||
|
|
93c706719f | ||
|
|
ed1ac1926b | ||
|
|
3adb6d01c0 | ||
|
|
9c195c1f30 | ||
|
|
e27093404b | ||
|
|
9602427ca0 | ||
|
|
46812cb5ff | ||
|
|
6258d92464 | ||
|
|
c57d913c46 | ||
|
|
5d285b7ac6 | ||
|
|
c96219f851 | ||
|
|
c7d949b1d1 | ||
|
|
6593a8e324 | ||
|
|
0988fa0115 | ||
|
|
9f0b1a172c | ||
|
|
0126a829dd | ||
|
|
b04a2750a3 | ||
|
|
9e22160b16 | ||
|
|
e8e4b093d2 | ||
|
|
0cb27f2637 | ||
|
|
1497c7385b | ||
|
|
327b06d2a1 | ||
|
|
2c2cfc0b73 | ||
|
|
93327b0a6a | ||
|
|
11c74d4117 | ||
|
|
f93bb6e33e | ||
|
|
73699bbda9 | ||
|
|
5f24ec2d86 | ||
|
|
c08bb8f8e0 | ||
|
|
d01e705866 | ||
|
|
ecc80c3c01 | ||
|
|
784a907384 | ||
|
|
b68a5f32ca | ||
|
|
467f9e2ffd | ||
|
|
1d25acde61 | ||
|
|
cb6ac86bb0 | ||
|
|
cd2da4441d | ||
|
|
d7515b172c | ||
|
|
e4de903cc9 | ||
|
|
45166a3b9c | ||
|
|
687b59e397 | ||
|
|
9a3be8d5a5 | ||
|
|
0c23c24651 | ||
|
|
e20e7f016c | ||
|
|
a0c1a24ba9 | ||
|
|
0475b3d01a | ||
|
|
39cb9b99f6 | ||
|
|
efd123ddfe | ||
|
|
413318886c | ||
|
|
187f9d2386 | ||
|
|
5f61c7baa6 | ||
|
|
e3c5ec9a58 | ||
|
|
fde52415cb | ||
|
|
d92225d093 | ||
|
|
e2a0932229 | ||
|
|
6acdf73557 | ||
|
|
4405b61e03 | ||
|
|
fba714d679 | ||
|
|
8e15282405 | ||
|
|
72b5816451 | ||
|
|
4016191497 | ||
|
|
94b36c1622 | ||
|
|
aa736b8b4a | ||
|
|
94cd937c56 | ||
|
|
9ccf3b7242 | ||
|
|
01b7a58c75 | ||
|
|
2b6c661fea | ||
|
|
fbd61cdf2f | ||
|
|
e9534181da | ||
|
|
fdc613fe8e | ||
|
|
fca09a84cb | ||
|
|
6ec6fb1f11 | ||
|
|
3d51d3548f | ||
|
|
8479b6c822 | ||
|
|
21bb165103 | ||
|
|
c7ca01c0fb | ||
|
|
5d2172f2bf | ||
|
|
1decd9d7a5 | ||
|
|
d0091cfed3 | ||
|
|
0ebcf1729c | ||
|
|
51d101e8f0 | ||
|
|
2d9bf225a0 | ||
|
|
f85d12448c | ||
|
|
82c8ad98b4 | ||
|
|
2499c7c426 | ||
|
|
da530217ab | ||
|
|
4c70d8aac7 | ||
|
|
ba1dbcc9f3 | ||
|
|
0ad380cc81 | ||
|
|
b77a2e374e | ||
|
|
647108e8ee | ||
|
|
8e4e41ca07 | ||
|
|
a8eb3f5f1a | ||
|
|
eb5ac5c8ea | ||
|
|
29344e0c48 | ||
|
|
73f7ac057c | ||
|
|
d359d53fb7 | ||
|
|
ff83ee9c62 | ||
|
|
da3a8d7fbe | ||
|
|
ba2fc55188 | ||
|
|
a6896c9b50 | ||
|
|
f2563c5e8d | ||
|
|
9326291780 | ||
|
|
8dd74f310d | ||
|
|
44ed9e9ca6 | ||
|
|
ab22680625 | ||
|
|
a73e29efc9 | ||
|
|
26273b7240 | ||
|
|
c925407f05 | ||
|
|
622fc369d0 | ||
|
|
0c4b7573cc | ||
|
|
401ce5c290 | ||
|
|
9580ad3934 | ||
|
|
b33c0810d5 | ||
|
|
e3e84e0d18 | ||
|
|
55cb4a7144 | ||
|
|
8fbe639aa1 | ||
|
|
9bc042e2cd | ||
|
|
67302c6a17 | ||
|
|
d7e2ec3b08 | ||
|
|
fdaca0e8a7 | ||
|
|
3af5f89606 | ||
|
|
3ccccfc5e5 | ||
|
|
7959f903bb | ||
|
|
59eb91f28f | ||
|
|
9aecfb22a5 | ||
|
|
ba4476a3a7 | ||
|
|
64724d2a92 | ||
|
|
be58ebdb7d | ||
|
|
746a9f5eb6 | ||
|
|
91a6d672fa | ||
|
|
50809476fa | ||
|
|
483f9cf192 | ||
|
|
f1c7dcda45 | ||
|
|
282ffb543c | ||
|
|
c840196a14 | ||
|
|
7d76037a43 | ||
|
|
68fea480ae | ||
|
|
b063e5c276 | ||
|
|
f2d98505f1 | ||
|
|
ec64ea8229 | ||
|
|
59dc181625 | ||
|
|
79387b83f6 | ||
|
|
b13433e554 | ||
|
|
d9fd382cb2 | ||
|
|
baa5e300c8 | ||
|
|
51747773ba | ||
|
|
4d324d885d | ||
|
|
af1a437523 | ||
|
|
dd8663591e | ||
|
|
de4f31f255 | ||
|
|
53c1bc9e59 | ||
|
|
77901ae1bc | ||
|
|
1aeff542e5 | ||
|
|
c05469d704 | ||
|
|
1e6bb542f9 | ||
|
|
b81ad079ee | ||
|
|
044edcda07 | ||
|
|
7eb85d2a99 | ||
|
|
7d4d6cd9df | ||
|
|
3f7eb4528e | ||
|
|
0008f70552 | ||
|
|
ff48517b43 | ||
|
|
2695715180 | ||
|
|
b3be1a561c | ||
|
|
cab7a7f8bb | ||
|
|
f6da5961c3 | ||
|
|
d297f1a27b | ||
|
|
b513509b01 | ||
|
|
bfa3f7e04e | ||
|
|
539b516e45 | ||
|
|
25744efc66 | ||
|
|
21e37366fb | ||
|
|
6002ad44e1 | ||
|
|
a431270718 | ||
|
|
3f6a47c39d | ||
|
|
4dace0a32b | ||
|
|
efb5318fc5 | ||
|
|
a5e83f3133 | ||
|
|
7b6ca12744 | ||
|
|
da1b5a969c | ||
|
|
5d790231ea | ||
|
|
ad363ce38c | ||
|
|
8e5d6eec6c | ||
|
|
92f1400a35 | ||
|
|
47f1938136 | ||
|
|
13d3cfb290 | ||
|
|
c657bdfb63 | ||
|
|
f226218b3a | ||
|
|
8f7fa0dea1 | ||
|
|
292b3c805d | ||
|
|
ded26f92af | ||
|
|
8cce7bbaf4 | ||
|
|
228b8c3ec7 | ||
|
|
2e298ce757 | ||
|
|
81daa9f0b0 | ||
|
|
713c8d9177 | ||
|
|
f571162eed | ||
|
|
19d2872943 | ||
|
|
0752b8bf7f | ||
|
|
c01097196a | ||
|
|
5f1bee018d | ||
|
|
8655b81618 | ||
|
|
1702e43373 | ||
|
|
a7d411f6ff | ||
|
|
6a84da0f96 | ||
|
|
79425f4490 | ||
|
|
2fcddea0fa | ||
|
|
ac1c8552e1 | ||
|
|
6a18eefe34 | ||
|
|
a43eedaf9f | ||
|
|
77bc24990e | ||
|
|
edbc79c89e | ||
|
|
527a090f1b | ||
|
|
370b86337f | ||
|
|
1ce3acdde0 | ||
|
|
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 | ||
|
|
3bf731055f |
+40
-8
@@ -1,12 +1,11 @@
|
|||||||
# Environnement
|
# Environment
|
||||||
NODE_ENV=development
|
NODE_ENV=development
|
||||||
|
|
||||||
# Base de données
|
# Database
|
||||||
DATABASE_URL=postgresql://devuser:changeme@localhost:5433/devdb?schema=public
|
DATABASE_URL=postgresql://devuser:changeme@localhost:5433/devdb?schema=public
|
||||||
|
|
||||||
# Projet
|
# Project
|
||||||
PROJECT_NAME="Portabase"
|
PROJECT_NAME="Portabase"
|
||||||
PROJECT_DESCRIPTION="Portabase is a powerful database manager"
|
|
||||||
PROJECT_URL=http://localhost:8887
|
PROJECT_URL=http://localhost:8887
|
||||||
PROJECT_SECRET=
|
PROJECT_SECRET=
|
||||||
|
|
||||||
@@ -21,11 +20,44 @@ SMTP_FROM=
|
|||||||
#Port 465 -> secure: true
|
#Port 465 -> secure: true
|
||||||
#By default : true
|
#By default : true
|
||||||
|
|
||||||
# Google
|
# OIDC SSO Authentification
|
||||||
AUTH_GOOGLE_ID=
|
AUTH_OIDC_ID=""
|
||||||
AUTH_GOOGLE_SECRET=
|
AUTH_OIDC_TITLE=""
|
||||||
AUTH_GOOGLE_METHOD=
|
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
|
||||||
RETENTION_CRON="* * * * *"
|
RETENTION_CRON="* * * * *"
|
||||||
|
|
||||||
|
TRUSTED_DOMAINS="http://localhost:8887, http://localhost:3055, http://localhost:3056"
|
||||||
|
|
||||||
|
|||||||
@@ -60,7 +60,7 @@ representative at an online or offline event.
|
|||||||
|
|
||||||
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
||||||
reported to the community leaders responsible for enforcement at
|
reported to the community leaders responsible for enforcement at
|
||||||
contact@soluce-technologies.com.
|
contact@portabase.io.
|
||||||
All complaints will be reviewed and investigated promptly and fairly.
|
All complaints will be reviewed and investigated promptly and fairly.
|
||||||
|
|
||||||
All community leaders are obligated to respect the privacy and security of the
|
All community leaders are obligated to respect the privacy and security of the
|
||||||
|
|||||||
@@ -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! 🙌
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|||||||
+1
-1
@@ -18,7 +18,7 @@ We take security seriously and aim to support the following versions of the proj
|
|||||||
If you discover a security vulnerability in this project, we appreciate your help in disclosing it responsibly.
|
If you discover a security vulnerability in this project, we appreciate your help in disclosing it responsibly.
|
||||||
|
|
||||||
1. **Contact Us**
|
1. **Contact Us**
|
||||||
Please report the vulnerability by emailing **[contact@soluce-technologies.com](mailto:contact@soluce-technologies.com)**. Include the following details:
|
Please report the vulnerability by emailing **[contact@portabase.io](mailto:contact@portabase.io)**. Include the following details:
|
||||||
- A detailed description of the issue.
|
- A detailed description of the issue.
|
||||||
- Steps to reproduce the vulnerability (if applicable).
|
- Steps to reproduce the vulnerability (if applicable).
|
||||||
- Any potential impacts or risks.
|
- Any potential impacts or risks.
|
||||||
|
|||||||
@@ -0,0 +1,73 @@
|
|||||||
|
name: Discord Notification
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_call:
|
||||||
|
inputs:
|
||||||
|
release_tag:
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
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 "${{ inputs.release_tag }}" -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="${{ inputs.release_tag }}"; 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"
|
||||||
@@ -3,10 +3,16 @@ name: Docker Publish
|
|||||||
on:
|
on:
|
||||||
workflow_call:
|
workflow_call:
|
||||||
inputs:
|
inputs:
|
||||||
|
version:
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
ref:
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
image_name:
|
image_name:
|
||||||
required: false
|
required: false
|
||||||
type: string
|
type: string
|
||||||
default: 'portabase/portabase'
|
default: "portabase/portabase"
|
||||||
add_latest:
|
add_latest:
|
||||||
required: false
|
required: false
|
||||||
type: boolean
|
type: boolean
|
||||||
@@ -14,11 +20,11 @@ on:
|
|||||||
target:
|
target:
|
||||||
required: false
|
required: false
|
||||||
type: string
|
type: string
|
||||||
default: 'prod'
|
default: "prod"
|
||||||
dockerfile:
|
dockerfile:
|
||||||
required: false
|
required: false
|
||||||
type: string
|
type: string
|
||||||
default: './docker/dockerfile/Dockerfile'
|
default: "./docker/dockerfile/Dockerfile"
|
||||||
secrets:
|
secrets:
|
||||||
DOCKER_USERNAME:
|
DOCKER_USERNAME:
|
||||||
required: true
|
required: true
|
||||||
@@ -26,10 +32,9 @@ on:
|
|||||||
required: true
|
required: true
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
|
|
||||||
build:
|
build:
|
||||||
name: Build and push Docker images
|
name: Build architectures
|
||||||
runs-on: ${{ matrix.platform == 'linux/amd64' && 'ubuntu-latest' || matrix.platform == 'linux/arm64' && 'ubuntu-24.04-arm' }}
|
runs-on: ${{ matrix.platform == 'linux/amd64' && 'ubuntu-latest' || 'ubuntu-24.04-arm' }}
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
@@ -37,23 +42,26 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
ref: ${{ inputs.ref }}
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
- name: Login to Docker
|
- name: Login to Docker
|
||||||
uses: docker/login-action@f4ef78c080cd8ba55a85445d5b36e214a81df20a
|
uses: docker/login-action@v3
|
||||||
with:
|
with:
|
||||||
username: ${{ secrets.DOCKER_USERNAME }}
|
username: ${{ secrets.DOCKER_USERNAME }}
|
||||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||||
|
|
||||||
- name: Set image tag
|
- name: Prepare Image Tags
|
||||||
id: set-tags
|
id: prep
|
||||||
run: |
|
run: |
|
||||||
REF_NAME=${GITHUB_REF#refs/tags/}
|
ARCH=${{ matrix.platform == 'linux/amd64' && 'amd64' || 'arm64' }}
|
||||||
if [ "${{ matrix.platform }}" = "linux/amd64" ]; then
|
echo "image=${{ inputs.image_name }}:${{inputs.version}}-$ARCH" >> $GITHUB_OUTPUT
|
||||||
IMAGE="${{ inputs.image_name }}:$REF_NAME-amd64"
|
echo "safe_platform=${{ matrix.platform == 'linux/amd64' && 'linux-amd64' || 'linux-arm64' }}" >> $GITHUB_OUTPUT
|
||||||
else
|
|
||||||
IMAGE="${{ inputs.image_name }}:$REF_NAME-arm64"
|
|
||||||
fi
|
|
||||||
echo "image=$IMAGE" >> $GITHUB_OUTPUT
|
|
||||||
|
|
||||||
- name: Build and push image
|
- name: Build and push image
|
||||||
uses: docker/build-push-action@v6
|
uses: docker/build-push-action@v6
|
||||||
@@ -62,61 +70,73 @@ jobs:
|
|||||||
file: ${{ inputs.dockerfile }}
|
file: ${{ inputs.dockerfile }}
|
||||||
platforms: ${{ matrix.platform }}
|
platforms: ${{ matrix.platform }}
|
||||||
push: true
|
push: true
|
||||||
tags: ${{ steps.set-tags.outputs.image }}
|
tags: ${{ steps.prep.outputs.image }}
|
||||||
target: ${{ inputs.target }}
|
target: ${{ inputs.target }}
|
||||||
|
cache-from: type=gha,scope=build-${{ matrix.platform }}
|
||||||
|
cache-to: type=gha,mode=max,scope=build-${{ matrix.platform }}
|
||||||
|
|
||||||
- name: Prepare artifact name
|
- name: Save image name for manifest
|
||||||
id: artifact
|
run: echo "${{ steps.prep.outputs.image }}" > image.txt
|
||||||
run: |
|
|
||||||
platform=${{ matrix.platform }}
|
|
||||||
echo "safe_platform=${platform//\//-}" >> $GITHUB_OUTPUT
|
|
||||||
echo "${{ steps.set-tags.outputs.image }}" > image.txt
|
|
||||||
|
|
||||||
- uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f
|
- uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: image-${{ steps.artifact.outputs.safe_platform }}
|
name: image-${{ steps.prep.outputs.safe_platform }}
|
||||||
path: image.txt
|
path: image.txt
|
||||||
if-no-files-found: warn
|
retention-days: 1
|
||||||
compression-level: 6
|
|
||||||
overwrite: false
|
|
||||||
include-hidden-files: false
|
|
||||||
|
|
||||||
create-manifest:
|
create-manifest:
|
||||||
name: Create multi-arch Docker manifest (Portabase)
|
name: Create multi-arch manifest
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
needs: build
|
needs: build
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131
|
- uses: actions/download-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: image-linux-amd64
|
name: image-linux-amd64
|
||||||
path: /tmp/digests/amd64
|
path: /tmp/digests/amd64
|
||||||
|
|
||||||
- uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131
|
- uses: actions/download-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: image-linux-arm64
|
name: image-linux-arm64
|
||||||
path: /tmp/digests/arm64
|
path: /tmp/digests/arm64
|
||||||
|
|
||||||
- name: Login to Docker
|
- name: Login to Docker
|
||||||
uses: docker/login-action@f4ef78c080cd8ba55a85445d5b36e214a81df20a
|
uses: docker/login-action@v3
|
||||||
with:
|
with:
|
||||||
username: ${{ secrets.DOCKER_USERNAME }}
|
username: ${{ secrets.DOCKER_USERNAME }}
|
||||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||||
|
|
||||||
|
- name: Generate semantic tags
|
||||||
|
id: tags
|
||||||
|
run: |
|
||||||
|
VERSION="${{ inputs.version }}"
|
||||||
|
IFS='.' read -r MAJOR MINOR PATCH <<< "$VERSION"
|
||||||
|
|
||||||
|
echo "VERSION_TAG=$VERSION" >> $GITHUB_OUTPUT
|
||||||
|
if [ -n "$MINOR" ]; then
|
||||||
|
echo "MINOR_TAG=$MAJOR.$MINOR" >> $GITHUB_OUTPUT
|
||||||
|
fi
|
||||||
|
if [ -n "$MAJOR" ]; then
|
||||||
|
echo "MAJOR_TAG=$MAJOR" >> $GITHUB_OUTPUT
|
||||||
|
fi
|
||||||
|
if [ "${{ inputs.add_latest }}" = "true" ]; then
|
||||||
|
echo "LATEST_TAG=latest" >> $GITHUB_OUTPUT
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Extract Docker metadata
|
||||||
|
id: meta
|
||||||
|
uses: docker/metadata-action@v5
|
||||||
|
with:
|
||||||
|
images: ${{ inputs.image_name }}
|
||||||
|
tags: |
|
||||||
|
type=raw,value=${{ steps.tags.outputs.VERSION_TAG }}
|
||||||
|
type=raw,value=${{ steps.tags.outputs.MINOR_TAG }}
|
||||||
|
type=raw,value=${{ steps.tags.outputs.MAJOR_TAG }}
|
||||||
|
type=raw,value=${{ steps.tags.outputs.LATEST_TAG }}
|
||||||
|
|
||||||
- name: Create and push manifest list
|
- name: Create and push manifest list
|
||||||
working-directory: /tmp/digests
|
working-directory: /tmp/digests
|
||||||
run: |
|
run: |
|
||||||
DOCKER_IMAGES="$(cat amd64/image.txt) $(cat arm64/image.txt)"
|
DOCKER_IMAGES="$(cat amd64/image.txt) $(cat arm64/image.txt)"
|
||||||
REF_NAME=${GITHUB_REF#refs/tags/}
|
TAG_ARGS=$(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON")
|
||||||
MANIFEST_IMAGE="${{ inputs.image_name }}:$REF_NAME"
|
echo $TAG_ARGS
|
||||||
|
docker buildx imagetools create $TAG_ARGS $DOCKER_IMAGES
|
||||||
docker buildx imagetools create $DOCKER_IMAGES -t $MANIFEST_IMAGE
|
|
||||||
docker buildx imagetools inspect $MANIFEST_IMAGE
|
|
||||||
|
|
||||||
if [ "${{ inputs.add_latest }}" = "true" ]; then
|
|
||||||
docker buildx imagetools create $DOCKER_IMAGES -t ${{ inputs.image_name }}:latest
|
|
||||||
docker buildx imagetools inspect ${{ inputs.image_name }}:latest
|
|
||||||
fi
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,77 @@
|
|||||||
|
name: End-to-end testing
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
branches: [main]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build-and-test:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Setup pnpm
|
||||||
|
uses: pnpm/action-setup@v4
|
||||||
|
|
||||||
|
- name: Setup Node.js
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: 22
|
||||||
|
cache: 'pnpm'
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: pnpm install --frozen-lockfile
|
||||||
|
|
||||||
|
- name: Build Docker image
|
||||||
|
uses: docker/build-push-action@v6
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
file: ./docker/dockerfile/Dockerfile
|
||||||
|
push: false
|
||||||
|
load: true
|
||||||
|
tags: portabase/portabase:test
|
||||||
|
|
||||||
|
- name: Run app container
|
||||||
|
run: |
|
||||||
|
docker run -d --name myapp \
|
||||||
|
-p 8887:80 \
|
||||||
|
-e NODE_ENV=production \
|
||||||
|
-e PROJECT_URL=http://localhost:8887 \
|
||||||
|
-e PROJECT_SECRET=80af5e4c875dfded3a6ba511c6ed8b0160cad723f848ee0851403ed6141f6ba1 \
|
||||||
|
portabase/portabase:test
|
||||||
|
|
||||||
|
- name: Wait for app port to be listening
|
||||||
|
run: |
|
||||||
|
for i in {1..40}; do
|
||||||
|
|
||||||
|
if curl -fsS http://localhost:8887/ >/dev/null; then
|
||||||
|
echo "App is responding on port 8887"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
echo "Waiting for app readiness... ($i/40)"
|
||||||
|
sleep 2
|
||||||
|
done
|
||||||
|
echo "Timeout: app never became ready"
|
||||||
|
docker logs myapp
|
||||||
|
exit 1
|
||||||
|
|
||||||
|
- name: Install Playwright browsers
|
||||||
|
run: pnpm exec playwright install --with-deps chromium firefox webkit
|
||||||
|
|
||||||
|
- name: Run Playwright tests
|
||||||
|
run: pnpm exec playwright test --project chromium
|
||||||
|
env:
|
||||||
|
PROJECT_URL: http://localhost:8887
|
||||||
|
|
||||||
|
- name: Upload report if failed
|
||||||
|
if: failure()
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: playwright-report
|
||||||
|
path: playwright-report/
|
||||||
|
retention-days: 7
|
||||||
|
|
||||||
|
- name: Cleanup
|
||||||
|
if: always()
|
||||||
|
run: docker stop myapp && docker rm myapp || true
|
||||||
@@ -1,128 +0,0 @@
|
|||||||
name: GitHub Release
|
|
||||||
|
|
||||||
on:
|
|
||||||
workflow_call:
|
|
||||||
inputs:
|
|
||||||
prerelease:
|
|
||||||
required: false
|
|
||||||
type: boolean
|
|
||||||
default: false
|
|
||||||
make_latest:
|
|
||||||
required: false
|
|
||||||
type: boolean
|
|
||||||
default: false
|
|
||||||
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:
|
|
||||||
create-release:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
permissions:
|
|
||||||
contents: write
|
|
||||||
steps:
|
|
||||||
- name: Check out the repo
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
with:
|
|
||||||
fetch-depth: 0
|
|
||||||
|
|
||||||
- name: Build Changelog
|
|
||||||
id: build_changelog
|
|
||||||
uses: mikepenz/release-changelog-builder-action@v5
|
|
||||||
with:
|
|
||||||
mode: "COMMIT"
|
|
||||||
configurationJson: |
|
|
||||||
{
|
|
||||||
"template": "#{{CHANGELOG}}",
|
|
||||||
"categories": [
|
|
||||||
{
|
|
||||||
"title": "## Feature",
|
|
||||||
"labels": ["feat", "feature"]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"title": "## Fix",
|
|
||||||
"labels": ["fix", "bug"]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"title": "## Other",
|
|
||||||
"labels": []
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"label_extractor": [
|
|
||||||
{
|
|
||||||
"pattern": "^(build|chore|ci|docs|feat|fix|perf|refactor|revert|style|test){1}(\([\w\-\.]+\))?(!)?: ([\w ])+([\s\S]*)",
|
|
||||||
"on_property": "title",
|
|
||||||
"target": "$1"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
env:
|
|
||||||
GITHUB_TOKEN: ${{ secrets.GH_TOKEN }}
|
|
||||||
|
|
||||||
- name: Create GitHub Release
|
|
||||||
uses: softprops/action-gh-release@v2
|
|
||||||
with:
|
|
||||||
generate_release_notes: false
|
|
||||||
body: ${{ steps.build_changelog.outputs.changelog }}
|
|
||||||
prerelease: ${{ inputs.prerelease }}
|
|
||||||
make_latest: ${{ inputs.make_latest }}
|
|
||||||
env:
|
|
||||||
GITHUB_TOKEN: ${{ secrets.GH_TOKEN }}
|
|
||||||
|
|
||||||
- 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,37 @@
|
|||||||
|
name: Publish Helm Chart
|
||||||
|
on:
|
||||||
|
workflow_call:
|
||||||
|
inputs:
|
||||||
|
version:
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
secrets:
|
||||||
|
GH_TOKEN:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
publish-helm:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Set up Helm
|
||||||
|
uses: azure/setup-helm@v4
|
||||||
|
|
||||||
|
- name: Package Helm chart
|
||||||
|
run: |
|
||||||
|
mkdir -p ./helm-packages
|
||||||
|
helm package helm \
|
||||||
|
--version ${{ inputs.version }} \
|
||||||
|
--app-version ${{ inputs.version }} \
|
||||||
|
--destination ./helm-packages
|
||||||
|
|
||||||
|
- name: Authenticate to GitHub Packages
|
||||||
|
run: |
|
||||||
|
echo "${{ secrets.GH_TOKEN }}" | helm registry login ghcr.io -u ${{ github.actor }} --password-stdin
|
||||||
|
|
||||||
|
- name: Push Helm chart to GitHub Packages (OCI)
|
||||||
|
run: |
|
||||||
|
helm push ./helm-packages/portabase-${{ inputs.version }}.tgz oci://ghcr.io/portabase/charts
|
||||||
@@ -3,29 +3,28 @@ name: Publish Docker image for release candidate
|
|||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
tags:
|
tags:
|
||||||
- '*-rc*'
|
- "*.*.*-rc*"
|
||||||
|
- "!*-*-rc*"
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
contents: write
|
contents: read
|
||||||
packages: write
|
packages: write
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
docker_publish:
|
docker_publish:
|
||||||
uses: ./.github/workflows/docker.yml
|
uses: ./.github/workflows/docker.yml
|
||||||
with:
|
with:
|
||||||
add_latest: false
|
add_latest: true
|
||||||
secrets:
|
secrets:
|
||||||
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME_2 }}
|
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME_2 }}
|
||||||
DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD_2 }}
|
DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD_2 }}
|
||||||
|
|
||||||
github_release:
|
discord_notification:
|
||||||
needs: docker_publish
|
needs: docker_publish
|
||||||
uses: ./.github/workflows/github.yml
|
uses: ./.github/workflows/discord.yml
|
||||||
with:
|
with:
|
||||||
prerelease: true
|
discord_title: "New Release Candidate: ${{ github.ref_name }}"
|
||||||
make_latest: false
|
discord_color: 2044800
|
||||||
discord_title: "||@release-dashboard|| New release candidate published"
|
|
||||||
discord_color: 16776960
|
|
||||||
discord_footer: "Portabase"
|
discord_footer: "Portabase"
|
||||||
secrets:
|
secrets:
|
||||||
DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }}
|
DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }}
|
||||||
|
|||||||
+123
-13
@@ -1,32 +1,142 @@
|
|||||||
name: Publish Docker image for release
|
name: Auto Release & Publish
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
pull_request:
|
||||||
tags:
|
types: [ closed ]
|
||||||
- '*.*.*'
|
branches:
|
||||||
- '!*-*'
|
- main
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
contents: write
|
contents: write
|
||||||
packages: write
|
packages: write
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
docker_publish:
|
|
||||||
|
check-skip:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
outputs:
|
||||||
|
skip: ${{ steps.set-skip.outputs.skip }}
|
||||||
|
steps:
|
||||||
|
- name: Determine if release should be skipped
|
||||||
|
id: set-skip
|
||||||
|
run: |
|
||||||
|
TITLE="${{ github.event.pull_request.title }}"
|
||||||
|
echo "PR title: $TITLE"
|
||||||
|
|
||||||
|
if [[ "$TITLE" == *"[skip-release]"* ]]; then
|
||||||
|
echo "PR title contains [skip-release], skipping release jobs."
|
||||||
|
echo "skip=true" >> $GITHUB_OUTPUT
|
||||||
|
else
|
||||||
|
echo "skip=false" >> $GITHUB_OUTPUT
|
||||||
|
fi
|
||||||
|
|
||||||
|
create-release:
|
||||||
|
needs: check-skip
|
||||||
|
if: ${{ needs.check-skip.outputs.skip == 'false' }}
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
outputs:
|
||||||
|
draft_tag: ${{ steps.release_step.outputs.draft_tag }}
|
||||||
|
version: ${{ steps.release_step.outputs.version }}
|
||||||
|
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: main
|
||||||
|
|
||||||
|
- 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]'
|
||||||
|
git config --global user.email 'github-actions[bot]@users.noreply.github.com'
|
||||||
|
|
||||||
|
|
||||||
|
- name: Run release-it
|
||||||
|
id: release_step
|
||||||
|
run: |
|
||||||
|
git pull origin main
|
||||||
|
|
||||||
|
VERSION=$(pnpm exec release-it --ci --release-version)
|
||||||
|
echo $VERSION
|
||||||
|
echo "version=$VERSION" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
|
OUTPUT=$(pnpm exec release-it --ci)
|
||||||
|
echo "$OUTPUT"
|
||||||
|
|
||||||
|
DRAFT_TAG=$(echo "$OUTPUT" | grep -oE 'untagged-[a-z0-9]+')
|
||||||
|
echo $DRAFT_TAG
|
||||||
|
echo "draft_tag=$DRAFT_TAG" >> $GITHUB_OUTPUT
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ steps.app-token.outputs.token }}
|
||||||
|
|
||||||
|
|
||||||
|
publish-docker:
|
||||||
|
needs: create-release
|
||||||
|
if: ${{ needs.create-release.result == 'success' }}
|
||||||
uses: ./.github/workflows/docker.yml
|
uses: ./.github/workflows/docker.yml
|
||||||
with:
|
with:
|
||||||
|
version: ${{ needs.create-release.outputs.version }}
|
||||||
|
ref: ${{ needs.create-release.outputs.version }}
|
||||||
add_latest: true
|
add_latest: true
|
||||||
secrets:
|
secrets:
|
||||||
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME_2 }}
|
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME_2 }}
|
||||||
DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD_2 }}
|
DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD_2 }}
|
||||||
|
|
||||||
github_release:
|
publish-helm:
|
||||||
needs: docker_publish
|
needs: create-release
|
||||||
uses: ./.github/workflows/github.yml
|
if: ${{ needs.create-release.result == 'success' }}
|
||||||
|
uses: ./.github/workflows/helm.yml
|
||||||
with:
|
with:
|
||||||
prerelease: false
|
version: ${{ needs.create-release.outputs.version }}
|
||||||
make_latest: true
|
secrets:
|
||||||
discord_title: "||@release-dashboard|| New release published"
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
discord_color: 5814783
|
|
||||||
|
finalize-release:
|
||||||
|
needs:
|
||||||
|
- create-release
|
||||||
|
- publish-docker
|
||||||
|
- publish-helm
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
outputs:
|
||||||
|
release_tag: ${{ steps.publish_release_step.outputs.release_tag }}
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Publish GitHub Release
|
||||||
|
id: publish_release_step
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
run: |
|
||||||
|
OUTPUT=$(gh release edit ${{ needs.create-release.outputs.draft_tag }} --draft=false)
|
||||||
|
echo "$OUTPUT"
|
||||||
|
RELEASE_TAG=$(echo "$OUTPUT" | sed -E 's|.*/releases/tag/||')
|
||||||
|
echo "release_tag=$RELEASE_TAG" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
|
notify-discord:
|
||||||
|
needs:
|
||||||
|
- publish-docker
|
||||||
|
- publish-helm
|
||||||
|
- create-release
|
||||||
|
- finalize-release
|
||||||
|
uses: ./.github/workflows/discord.yml
|
||||||
|
with:
|
||||||
|
release_tag: ${{ needs.create-release.outputs.version }}
|
||||||
|
discord_title: "New Release"
|
||||||
|
discord_color: 3066993
|
||||||
discord_footer: "Portabase"
|
discord_footer: "Portabase"
|
||||||
secrets:
|
secrets:
|
||||||
DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }}
|
DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }}
|
||||||
|
|||||||
@@ -5,27 +5,27 @@ 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
|
||||||
- uses: aquasecurity/trivy-action@0.20.0
|
- uses: aquasecurity/trivy-action@0.35.0
|
||||||
with:
|
with:
|
||||||
scan-type: 'fs'
|
scan-type: 'fs'
|
||||||
format: 'table'
|
format: 'table'
|
||||||
severity: 'CRITICAL,HIGH'
|
severity: 'CRITICAL,HIGH'
|
||||||
ignore-unfixed: true
|
ignore-unfixed: true
|
||||||
|
|
||||||
|
secrets-gitleaks:
|
||||||
secrets-gitleaks: # Secrets exposure
|
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:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
GITLEAKS_LICENSE: ${{ secrets.GITLEAKS_LICENSE }}
|
GITLEAKS_LICENSE: ${{ secrets.GITLEAKS_LICENSE }}
|
||||||
with:
|
with:
|
||||||
config-path: .gitleaks.toml # Optional, if you have a custom config
|
config-path: .gitleaks.toml
|
||||||
+10
-1
@@ -44,4 +44,13 @@ private/keys/*
|
|||||||
|
|
||||||
/.env
|
/.env
|
||||||
|
|
||||||
certificates
|
seeds/pocket-id/*.db
|
||||||
|
|
||||||
|
certificates
|
||||||
|
|
||||||
|
# Playwright
|
||||||
|
/test-results/
|
||||||
|
/playwright-report/
|
||||||
|
/blob-report/
|
||||||
|
/playwright/.cache/
|
||||||
|
/playwright/.auth/
|
||||||
|
|||||||
+1
-3
@@ -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"
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -0,0 +1,57 @@
|
|||||||
|
{
|
||||||
|
"github": {
|
||||||
|
"release": true,
|
||||||
|
"draft": true,
|
||||||
|
"tokenRef": "GITHUB_TOKEN"
|
||||||
|
},
|
||||||
|
"git": {
|
||||||
|
"commit": true,
|
||||||
|
"commitMessage": "chore: release ${version}",
|
||||||
|
"requireCleanWorkingDir": true,
|
||||||
|
"tag": true,
|
||||||
|
"tagName": "${version}",
|
||||||
|
"push": true
|
||||||
|
},
|
||||||
|
"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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+9
-4
@@ -1,6 +1,6 @@
|
|||||||
cff-version: 1.2.0
|
cff-version: 1.2.0
|
||||||
title: Portabase
|
title: Portabase
|
||||||
message: "If you use this software, please cite it as below."
|
message: If you use this software, please cite it as below.
|
||||||
type: software
|
type: software
|
||||||
authors:
|
authors:
|
||||||
- family-names: Gauthereau
|
- family-names: Gauthereau
|
||||||
@@ -9,7 +9,12 @@ authors:
|
|||||||
given-names: Killian
|
given-names: Killian
|
||||||
repository-code: https://github.com/Portabase/portabase
|
repository-code: https://github.com/Portabase/portabase
|
||||||
url: https://portabase.io
|
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."
|
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:
|
keywords:
|
||||||
- docker
|
- docker
|
||||||
- kubernetes
|
- kubernetes
|
||||||
@@ -26,5 +31,5 @@ keywords:
|
|||||||
- web-ui
|
- web-ui
|
||||||
- agent
|
- agent
|
||||||
license: Apache-2.0
|
license: Apache-2.0
|
||||||
version: 1.2.6
|
version: 1.9.5
|
||||||
date-released: "2026-02-15"
|
date-released: '2026-03-02'
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -12,18 +12,20 @@
|
|||||||
|
|
||||||
|
|
||||||
[](LICENSE)
|
[](LICENSE)
|
||||||
[](https://hub.docker.com/r/solucetechnologies/portabase)
|
[](https://hub.docker.com/r/portabase/portabase)
|
||||||
[](https://github.com/Portabase/portabase)
|
[](https://github.com/Portabase/portabase)
|
||||||
[](https://www.buymeacoffee.com/portabase)
|
[](https://www.buymeacoffee.com/portabase)
|
||||||
|
|
||||||
[](https://www.postgresql.org/)
|
[](https://www.postgresql.org/)
|
||||||
[](https://www.mysql.com/)
|
[](https://www.mysql.com/)
|
||||||
[](https://mariadb.org/)
|
[](https://mariadb.org/)
|
||||||
|
[](https://sqlite.org/)
|
||||||
|
[](https://redis.io/)
|
||||||
|
[](https://valkey.io/)
|
||||||
[](https://www.mongodb.com/)
|
[](https://www.mongodb.com/)
|
||||||
[](https://github.com/Portabase/portabase)
|
[](https://github.com/Portabase/portabase)
|
||||||
[](https://github.com/Portabase/portabase)
|
[](https://github.com/Portabase/portabase)
|
||||||
|
|
||||||
|
|
||||||
[![NextJS][NextJS]][NextJS-url]
|
[![NextJS][NextJS]][NextJS-url]
|
||||||
[![BetterAuth][BetterAuth]][BetterAuth-url]
|
[![BetterAuth][BetterAuth]][BetterAuth-url]
|
||||||
[![Drizzle][Drizzle]][Drizzle-url]
|
[![Drizzle][Drizzle]][Drizzle-url]
|
||||||
@@ -34,14 +36,14 @@
|
|||||||
<strong>
|
<strong>
|
||||||
<a href="https://portabase.io">Website</a> •
|
<a href="https://portabase.io">Website</a> •
|
||||||
<a href="https://portabase.io/docs">Documentation</a> •
|
<a href="https://portabase.io/docs">Documentation</a> •
|
||||||
<a href="https://www.youtube.com/watch?v=hvLbX5LN1UE">Demo</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://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=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>
|
<a href="https://github.com/Portabase/portabase/issues/new?labels=enhancement&template=feature-request---.md">Request Feature</a>
|
||||||
</strong>
|
</strong>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
@@ -51,13 +53,27 @@
|
|||||||
You have 4 ways to install Portabase:
|
You have 4 ways to install Portabase:
|
||||||
|
|
||||||
- Automated CLI (recommended) - [details](https://portabase.io/docs/dashboard/setup#cli)
|
- Automated CLI (recommended) - [details](https://portabase.io/docs/dashboard/setup#cli)
|
||||||
- Docker Compose setup - [details](https://portabase.io/docs/dashboard/setup#docker)
|
- Docker Run - [details](https://portabase.io/docs/dashboard/setup#docker)
|
||||||
- Kubernetes with Helm (soon)
|
- Docker Compose setup - [details](https://portabase.io/docs/dashboard/setup#docker-compose)
|
||||||
|
- Kubernetes with Helm [details](https://portabase.io/docs/dashboard/setup#helm)
|
||||||
- Development setup - [details](https://portabase.io/docs/dashboard/setup#development)
|
- Development setup - [details](https://portabase.io/docs/dashboard/setup#development)
|
||||||
|
|
||||||
**Ensure Docker is installed on your machine before getting started.**
|
**Ensure Docker is installed on your machine before getting started.**
|
||||||
|
|
||||||
|
## Supported databases
|
||||||
|
|
||||||
|
| Engine | Support | Supported Versions | Restore |
|
||||||
|
|:-------------------|:----------|:----------------------------------|:--------|
|
||||||
|
| **PostgreSQL** | ✅ Stable | 12, 13, 14, 15, 16, 17 et 18 | Yes |
|
||||||
|
| **MySQL** | ✅ Stable | 5.7, 8 et 9 | Yes |
|
||||||
|
| **MariaDB** | ✅ Stable | 10 et 11 | Yes |
|
||||||
|
| **MongoDB** | ✅ Stable | 4, 5, 6, 7 et 8 | Yes |
|
||||||
|
| **SQLite** | ✅ Stable | 3.x | Yes |
|
||||||
|
| **Redis** | ✅ Stable | 2.8+ | No |
|
||||||
|
| **Valkey** | ✅ Stable | 7.2+ | No |
|
||||||
|
| **MSSQL Server** | ❌ Ongoing | - | Yes |
|
||||||
|
|
||||||
|
See the [Database Servers documentation](https://portabase.io/docs/agent/db) for version-specific backup and restore details.
|
||||||
|
|
||||||
## Contributors
|
## Contributors
|
||||||
|
|
||||||
@@ -89,4 +105,3 @@ Distributed under the Apache License. See `LICENSE.txt` for more details.
|
|||||||
[ShadcnUI-url]: https://ui.shadcn.com/
|
[ShadcnUI-url]: https://ui.shadcn.com/
|
||||||
|
|
||||||
[Docker-url]: https://www.docker.com/
|
[Docker-url]: https://www.docker.com/
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,15 @@
|
|||||||
import {CardContent, CardHeader} from "@/components/ui/card";
|
import { CardContent, CardHeader } from "@/components/ui/card";
|
||||||
|
|
||||||
import {TooltipProvider} from "@/components/ui/tooltip";
|
import { TooltipProvider } from "@/components/ui/tooltip";
|
||||||
import {ForgotPasswordForm} from "@/components/wrappers/auth/login/forgot-password-form/forgot-password-form";
|
import { ForgotPasswordForm } from "@/components/wrappers/auth/login/forgot-password-form/forgot-password-form";
|
||||||
import {CardAuth} from "@/features/layout/card-auth";
|
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 }> }) {
|
export default async function RoutePage(props: { searchParams: Promise<{ callbackUrl: string | undefined }> }) {
|
||||||
|
if (env.AUTH_EMAIL_PASSWORD_ENABLED !== "true") {
|
||||||
|
redirect("/login");
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TooltipProvider>
|
<TooltipProvider>
|
||||||
@@ -12,12 +17,11 @@ export default async function RoutePage(props: { searchParams: Promise<{ callbac
|
|||||||
<CardHeader>
|
<CardHeader>
|
||||||
<div className="grid gap-2 text-center mb-2">
|
<div className="grid gap-2 text-center mb-2">
|
||||||
<h1 className="text-3xl font-bold">Reset password</h1>
|
<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
|
<p className="text-balance text-muted-foreground">Enter your email address and we'll send you a link to reset your password.</p>
|
||||||
link to reset your password.</p>
|
|
||||||
</div>
|
</div>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<ForgotPasswordForm/>
|
<ForgotPasswordForm />
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</CardAuth>
|
</CardAuth>
|
||||||
</TooltipProvider>
|
</TooltipProvider>
|
||||||
|
|||||||
+30
-27
@@ -1,32 +1,35 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import {redirect} from "next/navigation";
|
import { redirect } from "next/navigation";
|
||||||
import {currentUser} from "@/lib/auth/current-user";
|
import { currentUser } from "@/lib/auth/current-user";
|
||||||
import {AuthLogoSection} from "@/components/wrappers/auth/auth-logo-section";
|
import { AuthLogoSection } from "@/components/wrappers/auth/auth-logo-section";
|
||||||
import {env} from "@/env.mjs";
|
import { Heart } from "lucide-react";
|
||||||
import {Heart} from "lucide-react";
|
|
||||||
|
|
||||||
export default async function Layout({children}: { children: React.ReactNode }) {
|
export default async function Layout({
|
||||||
|
children,
|
||||||
|
}: {
|
||||||
|
children: React.ReactNode;
|
||||||
|
}) {
|
||||||
|
const user = await currentUser();
|
||||||
|
|
||||||
const user = await currentUser();
|
if (user && !user.banned && user.role !== "pending") {
|
||||||
|
redirect("/dashboard/home");
|
||||||
|
}
|
||||||
|
|
||||||
if (user && !user.banned && user.role !== "pending") {
|
return (
|
||||||
redirect("/dashboard/home");
|
<div className="flex min-h-screen flex-col justify-between py-10 sm:px-6 lg:px-8 ">
|
||||||
}
|
<div className="flex flex-col items-center justify-center flex-1">
|
||||||
|
<div className="mx-auto w-full max-w-md">
|
||||||
return (
|
<AuthLogoSection />
|
||||||
<div className="flex min-h-screen flex-col justify-between py-10 sm:px-6 lg:px-8 ">
|
<div className="mt-4">{children}</div>
|
||||||
<div className="flex flex-col items-center justify-center flex-1">
|
|
||||||
<div className="mx-auto w-full max-w-md">
|
|
||||||
<AuthLogoSection/>
|
|
||||||
<div className="mt-4">{children}</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" /> by <span className="font-medium text-foreground">Portabase</span>
|
|
||||||
</p>
|
|
||||||
<p>v{env.NEXT_PUBLIC_PROJECT_VERSION}</p>
|
|
||||||
</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>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
+49
-42
@@ -1,52 +1,59 @@
|
|||||||
import {LoginForm} from "@/components/wrappers/auth/login/login-form/login-form";
|
import { LoginForm } from "@/components/wrappers/auth/login/login-form/login-form";
|
||||||
import {Metadata} from "next";
|
import { Metadata } from "next";
|
||||||
import {SUPPORTED_PROVIDERS} from "../../../portabase.config";
|
import { SUPPORTED_PROVIDERS } from "@/lib/auth/config";
|
||||||
import {SocialAuthButtons} from "@/components/wrappers/auth/social-buttons";
|
import { SocialAuthButtons } from "@/components/wrappers/auth/social-buttons";
|
||||||
import {TooltipProvider} from "@/components/ui/tooltip";
|
import { TooltipProvider } from "@/components/ui/tooltip";
|
||||||
import {CardContent, CardHeader} from "@/components/ui/card";
|
import { CardContent, CardHeader } from "@/components/ui/card";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import {Separator} from "@/components/ui/separator";
|
import { Separator } from "@/components/ui/separator";
|
||||||
import {CardAuth} from "@/features/layout/card-auth";
|
import { CardAuth } from "@/features/layout/card-auth";
|
||||||
|
import { env } from "@/env.mjs";
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: "Login",
|
title: "Login",
|
||||||
};
|
};
|
||||||
|
|
||||||
export default async function SignInPage() {
|
export default async function SignInPage() {
|
||||||
|
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"} />
|
||||||
|
)}
|
||||||
|
|
||||||
return (
|
{env.AUTH_EMAIL_PASSWORD_ENABLED === "true" &&
|
||||||
<TooltipProvider>
|
SUPPORTED_PROVIDERS.filter((p) => !p.isManual && p.isActive)
|
||||||
<CardAuth className="w-full">
|
.length > 0 && (
|
||||||
<CardHeader>
|
<div className="relative my-4 flex items-center justify-center overflow-hidden">
|
||||||
<div className="grid gap-2 text-center mb-2">
|
<Separator />
|
||||||
<h1 className="text-3xl font-bold">Login</h1>
|
<div className="px-2 text-center text-sm">OR</div>
|
||||||
<p className="text-balance text-muted-foreground">Fill your login informations</p>
|
<Separator />
|
||||||
</div>
|
</div>
|
||||||
</CardHeader>
|
)}
|
||||||
<CardContent className="space-y-4">
|
|
||||||
<LoginForm/>
|
|
||||||
|
|
||||||
{SUPPORTED_PROVIDERS.filter((p) => !p.isManual && p.isActive).length > 0 && (
|
{SUPPORTED_PROVIDERS.filter((p) => !p.isManual && p.isActive).length >
|
||||||
<>
|
0 && <SocialAuthButtons providers={SUPPORTED_PROVIDERS} />}
|
||||||
<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>
|
|
||||||
<SocialAuthButtons providers={SUPPORTED_PROVIDERS}/>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div className="mt-4 text-center text-sm">
|
{env.AUTH_SIGNUP_ENABLED !== "true" ||
|
||||||
Don't have an account ?{" "}
|
(env.AUTH_EMAIL_PASSWORD_ENABLED === "true" && (
|
||||||
<Link href="/register" className="underline">
|
<div className="mt-4 text-center text-sm">
|
||||||
Sign up
|
Don't have an account ?{" "}
|
||||||
</Link>
|
<Link href="/register" className="underline">
|
||||||
</div>
|
Sign up
|
||||||
</CardContent>
|
</Link>
|
||||||
</CardAuth>
|
</div>
|
||||||
</TooltipProvider>
|
))}
|
||||||
|
</CardContent>
|
||||||
|
</CardAuth>
|
||||||
)
|
</TooltipProvider>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,15 +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 { Metadata } from "next";
|
||||||
|
import { redirect } from "next/navigation";
|
||||||
|
import { env } from "@/env.mjs";
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: "Register",
|
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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
@@ -4,9 +4,13 @@ import { ResetPasswordForm } from "@/components/wrappers/auth/login/reset-passwo
|
|||||||
import { redirect } from "next/navigation";
|
import { redirect } from "next/navigation";
|
||||||
import { auth } from "@/lib/auth/auth";
|
import { auth } from "@/lib/auth/auth";
|
||||||
import { Avatar, AvatarImage, AvatarFallback } from "@radix-ui/react-avatar";
|
import { Avatar, AvatarImage, AvatarFallback } from "@radix-ui/react-avatar";
|
||||||
import {CardAuth} from "@/features/layout/card-auth";
|
import { CardAuth } from "@/features/layout/card-auth";
|
||||||
|
import { env } from "@/env.mjs";
|
||||||
|
|
||||||
export default async function RoutePage(props: { searchParams: Promise<{ token: string | undefined }> }) {
|
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;
|
const { token } = await props.searchParams;
|
||||||
|
|
||||||
|
|||||||
@@ -1,30 +0,0 @@
|
|||||||
import {PageParams} from "@/types/next";
|
|
||||||
import {Page, PageContent, PageHeader, PageTitle} from "@/features/layout/page";
|
|
||||||
import {db} from "@/db";
|
|
||||||
import {SettingsEmailSection} from "@/components/wrappers/dashboard/admin/settings/email/settings-email-section";
|
|
||||||
import {notFound} from "next/navigation";
|
|
||||||
|
|
||||||
export default async function RoutePage(props: PageParams<{}>) {
|
|
||||||
|
|
||||||
const settings = await db.query.setting.findFirst({
|
|
||||||
where: (fields, {eq}) => eq(fields.name, "system"),
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!settings) {
|
|
||||||
notFound()
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Page>
|
|
||||||
<PageHeader className="flex flex-col">
|
|
||||||
<div className="flex justify-between">
|
|
||||||
<PageTitle className="mb-3">System email</PageTitle>
|
|
||||||
</div>
|
|
||||||
</PageHeader>
|
|
||||||
<PageContent className="flex flex-col gap-5">
|
|
||||||
<SettingsEmailSection settings={settings}/>
|
|
||||||
</PageContent>
|
|
||||||
</Page>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
import {PageParams} from "@/types/next";
|
|
||||||
import {Page, PageContent, PageHeader, PageTitle} from "@/features/layout/page";
|
|
||||||
import {SettingsStorageSection} from "@/components/wrappers/dashboard/admin/settings/storage/settings-storage-section";
|
|
||||||
import {db} from "@/db";
|
|
||||||
import {notFound} from "next/navigation";
|
|
||||||
|
|
||||||
export default async function RoutePage(props: PageParams<{}>) {
|
|
||||||
|
|
||||||
const settings = await db.query.setting.findFirst({
|
|
||||||
where: (fields, {eq}) => eq(fields.name, "system"),
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!settings) {
|
|
||||||
notFound()
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Page>
|
|
||||||
<PageHeader className="flex flex-col">
|
|
||||||
<div className="flex justify-between">
|
|
||||||
<PageTitle className="mb-3">System storage</PageTitle>
|
|
||||||
</div>
|
|
||||||
</PageHeader>
|
|
||||||
<PageContent className="flex flex-col gap-5">
|
|
||||||
<SettingsStorageSection settings={settings}/>
|
|
||||||
</PageContent>
|
|
||||||
</Page>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -6,6 +6,7 @@ import {SettingsTabs} from "@/components/wrappers/dashboard/admin/settings/setti
|
|||||||
import {desc, isNull} from "drizzle-orm";
|
import {desc, isNull} from "drizzle-orm";
|
||||||
import * as drizzleDb from "@/db";
|
import * as drizzleDb from "@/db";
|
||||||
import {StorageChannelWith} from "@/db/schema/12_storage-channel";
|
import {StorageChannelWith} from "@/db/schema/12_storage-channel";
|
||||||
|
import {NotificationChannelWith} from "@/db/schema/09_notification-channel";
|
||||||
|
|
||||||
export default async function RoutePage(props: PageParams<{}>) {
|
export default async function RoutePage(props: PageParams<{}>) {
|
||||||
|
|
||||||
@@ -13,6 +14,8 @@ export default async function RoutePage(props: PageParams<{}>) {
|
|||||||
where: (fields, {eq}) => eq(fields.name, "system"),
|
where: (fields, {eq}) => eq(fields.name, "system"),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
console.log(settings)
|
||||||
|
|
||||||
const storageChannels = await db.query.storageChannel.findMany({
|
const storageChannels = await db.query.storageChannel.findMany({
|
||||||
with: {
|
with: {
|
||||||
organizations: true
|
organizations: true
|
||||||
@@ -21,7 +24,16 @@ export default async function RoutePage(props: PageParams<{}>) {
|
|||||||
orderBy: desc(drizzleDb.schemas.storageChannel.createdAt)
|
orderBy: desc(drizzleDb.schemas.storageChannel.createdAt)
|
||||||
}) as StorageChannelWith[]
|
}) as StorageChannelWith[]
|
||||||
|
|
||||||
if (!settings || !storageChannels ) {
|
const notificationChannels = await db.query.notificationChannel.findMany({
|
||||||
|
with: {
|
||||||
|
organizations: true
|
||||||
|
},
|
||||||
|
where: isNull(drizzleDb.schemas.notificationChannel.organizationId),
|
||||||
|
orderBy: desc(drizzleDb.schemas.notificationChannel.createdAt)
|
||||||
|
}) as NotificationChannelWith[]
|
||||||
|
|
||||||
|
|
||||||
|
if (!settings || !storageChannels || !notificationChannels ) {
|
||||||
notFound()
|
notFound()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -33,7 +45,7 @@ export default async function RoutePage(props: PageParams<{}>) {
|
|||||||
</div>
|
</div>
|
||||||
</PageHeader>
|
</PageHeader>
|
||||||
<PageContent className="flex flex-col gap-5">
|
<PageContent className="flex flex-col gap-5">
|
||||||
<SettingsTabs storageChannels={storageChannels} settings={settings} />
|
<SettingsTabs storageChannels={storageChannels} notificationChannels={notificationChannels} settings={settings} />
|
||||||
</PageContent>
|
</PageContent>
|
||||||
</Page>
|
</Page>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import {db} from "@/db";
|
|||||||
import {desc, isNull} from "drizzle-orm";
|
import {desc, isNull} from "drizzle-orm";
|
||||||
import {AdminUserList} from "@/components/wrappers/dashboard/admin/users/admin-user-list";
|
import {AdminUserList} from "@/components/wrappers/dashboard/admin/users/admin-user-list";
|
||||||
import {AdminUserAddModal} from "@/components/wrappers/dashboard/admin/users/admin-user-add-modal";
|
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<{}>) {
|
export default async function RoutePage(props: PageParams<{}>) {
|
||||||
|
|
||||||
@@ -21,6 +22,9 @@ export default async function RoutePage(props: PageParams<{}>) {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const credentialProvider = SUPPORTED_PROVIDERS.find(p => p.id === 'credential');
|
||||||
|
const isPasswordAuthEnabled = credentialProvider?.isActive || false;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Page>
|
<Page>
|
||||||
<PageHeader className="flex flex-col">
|
<PageHeader className="flex flex-col">
|
||||||
@@ -32,7 +36,7 @@ export default async function RoutePage(props: PageParams<{}>) {
|
|||||||
</div>
|
</div>
|
||||||
</PageHeader>
|
</PageHeader>
|
||||||
<PageContent className="flex flex-col gap-5">
|
<PageContent className="flex flex-col gap-5">
|
||||||
<AdminUserList users={users}/>
|
<AdminUserList users={users} isPasswordAuthEnabled={isPasswordAuthEnabled}/>
|
||||||
</PageContent>
|
</PageContent>
|
||||||
</Page>
|
</Page>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,61 +1,69 @@
|
|||||||
import {PageParams} from "@/types/next";
|
import { PageParams } from "@/types/next";
|
||||||
import {Page, PageContent, PageDescription, PageTitle} from "@/features/layout/page";
|
import {
|
||||||
import {db} from "@/db";
|
Page,
|
||||||
|
PageContent,
|
||||||
|
PageDescription,
|
||||||
|
PageTitle,
|
||||||
|
} from "@/features/layout/page";
|
||||||
|
import { db } from "@/db";
|
||||||
import * as drizzleDb from "@/db";
|
import * as drizzleDb from "@/db";
|
||||||
import {eq} from "drizzle-orm";
|
import { eq } from "drizzle-orm";
|
||||||
import {notFound} from "next/navigation";
|
import { notFound } from "next/navigation";
|
||||||
import {ButtonDeleteAgent} from "@/components/wrappers/dashboard/agent/button-delete-agent/button-delete-agent";
|
import { ButtonDeleteAgent } from "@/components/wrappers/dashboard/agent/button-delete-agent/button-delete-agent";
|
||||||
import {capitalizeFirstLetter} from "@/utils/text";
|
import { capitalizeFirstLetter } from "@/utils/text";
|
||||||
import {generateEdgeKey} from "@/utils/edge_key";
|
import { generateEdgeKey } from "@/utils/edge_key";
|
||||||
import {getServerUrl} from "@/utils/get-server-url";
|
import { getServerUrl } from "@/utils/get-server-url";
|
||||||
import {AgentContentPage} from "@/components/wrappers/dashboard/agent/agent-content";
|
import { AgentContentPage } from "@/components/wrappers/dashboard/agent/agent-content";
|
||||||
import {AgentDialog} from "@/features/agents/components/agent.dialog";
|
import { AgentDialog } from "@/features/agents/components/agent.dialog";
|
||||||
import {AgentType} from "@/features/agents/agents.schema";
|
import { AgentType } from "@/features/agents/agents.schema";
|
||||||
|
|
||||||
export default async function RoutePage(props: PageParams<{ agentId: string }>) {
|
export default async function RoutePage(
|
||||||
|
props: PageParams<{ agentId: string }>,
|
||||||
|
) {
|
||||||
|
const { agentId } = await props.params;
|
||||||
|
|
||||||
const {agentId} = await props.params
|
const agent = await db.query.agent.findFirst({
|
||||||
|
where: eq(drizzleDb.schemas.agent.id, agentId),
|
||||||
|
with: {
|
||||||
|
databases: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
const agent = await db.query.agent.findFirst({
|
if (!agent) {
|
||||||
where: eq(drizzleDb.schemas.agent.id, agentId),
|
notFound();
|
||||||
with: {
|
}
|
||||||
databases: true
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
if (!agent) {
|
const edgeKey = await generateEdgeKey(getServerUrl(), agent.id);
|
||||||
notFound()
|
|
||||||
}
|
|
||||||
|
|
||||||
const edgeKey = await generateEdgeKey(getServerUrl(), agent.id);
|
return (
|
||||||
|
<Page>
|
||||||
return (
|
<div className="justify-between gap-2 sm:flex">
|
||||||
<Page>
|
<PageTitle className="flex flex-col md:flex-row items-center justify-between w-full ">
|
||||||
<div className="justify-between gap-2 sm:flex">
|
<div className="min-w-full md:min-w-fit ">
|
||||||
<PageTitle className="flex flex-col md:flex-row items-center justify-between w-full ">
|
{capitalizeFirstLetter(agent.name)}
|
||||||
<div className="min-w-full md:min-w-fit ">
|
</div>
|
||||||
{capitalizeFirstLetter(agent.name)}
|
<div className="flex items-center gap-2 md:justify-between w-full ">
|
||||||
</div>
|
<div className="flex items-center gap-2">
|
||||||
<div className="flex items-center gap-2 md:justify-between w-full ">
|
<AgentDialog
|
||||||
<div className="flex items-center gap-2">
|
agent={agent as AgentType & { id: string }}
|
||||||
<AgentDialog agent={agent as AgentType & { id: string }} typeTrigger={"edit"}/>
|
typeTrigger={"edit"}
|
||||||
</div>
|
/>
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<ButtonDeleteAgent agentId={agentId} text={"Delete Agent"}/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</PageTitle>
|
|
||||||
</div>
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<ButtonDeleteAgent agentId={agentId} text={"Delete Agent"} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</PageTitle>
|
||||||
|
</div>
|
||||||
|
|
||||||
{agent.description && (
|
{agent.description && (
|
||||||
<PageDescription className="mt-5 sm:mt-0">{agent.description}</PageDescription>
|
<PageDescription className="mt-5 sm:mt-0">
|
||||||
)}
|
{agent.description}
|
||||||
<PageContent className="flex flex-col w-full h-full justify-between gap-6">
|
</PageDescription>
|
||||||
<AgentContentPage
|
)}
|
||||||
agent={agent}
|
<PageContent className="flex flex-col w-full h-full justify-between gap-6">
|
||||||
edgeKey={edgeKey}
|
<AgentContentPage agent={agent} edgeKey={edgeKey} />
|
||||||
/>
|
</PageContent>
|
||||||
</PageContent>
|
</Page>
|
||||||
</Page>
|
);
|
||||||
)
|
}
|
||||||
}
|
|
||||||
|
|||||||
@@ -35,12 +35,10 @@ export default async function RoutePage(props: PageParams<{}>) {
|
|||||||
<PageHeader>
|
<PageHeader>
|
||||||
<PageTitle>Notification channels</PageTitle>
|
<PageTitle>Notification channels</PageTitle>
|
||||||
<PageActions>
|
<PageActions>
|
||||||
{/*<NotifierAddEditModal adminView={false}/>*/}
|
|
||||||
<ChannelAddEditModal kind="notification" adminView={false}/>
|
<ChannelAddEditModal kind="notification" adminView={false}/>
|
||||||
</PageActions>
|
</PageActions>
|
||||||
</PageHeader>
|
</PageHeader>
|
||||||
<PageContent>
|
<PageContent>
|
||||||
{/*<NotificationChannelsSection organizations={organizations} notificationChannels={notificationChannels}/>*/}
|
|
||||||
<ChannelsSection kind="notification" organizations={organizations}
|
<ChannelsSection kind="notification" organizations={organizations}
|
||||||
channels={notificationChannels}/>
|
channels={notificationChannels}/>
|
||||||
</PageContent>
|
</PageContent>
|
||||||
|
|||||||
-36
@@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
+4
@@ -8,6 +8,7 @@ import {getOrganizationProjectDatabases} from "@/lib/services";
|
|||||||
import {getActiveMember, getOrganization} from "@/lib/auth/auth";
|
import {getActiveMember, getOrganization} from "@/lib/auth/auth";
|
||||||
import {BackupModalProvider} from "@/components/wrappers/dashboard/database/backup/backup-modal-context";
|
import {BackupModalProvider} from "@/components/wrappers/dashboard/database/backup/backup-modal-context";
|
||||||
import {DatabaseContent} from "@/components/wrappers/dashboard/projects/database/database-content";
|
import {DatabaseContent} from "@/components/wrappers/dashboard/projects/database/database-content";
|
||||||
|
import {getHealthLast12hLogs} from "@/db/services/healthcheck";
|
||||||
|
|
||||||
export default async function RoutePage(props: PageParams<{
|
export default async function RoutePage(props: PageParams<{
|
||||||
projectId: string;
|
projectId: string;
|
||||||
@@ -83,6 +84,8 @@ export default async function RoutePage(props: PageParams<{
|
|||||||
notFound();
|
notFound();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const databaseHealthLogs = dbItem ? await getHealthLast12hLogs({ id: dbItem.id }) : []
|
||||||
|
|
||||||
|
|
||||||
const successRate = totalBackups > 0 ? (successfulBackups / totalBackups) * 100 : null;
|
const successRate = totalBackups > 0 ? (successfulBackups / totalBackups) * 100 : null;
|
||||||
|
|
||||||
@@ -95,6 +98,7 @@ export default async function RoutePage(props: PageParams<{
|
|||||||
activeMember={activeMember}
|
activeMember={activeMember}
|
||||||
settings={settings}
|
settings={settings}
|
||||||
database={dbItem}
|
database={dbItem}
|
||||||
|
databaseHealthLogs={databaseHealthLogs}
|
||||||
isAlreadyRestore={isAlreadyRestore}
|
isAlreadyRestore={isAlreadyRestore}
|
||||||
restorations={restorations}
|
restorations={restorations}
|
||||||
backups={backups}
|
backups={backups}
|
||||||
|
|||||||
@@ -1,89 +1,95 @@
|
|||||||
import {PageParams} from "@/types/next";
|
import { PageParams } from "@/types/next";
|
||||||
import {Page, PageActions, PageContent, PageHeader, PageTitle} from "@/features/layout/page";
|
import {
|
||||||
import {currentUser} from "@/lib/auth/current-user";
|
Page,
|
||||||
import {getActiveMember, getOrganization} from "@/lib/auth/auth";
|
PageActions,
|
||||||
import {notFound} from "next/navigation";
|
PageContent,
|
||||||
import {Metadata} from "next";
|
PageHeader,
|
||||||
import {OrganizationTabs} from "@/components/wrappers/dashboard/organization/tabs/organization-tabs";
|
PageTitle,
|
||||||
import {getOrganizationChannels} from "@/db/services/notification-channel";
|
} from "@/features/layout/page";
|
||||||
import {computeOrganizationPermissions} from "@/lib/acl/organization-acl";
|
import { currentUser } from "@/lib/auth/current-user";
|
||||||
import {getOrganizationStorageChannels} from "@/db/services/storage-channel";
|
import { getActiveMember, getOrganization } from "@/lib/auth/auth";
|
||||||
import {DeleteOrganizationButton} from "@/components/wrappers/dashboard/organization/delete-organization-button";
|
import { notFound } from "next/navigation";
|
||||||
import {EditOrganizationDialog} from "@/features/organization/components/edit-organization.dialog";
|
import { Metadata } from "next";
|
||||||
import {Button} from "@/components/ui/button";
|
import { OrganizationTabs } from "@/components/wrappers/dashboard/organization/tabs/organization-tabs";
|
||||||
import {GearIcon} from "@radix-ui/react-icons";
|
import { getOrganizationChannels } from "@/db/services/notification-channel";
|
||||||
import {db} from "@/db";
|
import { computeOrganizationPermissions } from "@/lib/acl/organization-acl";
|
||||||
import {isNull} from "drizzle-orm";
|
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 * as drizzleDb from "@/db";
|
||||||
import {eq} from "drizzle-orm";
|
import { eq } from "drizzle-orm";
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: "Settings",
|
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 || !activeMember || !user) {
|
if (!organization || !activeMember || !user) {
|
||||||
notFound();
|
notFound();
|
||||||
}
|
}
|
||||||
|
|
||||||
const notificationChannels = await getOrganizationChannels(organization.id)
|
const notificationChannels = await getOrganizationChannels(organization.id);
|
||||||
const storageChannels = await getOrganizationStorageChannels(organization.id)
|
const storageChannels = await getOrganizationStorageChannels(organization.id);
|
||||||
const permissions = computeOrganizationPermissions(activeMember);
|
const permissions = computeOrganizationPermissions(activeMember);
|
||||||
|
|
||||||
const users = await db.query.user.findMany({
|
const users = await db.query.user.findMany({
|
||||||
where: (fields) => isNull(fields.deletedAt)
|
where: (fields) => isNull(fields.deletedAt),
|
||||||
});
|
});
|
||||||
|
|
||||||
const organizationWithMembers = await db.query.organization.findFirst({
|
const organizationWithMembers = await db.query.organization.findFirst({
|
||||||
where: eq(drizzleDb.schemas.organization.id, organization.id),
|
where: eq(drizzleDb.schemas.organization.id, organization.id),
|
||||||
|
with: {
|
||||||
|
members: {
|
||||||
with: {
|
with: {
|
||||||
members: {
|
user: true,
|
||||||
with: {
|
},
|
||||||
user: true
|
},
|
||||||
}
|
},
|
||||||
}
|
});
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!organizationWithMembers) notFound();
|
if (!organizationWithMembers) notFound();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Page>
|
<Page>
|
||||||
<PageHeader>
|
<PageHeader>
|
||||||
<PageTitle className="flex flex-col md:flex-row items-center justify-between w-full ">
|
<PageTitle className="flex flex-col md:flex-row items-center justify-between w-full ">
|
||||||
<div className="min-w-full md:min-w-fit ">
|
<div className="min-w-full md:min-w-fit ">Organization settings</div>
|
||||||
Organization settings
|
<div className="flex items-center gap-2 md:justify-between w-full ">
|
||||||
</div>
|
<div className="flex items-center gap-2">
|
||||||
<div className="flex items-center gap-2 md:justify-between w-full ">
|
{permissions.canManageSettings &&
|
||||||
<div className="flex items-center gap-2">
|
organization.slug !== "default" && (
|
||||||
{permissions.canManageSettings && organization.slug !== "default" && (
|
<EditOrganizationDialog
|
||||||
<EditOrganizationDialog
|
organization={organizationWithMembers}
|
||||||
organization={organizationWithMembers}
|
users={users}
|
||||||
users={users}
|
currentUser={user}
|
||||||
currentUser={user}
|
/>
|
||||||
/>
|
)}
|
||||||
)}
|
</div>
|
||||||
</div>
|
<div className="flex items-center gap-2">
|
||||||
<div className="flex items-center gap-2">
|
{permissions.canManageDangerZone &&
|
||||||
{permissions.canManageDangerZone && organization.slug !== "default" && (
|
organization.slug !== "default" && (
|
||||||
<DeleteOrganizationButton organizationSlug={organization.slug}/>
|
<DeleteOrganizationButton
|
||||||
)}
|
organizationSlug={organization.slug}
|
||||||
</div>
|
/>
|
||||||
</div>
|
)}
|
||||||
</PageTitle>
|
</div>
|
||||||
</PageHeader>
|
</div>
|
||||||
<PageContent>
|
</PageTitle>
|
||||||
<OrganizationTabs
|
</PageHeader>
|
||||||
activeMember={activeMember}
|
<PageContent>
|
||||||
organization={organization}
|
<OrganizationTabs
|
||||||
notificationChannels={notificationChannels}
|
activeMember={activeMember}
|
||||||
storageChannels={storageChannels}
|
organization={organization}
|
||||||
/>
|
notificationChannels={notificationChannels}
|
||||||
</PageContent>
|
storageChannels={storageChannels}
|
||||||
</Page>
|
/>
|
||||||
)
|
</PageContent>
|
||||||
|
</Page>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,27 +1,26 @@
|
|||||||
import React, {ReactNode} 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";
|
import { ThemeMetaUpdater } from "@/features/browser/theme-meta-updater";
|
||||||
import {BackupModalProvider} from "@/components/wrappers/dashboard/database/backup/backup-modal-context";
|
|
||||||
|
|
||||||
export default async function Layout({children}: { children: 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/>
|
<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,7 +1,10 @@
|
|||||||
import { NextResponse } from "next/server";
|
import {NextResponse} from "next/server";
|
||||||
import { eq } from "drizzle-orm";
|
import {eq} from "drizzle-orm";
|
||||||
import { db } from "@/db";
|
import {db} from "@/db";
|
||||||
import * as drizzleDb from "@/db";
|
import * as drizzleDb from "@/db";
|
||||||
|
import {logger} from "@/lib/logger";
|
||||||
|
|
||||||
|
const log = logger.child({module: "api/agent/backup/helpers"});
|
||||||
|
|
||||||
export function withAgentCheck(handler: Function) {
|
export function withAgentCheck(handler: Function) {
|
||||||
return async (request: Request, context: { params: Promise<{ agentId: string }> }) => {
|
return async (request: Request, context: { params: Promise<{ agentId: string }> }) => {
|
||||||
@@ -14,23 +17,22 @@ export function withAgentCheck(handler: Function) {
|
|||||||
|
|
||||||
if (!agent) {
|
if (!agent) {
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{ error: "Agent not found" },
|
{error: "Agent not found"},
|
||||||
{ status: 404 }
|
{status: 404}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return handler(request, { ...context, agent });
|
return handler(request, {...context, agent});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("Error in agent middleware:", err);
|
log.error({error: err, name: "withAgentCheck"}, "Error in agent middleware");
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{ error: "Internal server error" },
|
{error: "Internal server error"},
|
||||||
{ status: 500 }
|
{status: 500}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export async function getDatabaseOrThrow(generatedId: string) {
|
export async function getDatabaseOrThrow(generatedId: string) {
|
||||||
const database = await db.query.database.findFirst({
|
const database = await db.query.database.findFirst({
|
||||||
where: eq(drizzleDb.schemas.database.agentDatabaseId, generatedId),
|
where: eq(drizzleDb.schemas.database.agentDatabaseId, generatedId),
|
||||||
@@ -43,8 +45,8 @@ export async function getDatabaseOrThrow(generatedId: string) {
|
|||||||
|
|
||||||
if (!database) {
|
if (!database) {
|
||||||
throw NextResponse.json(
|
throw NextResponse.json(
|
||||||
{ error: "Database associated with generatedId not found" },
|
{error: "Database associated with generatedId not found"},
|
||||||
{ status: 404 }
|
{status: 404}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,9 @@ import {withUpdatedAt} from "@/db/utils";
|
|||||||
import {eventEmitter} from "@/features/shared/event";
|
import {eventEmitter} from "@/features/shared/event";
|
||||||
import {sendNotificationsBackupRestore} from "@/features/notifications/helpers";
|
import {sendNotificationsBackupRestore} from "@/features/notifications/helpers";
|
||||||
import {EventKind} from "@/features/notifications/types";
|
import {EventKind} from "@/features/notifications/types";
|
||||||
|
import {logger} from "@/lib/logger";
|
||||||
|
|
||||||
|
const log = logger.child({module: "api/agent/backup/route"});
|
||||||
|
|
||||||
export type BodyPost = {
|
export type BodyPost = {
|
||||||
method: "manual" | "automatic"
|
method: "manual" | "automatic"
|
||||||
@@ -77,7 +80,6 @@ export const POST = withAgentCheck(async (request: Request, {params, agent}: {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
eventEmitter.emit('modification', {update: true});
|
eventEmitter.emit('modification', {update: true});
|
||||||
|
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
@@ -88,7 +90,7 @@ export const POST = withAgentCheck(async (request: Request, {params, agent}: {
|
|||||||
{status: 200}
|
{status: 200}
|
||||||
);
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error in POST for INIT backup:", error);
|
log.error({error: error}, "Error in POST for INIT backup");
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{error: "Internal server error"},
|
{error: "Internal server error"},
|
||||||
{status: 500}
|
{status: 500}
|
||||||
@@ -102,6 +104,7 @@ export const PATCH = withAgentCheck(async (request: Request, {params, agent}: {
|
|||||||
}) => {
|
}) => {
|
||||||
try {
|
try {
|
||||||
const body: BodyPatch = await request.json();
|
const body: BodyPatch = await request.json();
|
||||||
|
log.info({data: body}, "Body from PATH in backup route");
|
||||||
|
|
||||||
const status = body.status
|
const status = body.status
|
||||||
const backupId = body.backupId
|
const backupId = body.backupId
|
||||||
@@ -131,7 +134,7 @@ export const PATCH = withAgentCheck(async (request: Request, {params, agent}: {
|
|||||||
|
|
||||||
|
|
||||||
eventEmitter.emit('modification', {update: true});
|
eventEmitter.emit('modification', {update: true});
|
||||||
await sendNotificationsBackupRestore(database, `${status}_backup` as EventKind);
|
await sendNotificationsBackupRestore(database, status == "failed" ? "error_backup" : "success_backup" as EventKind);
|
||||||
|
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{
|
{
|
||||||
@@ -141,9 +144,7 @@ export const PATCH = withAgentCheck(async (request: Request, {params, agent}: {
|
|||||||
{status: 200}
|
{status: 200}
|
||||||
);
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
log.error({error: error}, "Error in PATCH backup")
|
||||||
|
|
||||||
console.error("Error in PATCH backup:", error);
|
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{error: "Internal server error"},
|
{error: "Internal server error"},
|
||||||
{status: 500}
|
{status: 500}
|
||||||
|
|||||||
@@ -5,6 +5,9 @@ import {db} from "@/db";
|
|||||||
import {getDatabaseOrThrow, withAgentCheck} from "../../helpers";
|
import {getDatabaseOrThrow, withAgentCheck} from "../../helpers";
|
||||||
import {isUuidv4} from "@/utils/verify-uuid";
|
import {isUuidv4} from "@/utils/verify-uuid";
|
||||||
import {eventEmitter} from "@/features/shared/event";
|
import {eventEmitter} from "@/features/shared/event";
|
||||||
|
import {logger} from "@/lib/logger";
|
||||||
|
|
||||||
|
const log = logger.child({module: "api/agent/backup/upload/init"});
|
||||||
|
|
||||||
export type Body = {
|
export type Body = {
|
||||||
generatedId: string
|
generatedId: string
|
||||||
@@ -18,7 +21,7 @@ export const POST = withAgentCheck(async (request: Request, {params, agent}: {
|
|||||||
try {
|
try {
|
||||||
const body: Body = await request.json();
|
const body: Body = await request.json();
|
||||||
|
|
||||||
console.log("body", body);
|
log.info({data: body}, "Body for backup upload init");
|
||||||
|
|
||||||
const generatedId = body.generatedId;
|
const generatedId = body.generatedId;
|
||||||
const storageChannelId = body.storageChannelId;
|
const storageChannelId = body.storageChannelId;
|
||||||
@@ -66,7 +69,7 @@ export const POST = withAgentCheck(async (request: Request, {params, agent}: {
|
|||||||
{status: 200}
|
{status: 200}
|
||||||
);
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error in POST for INIT backup:", error);
|
log.error({error: error}, "Error in POST for INIT backup");
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{error: "Internal server error"},
|
{error: "Internal server error"},
|
||||||
{status: 500}
|
{status: 500}
|
||||||
|
|||||||
@@ -5,6 +5,9 @@ import {db as dbClient, db} from "@/db";
|
|||||||
import {withUpdatedAt} from "@/db/utils";
|
import {withUpdatedAt} from "@/db/utils";
|
||||||
import {getDatabaseOrThrow, withAgentCheck} from "../../helpers";
|
import {getDatabaseOrThrow, withAgentCheck} from "../../helpers";
|
||||||
import {eventEmitter} from "@/features/shared/event";
|
import {eventEmitter} from "@/features/shared/event";
|
||||||
|
import {logger} from "@/lib/logger";
|
||||||
|
|
||||||
|
const log = logger.child({module: "api/agent/backup/upload/status"});
|
||||||
|
|
||||||
export type Body = {
|
export type Body = {
|
||||||
generatedId: string
|
generatedId: string
|
||||||
@@ -27,8 +30,7 @@ export const PATCH = withAgentCheck(async (request: Request, {params, agent}: {
|
|||||||
const backupStorageId = body.backupStorageId;
|
const backupStorageId = body.backupStorageId;
|
||||||
const backupId = body.backupId;
|
const backupId = body.backupId;
|
||||||
|
|
||||||
|
log.info({data: body}, "Body for backup upload status");
|
||||||
console.log("body", body);
|
|
||||||
|
|
||||||
const database = await getDatabaseOrThrow(generatedId);
|
const database = await getDatabaseOrThrow(generatedId);
|
||||||
|
|
||||||
@@ -85,7 +87,7 @@ export const PATCH = withAgentCheck(async (request: Request, {params, agent}: {
|
|||||||
{status: 200}
|
{status: 200}
|
||||||
);
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error in POST for INIT backup:", error);
|
log.error({error: error},"Error in POST for INIT backup");
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{error: "Internal server error"},
|
{error: "Internal server error"},
|
||||||
{status: 500}
|
{status: 500}
|
||||||
|
|||||||
@@ -4,7 +4,9 @@ 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 {sendNotificationsBackupRestore} from "@/features/notifications/helpers";
|
||||||
import {eventEmitter} from "@/features/shared/event";
|
import {logger} from "@/lib/logger";
|
||||||
|
|
||||||
|
const log = logger.child({module: "api/agent/restore"});
|
||||||
|
|
||||||
export type BodyResultRestore = {
|
export type BodyResultRestore = {
|
||||||
generatedId: string
|
generatedId: string
|
||||||
@@ -63,18 +65,16 @@ export async function POST(
|
|||||||
.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");
|
||||||
await sendNotificationsBackupRestore(database, body.status == "error" ? "error_restore" : "success_restore");
|
|
||||||
|
|
||||||
const response = {
|
const response = {
|
||||||
message: true,
|
status: true,
|
||||||
details: "Restoration successfully updated"
|
message: "Restoration successfully updated"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
return Response.json(response, {status: 200})
|
return Response.json(response, {status: 200})
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error in POST handler:', error);
|
log.error({error: error}, "Error in POST handler")
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{error: 'Internal server error'},
|
{error: 'Internal server error'},
|
||||||
{status: 500}
|
{status: 500}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ 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 {Agent} from "@/db/schema/08_agent";
|
import {Agent} from "@/db/schema/08_agent";
|
||||||
import {Database, DatabaseWith} from "@/db/schema/07_database";
|
import {DatabaseWith} from "@/db/schema/07_database";
|
||||||
import * as drizzleDb from "@/db";
|
import * as drizzleDb from "@/db";
|
||||||
import {db, db as dbClient} from "@/db";
|
import {db, db as dbClient} from "@/db";
|
||||||
import {and, eq, inArray} from "drizzle-orm";
|
import {and, eq, inArray} from "drizzle-orm";
|
||||||
@@ -11,6 +11,9 @@ import {withUpdatedAt} from "@/db/utils";
|
|||||||
import type {StorageInput} from "@/features/storages/types";
|
import type {StorageInput} from "@/features/storages/types";
|
||||||
import {dispatchStorage} from "@/features/storages/dispatch";
|
import {dispatchStorage} from "@/features/storages/dispatch";
|
||||||
import {Setting} from "@/db/schema/01_setting";
|
import {Setting} from "@/db/schema/01_setting";
|
||||||
|
import {logger} from "@/lib/logger";
|
||||||
|
|
||||||
|
const log = logger.child({module: "api/agent/status/helpers"});
|
||||||
|
|
||||||
export async function handleDatabases(body: Body, agent: Agent, lastContact: Date, settings: Setting) {
|
export async function handleDatabases(body: Body, agent: Agent, lastContact: Date, settings: Setting) {
|
||||||
const databasesResponse = [];
|
const databasesResponse = [];
|
||||||
@@ -56,7 +59,7 @@ export async function handleDatabases(body: Body, agent: Agent, lastContact: Dat
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!dbmsEnumSchema.safeParse(db.dbms).success) {
|
if (!dbmsEnumSchema.safeParse(db.dbms).success) {
|
||||||
console.log(`Database type not available: ${db.dbms}`);
|
log.error({name: "handleDatabases"},`Database type not available: ${db.dbms}`);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -67,12 +70,24 @@ export async function handleDatabases(body: Body, agent: Agent, lastContact: Dat
|
|||||||
name: db.name,
|
name: db.name,
|
||||||
dbms: db.dbms as EDbmsSchema,
|
dbms: db.dbms as EDbmsSchema,
|
||||||
agentDatabaseId: db.generatedId,
|
agentDatabaseId: db.generatedId,
|
||||||
lastContact: lastContact,
|
lastContact: db.pingStatus ? lastContact : null,
|
||||||
|
healthErrorCount: null
|
||||||
})
|
})
|
||||||
.returning();
|
.returning();
|
||||||
|
|
||||||
|
|
||||||
if (databaseCreated) {
|
if (databaseCreated) {
|
||||||
|
|
||||||
|
|
||||||
|
await dbClient
|
||||||
|
.insert(drizzleDb.schemas.healthcheckLog)
|
||||||
|
.values({
|
||||||
|
kind: "database",
|
||||||
|
status: db.pingStatus ? "success" : "failed",
|
||||||
|
objectId: databaseCreated.id,
|
||||||
|
date: lastContact
|
||||||
|
})
|
||||||
|
|
||||||
const storages = await getDatabaseStorageChannels(databaseCreated.id)
|
const storages = await getDatabaseStorageChannels(databaseCreated.id)
|
||||||
|
|
||||||
databasesResponse.push(formatDatabase(databaseCreated, backupAction, restoreAction, urlBackup, storages, null));
|
databasesResponse.push(formatDatabase(databaseCreated, backupAction, restoreAction, urlBackup, storages, null));
|
||||||
@@ -84,11 +99,24 @@ export async function handleDatabases(body: Body, agent: Agent, lastContact: Dat
|
|||||||
.set(withUpdatedAt({
|
.set(withUpdatedAt({
|
||||||
name: db.name,
|
name: db.name,
|
||||||
agentId: agent.id,
|
agentId: agent.id,
|
||||||
lastContact: lastContact
|
dbms: db.dbms as EDbmsSchema,
|
||||||
|
lastContact: db.pingStatus ? lastContact : existingDatabase.lastContact,
|
||||||
|
healthErrorCount: db.pingStatus ? null : existingDatabase.healthErrorCount,
|
||||||
}))
|
}))
|
||||||
.where(eq(drizzleDb.schemas.database.id, existingDatabase.id))
|
.where(eq(drizzleDb.schemas.database.id, existingDatabase.id))
|
||||||
.returning();
|
.returning();
|
||||||
|
|
||||||
|
|
||||||
|
await dbClient
|
||||||
|
.insert(drizzleDb.schemas.healthcheckLog)
|
||||||
|
.values({
|
||||||
|
kind: "database",
|
||||||
|
status: db.pingStatus ? "success" : "failed",
|
||||||
|
objectId: databaseUpdated.id,
|
||||||
|
date: lastContact
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
const activeBackup = await dbClient.query.backup.findFirst({
|
const activeBackup = await dbClient.query.backup.findFirst({
|
||||||
where: and(
|
where: and(
|
||||||
eq(drizzleDb.schemas.backup.databaseId, databaseUpdated.id),
|
eq(drizzleDb.schemas.backup.databaseId, databaseUpdated.id),
|
||||||
@@ -159,11 +187,11 @@ export async function handleDatabases(body: Body, agent: Agent, lastContact: Dat
|
|||||||
.where(eq(drizzleDb.schemas.restoration.id, restoration.id));
|
.where(eq(drizzleDb.schemas.restoration.id, restoration.id));
|
||||||
|
|
||||||
const errorMessage = "Failed to get backup URL";
|
const errorMessage = "Failed to get backup URL";
|
||||||
console.error("Restoration failed: ", errorMessage);
|
log.error({error: errorMessage, name: "handleDatabases"}, "Restoration failed");
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("Restoration crashed unexpectedly:", err);
|
log.error({error: err, name: "handleDatabases"}, "Restoration crashed unexpectedly");
|
||||||
await dbClient
|
await dbClient
|
||||||
.update(drizzleDb.schemas.restoration)
|
.update(drizzleDb.schemas.restoration)
|
||||||
.set({status: "failed"})
|
.set({status: "failed"})
|
||||||
@@ -180,7 +208,6 @@ export async function handleDatabases(body: Body, agent: Agent, lastContact: Dat
|
|||||||
databasesResponse.push(formatDatabase(databaseUpdated, backupAction, restoreAction, urlBackup, storages, urlMeta));
|
databasesResponse.push(formatDatabase(databaseUpdated, backupAction, restoreAction, urlBackup, storages, urlMeta));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return databasesResponse;
|
return databasesResponse;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,13 +6,17 @@ 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 {withUpdatedAt} from "@/db/utils";
|
||||||
import {eventEmitter} from "@/features/shared/event";
|
import {logger} from "@/lib/logger";
|
||||||
import {notFound} from "next/navigation";
|
|
||||||
|
|
||||||
|
const log = logger.child({module: "api/agent/status/route"});
|
||||||
|
|
||||||
|
|
||||||
export type databaseAgent = {
|
export type databaseAgent = {
|
||||||
name: string,
|
name: string,
|
||||||
dbms: EDbmsSchema,
|
dbms: EDbmsSchema,
|
||||||
generatedId: string
|
generatedId: string
|
||||||
|
pingStatus: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export type Body = {
|
export type Body = {
|
||||||
@@ -27,14 +31,14 @@ export async function POST(
|
|||||||
) {
|
) {
|
||||||
try {
|
try {
|
||||||
const agentId = (await params).agentId
|
const agentId = (await params).agentId
|
||||||
console.log(agentId)
|
log.info(`Agent ID: ${agentId}`)
|
||||||
const body: Body = await request.json();
|
const body: Body = await request.json();
|
||||||
const lastContact = new Date();
|
const lastContact = new Date();
|
||||||
let message: string
|
let message: string
|
||||||
|
|
||||||
if (!isUuidv4(agentId)) {
|
if (!isUuidv4(agentId)) {
|
||||||
message = "agentId is not a valid uuid"
|
message = "agentId is not a valid uuid"
|
||||||
console.error(message)
|
log.error({error: message}, "An error occurred")
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{error: "agentId is not a valid uuid"},
|
{error: "agentId is not a valid uuid"},
|
||||||
{status: 500}
|
{status: 500}
|
||||||
@@ -55,17 +59,25 @@ export async function POST(
|
|||||||
return NextResponse.json({error: "An error occured"}, {status: 404})
|
return NextResponse.json({error: "An error occured"}, {status: 404})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
const databasesResponse = await handleDatabases(body, agent, lastContact, settings)
|
const databasesResponse = await handleDatabases(body, agent, lastContact, settings)
|
||||||
|
|
||||||
await db
|
await db
|
||||||
.update(drizzleDb.schemas.agent)
|
.update(drizzleDb.schemas.agent)
|
||||||
.set(withUpdatedAt({
|
.set(withUpdatedAt({
|
||||||
version: body.version,
|
version: body.version,
|
||||||
lastContact: lastContact
|
lastContact: lastContact,
|
||||||
|
healthErrorCount: null
|
||||||
}))
|
}))
|
||||||
.where(eq(drizzleDb.schemas.agent.id, agentId));
|
.where(eq(drizzleDb.schemas.agent.id, agentId));
|
||||||
|
|
||||||
|
await db
|
||||||
|
.insert(drizzleDb.schemas.healthcheckLog)
|
||||||
|
.values({
|
||||||
|
kind: "agent",
|
||||||
|
status: "success",
|
||||||
|
objectId: agentId,
|
||||||
|
date: lastContact
|
||||||
|
})
|
||||||
|
|
||||||
const response = {
|
const response = {
|
||||||
agent: {
|
agent: {
|
||||||
@@ -75,10 +87,9 @@ export async function POST(
|
|||||||
databases: databasesResponse
|
databases: databasesResponse
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
return Response.json(response)
|
return Response.json(response)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error in POST handler:', error);
|
log.error({error: error}, "Error in POST handler")
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{error: 'Internal server error'},
|
{error: 'Internal server error'},
|
||||||
{status: 500}
|
{status: 500}
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import {NextResponse} from "next/server";
|
import { NextResponse } from "next/server";
|
||||||
|
|
||||||
export async function GET() {
|
export async function GET() {
|
||||||
return NextResponse.json({
|
return NextResponse.json({
|
||||||
PROJECT_URL: process.env.PROJECT_URL,
|
PROJECT_URL: process.env.PROJECT_URL,
|
||||||
PROJECT_NAME: process.env.PROJECT_NAME,
|
PROJECT_NAME: process.env.PROJECT_NAME,
|
||||||
PROJECT_DESCRIPTION: process.env.PROJECT_DESCRIPTION,
|
PROJECT_DESCRIPTION: process.env.PROJECT_DESCRIPTION,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,9 @@ import {auth} from "@/lib/auth/auth";
|
|||||||
import {headers} from "next/headers";
|
import {headers} from "next/headers";
|
||||||
import {NextResponse} from "next/server";
|
import {NextResponse} from "next/server";
|
||||||
import {eventEmitter} from "@/features/shared/event";
|
import {eventEmitter} from "@/features/shared/event";
|
||||||
|
import {logger} from "@/lib/logger";
|
||||||
|
|
||||||
|
const log = logger.child({module: "api/events"});
|
||||||
|
|
||||||
export async function GET(request: Request) {
|
export async function GET(request: Request) {
|
||||||
|
|
||||||
@@ -16,9 +19,9 @@ export async function GET(request: Request) {
|
|||||||
return new Response(
|
return new Response(
|
||||||
new ReadableStream({
|
new ReadableStream({
|
||||||
start(controller) {
|
start(controller) {
|
||||||
console.log('Stream started');
|
log.info("Stream started");
|
||||||
const handleModification = (data: any) => {
|
const handleModification = (data: any) => {
|
||||||
console.log('Modification event triggered:', data);
|
log.info({data: data},"Modification event triggered");
|
||||||
controller.enqueue(`event: modification\n`);
|
controller.enqueue(`event: modification\n`);
|
||||||
controller.enqueue(`data: ${JSON.stringify(data)}\n\n`);
|
controller.enqueue(`data: ${JSON.stringify(data)}\n\n`);
|
||||||
};
|
};
|
||||||
@@ -26,7 +29,7 @@ export async function GET(request: Request) {
|
|||||||
eventEmitter.on('modification', handleModification);
|
eventEmitter.on('modification', handleModification);
|
||||||
|
|
||||||
request.signal.addEventListener('abort', () => {
|
request.signal.addEventListener('abort', () => {
|
||||||
console.log('Client disconnected');
|
log.info("Client disconnected");
|
||||||
controller.close();
|
controller.close();
|
||||||
eventEmitter.off('modification', handleModification);
|
eventEmitter.off('modification', handleModification);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -3,6 +3,9 @@ import path from "path";
|
|||||||
import type {StorageInput} from "@/features/storages/types";
|
import type {StorageInput} from "@/features/storages/types";
|
||||||
import {dispatchStorage} from "@/features/storages/dispatch";
|
import {dispatchStorage} from "@/features/storages/dispatch";
|
||||||
import {Readable} from "node:stream";
|
import {Readable} from "node:stream";
|
||||||
|
import {logger} from "@/lib/logger";
|
||||||
|
|
||||||
|
const log = logger.child({module: "api/files/backups"});
|
||||||
|
|
||||||
export async function GET(
|
export async function GET(
|
||||||
request: Request,
|
request: Request,
|
||||||
@@ -30,7 +33,7 @@ export async function GET(
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
console.debug(input);
|
log.info({input: input}, "Dispatch Storage");
|
||||||
|
|
||||||
const result = await dispatchStorage(input, undefined, storageId);
|
const result = await dispatchStorage(input, undefined, storageId);
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
import {NextResponse} from "next/server";
|
import {NextResponse} from "next/server";
|
||||||
import {auth} from "@/lib/auth/auth";
|
import {auth} from "@/lib/auth/auth";
|
||||||
import {headers} from "next/headers";
|
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 {StorageInput} from "@/features/storages/types";
|
||||||
import {dispatchStorage} from "@/features/storages/dispatch";
|
import {dispatchStorage} from "@/features/storages/dispatch";
|
||||||
import {Readable} from "node:stream";
|
import {Readable} from "node:stream";
|
||||||
|
import {logger} from "@/lib/logger";
|
||||||
|
|
||||||
|
const log = logger.child({module: "api/files/images"});
|
||||||
|
|
||||||
export async function GET(
|
export async function GET(
|
||||||
req: Request,
|
req: Request,
|
||||||
@@ -56,7 +56,7 @@ export async function GET(
|
|||||||
const result = await dispatchStorage(input, undefined, storageId);
|
const result = await dispatchStorage(input, undefined, storageId);
|
||||||
|
|
||||||
if (!result.file || !(result.file instanceof Readable)) {
|
if (!result.file || !(result.file instanceof Readable)) {
|
||||||
console.error(`An error occurred while getting file :`, result);
|
log.error({error: result}, `An error occurred while getting file`);
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{error: "Invalid file payload"},
|
{error: "Invalid file payload"},
|
||||||
{status: 500}
|
{status: 500}
|
||||||
@@ -82,7 +82,7 @@ export async function GET(
|
|||||||
});
|
});
|
||||||
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("Error streaming image:", err);
|
log.error({error: err}, `Error streaming image`);
|
||||||
return NextResponse.json({error: "Error fetching file"}, {status: 500});
|
return NextResponse.json({error: "Error fetching file"}, {status: 500});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2,7 +2,9 @@ import {NextResponse} from "next/server";
|
|||||||
import fs from "fs";
|
import fs from "fs";
|
||||||
import path from "path";
|
import path from "path";
|
||||||
import {env} from "@/env.mjs";
|
import {env} from "@/env.mjs";
|
||||||
|
import {logger} from "@/lib/logger";
|
||||||
|
|
||||||
|
const log = logger.child({module: "api/tus/hooks"});
|
||||||
|
|
||||||
export async function POST(request: Request) {
|
export async function POST(request: Request) {
|
||||||
try {
|
try {
|
||||||
@@ -13,7 +15,7 @@ export async function POST(request: Request) {
|
|||||||
const uploadOffset = headers["Upload-Offset"]?.[0];
|
const uploadOffset = headers["Upload-Offset"]?.[0];
|
||||||
const status = headers["X-Status"]?.[0];
|
const status = headers["X-Status"]?.[0];
|
||||||
|
|
||||||
console.log(`Upload ID : ${event.Upload.ID} (${uploadOffset}/${uploadLength})`);
|
log.info(`Upload ID : ${event.Upload.ID} (${uploadOffset}/${uploadLength})`);
|
||||||
|
|
||||||
if (status === "success") {
|
if (status === "success") {
|
||||||
if (
|
if (
|
||||||
@@ -29,9 +31,7 @@ export async function POST(request: Request) {
|
|||||||
return NextResponse.json({error: "Missing X-File-Path"}, {status: 500});
|
return NextResponse.json({error: "Missing X-File-Path"}, {status: 500});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const uploadDir = path.join(env.PRIVATE_PATH!, "/uploads/");
|
||||||
// const uploadDir = path.join(process.cwd(), "/private/uploads/");
|
|
||||||
const uploadDir = path.join(env.PRIVATE_PATH, "/uploads/");
|
|
||||||
|
|
||||||
const oldFilePath = path.join(uploadDir, "tmp", id);
|
const oldFilePath = path.join(uploadDir, "tmp", id);
|
||||||
const newFilePath = path.join(uploadDir, filePath);
|
const newFilePath = path.join(uploadDir, filePath);
|
||||||
@@ -74,7 +74,7 @@ export async function POST(request: Request) {
|
|||||||
}
|
}
|
||||||
return NextResponse.json({});
|
return NextResponse.json({});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Hook error:", error);
|
log.error({error: error},"TUS Hook error");
|
||||||
return NextResponse.json({error: "Internal server error"}, {status: 500});
|
return NextResponse.json({error: "Internal server error"}, {status: 500});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+30
-82
@@ -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>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
+33
-29
@@ -1,38 +1,42 @@
|
|||||||
import React from "react";
|
import type { Metadata } from "next";
|
||||||
import type {Metadata} from "next";
|
import type React from "react";
|
||||||
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 {author, geistMono, poppins} from "@/fonts/fonts";
|
import { Providers } from "./providers";
|
||||||
|
|
||||||
const title = process.env.PROJECT_NAME ?? "App Title";
|
const title = process.env.PROJECT_NAME ?? "Portabase";
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: {
|
title: {
|
||||||
default: title,
|
default: title,
|
||||||
template: `%s - ${title}`
|
template: `%s - ${title}`,
|
||||||
},
|
},
|
||||||
description: process.env.PROJECT_DESCRIPTION ?? undefined,
|
description: process.env.PROJECT_DESCRIPTION ?? undefined,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default async 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={title} />
|
||||||
<meta name="apple-mobile-web-app-title" content={title}/>
|
</head>
|
||||||
</head>
|
<body
|
||||||
<body className={cn(poppins.variable, author.variable, geistMono.variable, "font-sans h-full")}>
|
className={cn(
|
||||||
<ConsoleSilencer/>
|
poppins.variable,
|
||||||
<Providers>
|
author.variable,
|
||||||
{children}
|
geistMono.variable,
|
||||||
</Providers>
|
"font-sans h-full",
|
||||||
</body>
|
)}
|
||||||
</html>
|
>
|
||||||
);
|
<ConsoleSilencer />
|
||||||
|
<Providers>{children}</Providers>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
+14
-10
@@ -1,15 +1,19 @@
|
|||||||
import BackButton from "@/components/wrappers/common/button/back-button";
|
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>
|
||||||
<BackButton>Go home</BackButton>
|
<p className="leading-7 not-first:mt-6">
|
||||||
</div>
|
The content you are trying to view is not available.
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
)
|
<BackButton>Go home</BackButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
+19
-21
@@ -1,30 +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 {QueryClient, QueryClientProvider} from "@tanstack/react-query";
|
|
||||||
import {ThemeMetaUpdaterRoot} from "@/features/browser/theme-meta-updater-root";
|
|
||||||
|
|
||||||
|
import { Toaster } from "@/components/ui/sonner";
|
||||||
|
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
|
<ThemeProvider attribute="class" defaultTheme="system" enableSystem>
|
||||||
attribute="class"
|
<ThemeMetaUpdaterRoot />
|
||||||
defaultTheme="system"
|
<QueryClientProvider client={queryClient}>
|
||||||
enableSystem
|
<ErrorLayout>
|
||||||
>
|
<Toaster />
|
||||||
<ThemeMetaUpdaterRoot/>
|
{props.children}
|
||||||
<QueryClientProvider client={queryClient}>
|
</ErrorLayout>
|
||||||
<Toaster/>
|
</QueryClientProvider>
|
||||||
{props.children}
|
</ThemeProvider>
|
||||||
</QueryClientProvider>
|
</Suspense>
|
||||||
</ThemeProvider>
|
);
|
||||||
</Suspense>
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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:
|
||||||
@@ -4,7 +4,7 @@ services:
|
|||||||
# context: .
|
# context: .
|
||||||
# dockerfile: docker/dockerfile/Dockerfile
|
# dockerfile: docker/dockerfile/Dockerfile
|
||||||
# target: prod
|
# target: prod
|
||||||
image: portabase/portabase:latest
|
image: portabase/portabase:1.7.1
|
||||||
ports:
|
ports:
|
||||||
- '8887:80'
|
- '8887:80'
|
||||||
environment:
|
environment:
|
||||||
|
|||||||
+2
-1
@@ -12,7 +12,7 @@ services:
|
|||||||
- POSTGRES_USER=devuser
|
- POSTGRES_USER=devuser
|
||||||
- POSTGRES_PASSWORD=changeme
|
- POSTGRES_PASSWORD=changeme
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: [ "CMD-SHELL", "pg_isready -U devuser -d devdb" ]
|
test: ["CMD-SHELL", "pg_isready -U devuser -d devdb"]
|
||||||
interval: 10s
|
interval: 10s
|
||||||
timeout: 5s
|
timeout: 5s
|
||||||
retries: 5
|
retries: 5
|
||||||
@@ -30,6 +30,7 @@ services:
|
|||||||
- "localhost:host-gateway"
|
- "localhost:host-gateway"
|
||||||
volumes:
|
volumes:
|
||||||
- ./private/uploads/tmp:/data/uploads/tmp
|
- ./private/uploads/tmp:/data/uploads/tmp
|
||||||
|
user: "1000:1000"
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
postgres-data:
|
postgres-data:
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
#FROM node:22-bullseye AS base
|
|
||||||
FROM --platform=$BUILDPLATFORM node:22-bullseye AS base
|
FROM --platform=$BUILDPLATFORM node:22-bullseye AS base
|
||||||
|
|
||||||
RUN apt-get update && apt-get install -y \
|
RUN apt-get update && apt-get install -y \
|
||||||
@@ -112,8 +111,6 @@ RUN chmod +x /usr/local/bin/tusd
|
|||||||
|
|
||||||
COPY docker/nginx/nginx.conf /etc/nginx/nginx.conf
|
COPY docker/nginx/nginx.conf /etc/nginx/nginx.conf
|
||||||
|
|
||||||
#RUN mkdir -p .next /app/private/uploads \
|
|
||||||
# && chown -R nextjs:nodejs .next /app/private /app/public
|
|
||||||
|
|
||||||
RUN mkdir -p .next /data/private/uploads \
|
RUN mkdir -p .next /data/private/uploads \
|
||||||
&& chown -R nextjs:nodejs .next /data/private /app/public
|
&& chown -R nextjs:nodejs .next /data/private /app/public
|
||||||
|
|||||||
@@ -0,0 +1,135 @@
|
|||||||
|
import {test, expect, Page} from '@playwright/test';
|
||||||
|
|
||||||
|
|
||||||
|
type UserCredentials = {
|
||||||
|
username: string
|
||||||
|
email: string
|
||||||
|
password: string
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const users: Record<string, UserCredentials> = {
|
||||||
|
admin: {username: 'Admin', email: 'admin@example.com', password: 'testPASS123456!'},
|
||||||
|
normal: {username: 'John Doe', email: 'john.doe@example.com', password: 'testPASS123456!'},
|
||||||
|
}
|
||||||
|
|
||||||
|
async function register(page: Page, name: string, email: string, password: string, confirmPassword: string) {
|
||||||
|
await page.locator('input[name="name"]').fill(name)
|
||||||
|
await page.locator('input[name="email"]').fill(email)
|
||||||
|
await page.locator('input[name="password"]').fill(password)
|
||||||
|
await page.locator('input[name="confirmPassword"]').fill(confirmPassword)
|
||||||
|
|
||||||
|
await page.click('button[type="submit"]')
|
||||||
|
}
|
||||||
|
|
||||||
|
async function login(page: Page, email: string, password: string) {
|
||||||
|
await page.locator('input[name="email"]').fill(email)
|
||||||
|
await page.locator('input[name="password"]').fill(password)
|
||||||
|
|
||||||
|
await page.locator('button:has-text("Login")').click()
|
||||||
|
}
|
||||||
|
|
||||||
|
async function logout() {
|
||||||
|
//TODO
|
||||||
|
}
|
||||||
|
|
||||||
|
test.describe.serial('User signup and login flows', () => {
|
||||||
|
|
||||||
|
test('Redirect to login if not connected', async ({page}) => {
|
||||||
|
await page.goto('/dashboard/projects');
|
||||||
|
await expect(page).toHaveURL("login?redirect=%2Fdashboard%2Fprojects");
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Password too short', async ({page}) => {
|
||||||
|
await page.goto('')
|
||||||
|
await page.click('text=Sign up')
|
||||||
|
await expect(page).toHaveURL('/register')
|
||||||
|
|
||||||
|
let password = '123456'
|
||||||
|
await register(page, users["admin"].username, users["admin"].email, password, password)
|
||||||
|
|
||||||
|
const toast = page.locator('text=Must have at least 8 character')
|
||||||
|
await expect(toast).toBeVisible()
|
||||||
|
})
|
||||||
|
|
||||||
|
test('Password too simple', async ({page}) => {
|
||||||
|
await page.goto('')
|
||||||
|
await page.click('text=Sign up')
|
||||||
|
await expect(page).toHaveURL('/register')
|
||||||
|
|
||||||
|
let password = '12345678'
|
||||||
|
await register(page, users["admin"].username, users["admin"].email, password, password)
|
||||||
|
|
||||||
|
const toast = page.locator('text=Your password must contain at least one uppercase letter, one lowercase letter, one number, and one special character.')
|
||||||
|
await expect(toast).toBeVisible()
|
||||||
|
})
|
||||||
|
|
||||||
|
test('Password and confirm password mismatch', async ({page}) => {
|
||||||
|
await page.goto('/')
|
||||||
|
await page.click('text=Sign up')
|
||||||
|
await expect(page).toHaveURL('/register')
|
||||||
|
|
||||||
|
let password = 'testPASS123456!'
|
||||||
|
let confirmPassword = 'testPASS123456!!'
|
||||||
|
await register(page, users["admin"].username, users["admin"].email, password, confirmPassword)
|
||||||
|
|
||||||
|
const toast = page.locator('text=The passwords did not match')
|
||||||
|
await expect(toast).toBeVisible()
|
||||||
|
})
|
||||||
|
|
||||||
|
test('Successful register for admin', async ({page}) => {
|
||||||
|
await page.goto('/')
|
||||||
|
|
||||||
|
await page.click('text=Sign up')
|
||||||
|
await expect(page).toHaveURL('/register')
|
||||||
|
|
||||||
|
await register(page, users["admin"].username, users["admin"].email, users["admin"].password, users["admin"].password)
|
||||||
|
|
||||||
|
await expect(page).toHaveURL('/login')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('User already exists.', async ({page}) => {
|
||||||
|
await page.goto('/')
|
||||||
|
|
||||||
|
await page.click('text=Sign up')
|
||||||
|
await expect(page).toHaveURL('/register')
|
||||||
|
|
||||||
|
await register(page, users["admin"].username, users["admin"].email, users["admin"].password, users["admin"].password)
|
||||||
|
|
||||||
|
const toast = page.locator('text=User already exists. Use another email.')
|
||||||
|
await expect(toast).toBeVisible()
|
||||||
|
})
|
||||||
|
|
||||||
|
test('Successful register for normal', async ({page}) => {
|
||||||
|
await page.goto('/')
|
||||||
|
|
||||||
|
await page.click('text=Sign up')
|
||||||
|
await expect(page).toHaveURL('/register')
|
||||||
|
|
||||||
|
await register(page, users["normal"].username, users["normal"].email, users["normal"].password, users["normal"].password)
|
||||||
|
|
||||||
|
await expect(page).toHaveURL('login')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('Failed login because account not active', async ({page}) => {
|
||||||
|
await page.goto('/login')
|
||||||
|
await login(page, users["normal"].email, users["normal"].password)
|
||||||
|
|
||||||
|
const toast = page.locator('text=Your account is not active.')
|
||||||
|
await expect(toast).toBeVisible()
|
||||||
|
})
|
||||||
|
|
||||||
|
test('Successful login', async ({page}) => {
|
||||||
|
await page.goto('login')
|
||||||
|
await login(page, users["admin"].email, users["admin"].password)
|
||||||
|
|
||||||
|
await expect(page).toHaveURL('/dashboard/home')
|
||||||
|
})
|
||||||
|
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
// test('Successful logout', async ({page}) => {
|
||||||
|
// await logout()
|
||||||
|
// await expect(page).toHaveURL('/login')
|
||||||
|
// })
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
.DS_Store
|
||||||
|
.git/
|
||||||
|
.gitignore
|
||||||
|
.bzr/
|
||||||
|
.bzrignore
|
||||||
|
.hg/
|
||||||
|
.hgignore
|
||||||
|
.svn/
|
||||||
|
*.swp
|
||||||
|
*.bak
|
||||||
|
*.tmp
|
||||||
|
*.orig
|
||||||
|
*~
|
||||||
|
.project
|
||||||
|
.idea/
|
||||||
|
*.tmproj
|
||||||
|
.vscode/
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
apiVersion: v2
|
||||||
|
name: portabase
|
||||||
|
description: Helm chart for Portabase
|
||||||
|
type: application
|
||||||
|
version: 0.0.0
|
||||||
|
appVersion: "latest"
|
||||||
|
keywords:
|
||||||
|
- postgresql
|
||||||
|
- mariadb
|
||||||
|
- mongodb
|
||||||
|
- mysql
|
||||||
|
- sqlite
|
||||||
|
- backup
|
||||||
|
- database
|
||||||
|
- restore
|
||||||
|
home: https://github.com/Portabase/portabase
|
||||||
|
|
||||||
|
sources:
|
||||||
|
- https://github.com/Portabase/portabase
|
||||||
|
- https://github.com/Portabase/portabase/tree/main/helm
|
||||||
|
|
||||||
|
maintainers:
|
||||||
|
- name: Charles Gauthereau
|
||||||
|
url: https://github.com/RambokDev
|
||||||
|
- name: Killian Larcher
|
||||||
|
url: https://github.com/KillianLarcher
|
||||||
|
|
||||||
|
icon: https://raw.githubusercontent.com/Portabase/portabase/main/.github/assets/logo.png
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
# Development Notes
|
||||||
|
|
||||||
|
## Check that Kubernetes is reachable locally
|
||||||
|
|
||||||
|
```bash
|
||||||
|
kubectl get nodes
|
||||||
|
```
|
||||||
|
|
||||||
|
## Install the local Portabase Helm chart
|
||||||
|
|
||||||
|
```bash
|
||||||
|
helm install portabase . \
|
||||||
|
--set project.secret=$(openssl rand -hex 32)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Check the pods
|
||||||
|
|
||||||
|
```bash
|
||||||
|
kubectl get pods
|
||||||
|
```
|
||||||
|
|
||||||
|
## Check the services
|
||||||
|
|
||||||
|
```bash
|
||||||
|
kubectl get svc
|
||||||
|
```
|
||||||
|
|
||||||
|
## If the service type is ClusterIP, expose it locally using port forwarding:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
kubectl port-forward svc/portabase 8887:80
|
||||||
|
```
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
metadata:
|
||||||
|
name: portabase
|
||||||
|
spec:
|
||||||
|
replicas: 1
|
||||||
|
selector:
|
||||||
|
matchLabels:
|
||||||
|
app: portabase
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
app: portabase
|
||||||
|
spec:
|
||||||
|
containers:
|
||||||
|
- name: portabase
|
||||||
|
image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
|
||||||
|
ports:
|
||||||
|
- containerPort: 80
|
||||||
|
env:
|
||||||
|
- name: TZ
|
||||||
|
value: {{ .Values.timezone }}
|
||||||
|
- name: PROJECT_URL
|
||||||
|
value: {{ .Values.project.url }}
|
||||||
|
- name: PROJECT_SECRET
|
||||||
|
value: {{ .Values.project.secret }}
|
||||||
|
volumeMounts:
|
||||||
|
- name: data
|
||||||
|
mountPath: /data
|
||||||
|
volumes:
|
||||||
|
- name: data
|
||||||
|
persistentVolumeClaim:
|
||||||
|
claimName: portabase-data
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: PersistentVolumeClaim
|
||||||
|
metadata:
|
||||||
|
name: portabase-data
|
||||||
|
spec:
|
||||||
|
accessModes:
|
||||||
|
- ReadWriteOnce
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
storage: {{ .Values.persistence.size }}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: Service
|
||||||
|
metadata:
|
||||||
|
name: portabase
|
||||||
|
spec:
|
||||||
|
type: {{ .Values.service.type }}
|
||||||
|
selector:
|
||||||
|
app: portabase
|
||||||
|
ports:
|
||||||
|
- port: {{ .Values.service.port }}
|
||||||
|
targetPort: 80
|
||||||
|
nodePort: {{ .Values.service.nodePort }}
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
image:
|
||||||
|
repository: portabase/portabase
|
||||||
|
tag: latest
|
||||||
|
pullPolicy: Always
|
||||||
|
|
||||||
|
replicaCount: 1
|
||||||
|
|
||||||
|
service:
|
||||||
|
type: ClusterIP
|
||||||
|
port: 80
|
||||||
|
targetPort: 80
|
||||||
|
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
memory: "1Gi"
|
||||||
|
cpu: "1"
|
||||||
|
limits:
|
||||||
|
memory: "1Gi"
|
||||||
|
cpu: "1"
|
||||||
|
|
||||||
|
timezone: Europe/Paris
|
||||||
|
|
||||||
|
project:
|
||||||
|
url: http://localhost:8887
|
||||||
|
secret: "change_me_generate_secure_secret"
|
||||||
|
|
||||||
|
persistence:
|
||||||
|
enabled: true
|
||||||
|
storageClassName: ""
|
||||||
|
accessMode: ReadWriteOnce
|
||||||
|
size: 10Gi
|
||||||
|
mountPath: /data
|
||||||
|
|
||||||
|
ingress:
|
||||||
|
enabled: false
|
||||||
|
className: nginx
|
||||||
|
hosts:
|
||||||
|
- host: portabase.example.com
|
||||||
|
paths:
|
||||||
|
- path: /
|
||||||
|
pathType: Prefix
|
||||||
|
tls: []
|
||||||
|
|
||||||
|
updateStrategy:
|
||||||
|
type: RollingUpdate
|
||||||
|
rollingUpdate:
|
||||||
|
partition: 0
|
||||||
@@ -43,6 +43,9 @@ const nextConfig: NextConfig = {
|
|||||||
typescript: {
|
typescript: {
|
||||||
ignoreBuildErrors: true,
|
ignoreBuildErrors: true,
|
||||||
},
|
},
|
||||||
|
logging: {
|
||||||
|
browserToTerminal: false,
|
||||||
|
},
|
||||||
experimental: {
|
experimental: {
|
||||||
serverActions: {
|
serverActions: {
|
||||||
bodySizeLimit: "10gb",
|
bodySizeLimit: "10gb",
|
||||||
|
|||||||
+37
-30
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "portabase",
|
"name": "portabase",
|
||||||
"version": "1.2.6",
|
"version": "1.9.5",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "next dev --turbopack -p 8887",
|
"dev": "next dev --turbopack -p 8887",
|
||||||
@@ -11,9 +11,12 @@
|
|||||||
"db:generate": "drizzle-kit generate",
|
"db:generate": "drizzle-kit generate",
|
||||||
"db:migrate": "drizzle-kit migrate",
|
"db:migrate": "drizzle-kit migrate",
|
||||||
"db:drop": "drizzle-kit drop",
|
"db:drop": "drizzle-kit drop",
|
||||||
"auth:generate": "npx @better-auth/cli generate --config ./src/lib/auth/auth.ts"
|
"auth:generate": "npx @better-auth/cli generate --config ./src/lib/auth/auth.ts",
|
||||||
|
"release": "release-it"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@better-auth/passkey": "^1.5.6",
|
||||||
|
"@better-auth/sso": "^1.5.6",
|
||||||
"@hookform/resolvers": "^5.2.2",
|
"@hookform/resolvers": "^5.2.2",
|
||||||
"@radix-ui/react-accordion": "^1.2.12",
|
"@radix-ui/react-accordion": "^1.2.12",
|
||||||
"@radix-ui/react-alert-dialog": "^1.1.15",
|
"@radix-ui/react-alert-dialog": "^1.1.15",
|
||||||
@@ -44,46 +47,46 @@
|
|||||||
"@radix-ui/react-toggle-group": "^1.1.11",
|
"@radix-ui/react-toggle-group": "^1.1.11",
|
||||||
"@radix-ui/react-tooltip": "^1.2.8",
|
"@radix-ui/react-tooltip": "^1.2.8",
|
||||||
"@react-email/components": "^0.0.41",
|
"@react-email/components": "^0.0.41",
|
||||||
"@t3-oss/env-nextjs": "^0.13.10",
|
"@t3-oss/env-nextjs": "^0.13.11",
|
||||||
"@tanstack/react-query": "^5.90.21",
|
"@tanstack/react-query": "^5.95.2",
|
||||||
"@tanstack/react-table": "^8.21.3",
|
"@tanstack/react-table": "^8.21.3",
|
||||||
"@types/express": "^5.0.6",
|
"@types/nodemailer": "^6.4.23",
|
||||||
"@types/nodemailer": "^6.4.22",
|
|
||||||
"@types/ws": "^8.18.1",
|
"@types/ws": "^8.18.1",
|
||||||
"@zenstackhq/runtime": "2.14.2",
|
"@zenstackhq/runtime": "2.14.2",
|
||||||
"argon2": "^0.43.1",
|
"argon2": "^0.43.1",
|
||||||
"bcrypt": "^6.0.0",
|
"bcrypt": "^6.0.0",
|
||||||
"better-auth": "1.4.5",
|
"better-auth": "1.5.6",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"cmdk": "^1.1.1",
|
"cmdk": "^1.1.1",
|
||||||
"date-fns": "^4.1.0",
|
"date-fns": "^4.1.0",
|
||||||
"dockerode": "^4.0.9",
|
"dockerode": "^4.0.10",
|
||||||
"dotenv": "^16.6.1",
|
"dotenv": "^16.6.1",
|
||||||
"drizzle-orm": "^0.43.1",
|
"drizzle-orm": "^0.43.1",
|
||||||
"drizzle-zod": "^0.7.1",
|
"drizzle-zod": "^0.7.1",
|
||||||
"embla-carousel-react": "^8.6.0",
|
"embla-carousel-react": "^8.6.0",
|
||||||
"express": "^5.2.1",
|
|
||||||
"googleapis": "^170.1.0",
|
"googleapis": "^170.1.0",
|
||||||
"input-otp": "^1.4.2",
|
"input-otp": "^1.4.2",
|
||||||
"lucide-react": "^0.553.0",
|
"lucide-react": "^0.553.0",
|
||||||
"minio": "^8.0.6",
|
"minio": "^8.0.7",
|
||||||
"motion": "^12.34.0",
|
"motion": "^12.38.0",
|
||||||
"next": "16.1.5",
|
"next": "16.2.1",
|
||||||
"next-safe-action": "^7.10.8",
|
"next-safe-action": "^7.10.8",
|
||||||
"next-themes": "^0.4.6",
|
"next-themes": "^0.4.6",
|
||||||
"node-cron": "^4.2.1",
|
"node-cron": "^4.2.1",
|
||||||
"node-forge": "^1.3.3",
|
"node-forge": "^1.4.0",
|
||||||
"nodemailer": "^7.0.13",
|
"nodemailer": "^7.0.13",
|
||||||
"npm-check-updates": "^18.3.1",
|
"npm-check-updates": "^18.3.1",
|
||||||
"pg": "^8.18.0",
|
"pg": "^8.20.0",
|
||||||
|
"pino": "^10.3.1",
|
||||||
|
"pino-pretty": "^13.1.3",
|
||||||
"prettier": "^3.8.1",
|
"prettier": "^3.8.1",
|
||||||
"react": "^19.2.4",
|
"react": "^19.2.4",
|
||||||
"react-day-picker": "9.7.0",
|
"react-day-picker": "9.7.0",
|
||||||
"react-dom": "^19.2.4",
|
"react-dom": "^19.2.4",
|
||||||
"react-dropzone": "^14.4.1",
|
"react-dropzone": "^14.4.1",
|
||||||
"react-email": "^4.3.2",
|
"react-email": "^4.3.2",
|
||||||
"react-hook-form": "^7.71.1",
|
"react-hook-form": "^7.72.0",
|
||||||
"react-qr-code": "^2.0.18",
|
"react-qr-code": "^2.0.18",
|
||||||
"react-resizable-panels": "^3.0.6",
|
"react-resizable-panels": "^3.0.6",
|
||||||
"react-twc": "^1.5.1",
|
"react-twc": "^1.5.1",
|
||||||
@@ -93,39 +96,43 @@
|
|||||||
"socket.io": "^4.8.3",
|
"socket.io": "^4.8.3",
|
||||||
"socket.io-client": "^4.8.3",
|
"socket.io-client": "^4.8.3",
|
||||||
"sonner": "^2.0.7",
|
"sonner": "^2.0.7",
|
||||||
"swiper": "^12.1.0",
|
"swiper": "^12.1.3",
|
||||||
"tailwind-merge": "^3.4.0",
|
"tailwind-merge": "^3.5.0",
|
||||||
"uuid": "^11.1.0",
|
"uuid": "^11.1.0",
|
||||||
"vaul": "^1.1.2",
|
"vaul": "^1.1.2",
|
||||||
"ws": "^8.19.0",
|
"ws": "^8.20.0",
|
||||||
"zod": "^3.25.76"
|
"zod": "^3.25.76"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@iconify/react": "^6.0.2",
|
"@iconify/react": "^6.0.2",
|
||||||
|
"@playwright/test": "1.58.2",
|
||||||
"@react-email/preview-server": "4.3.2",
|
"@react-email/preview-server": "4.3.2",
|
||||||
"@react-email/render": "^2.0.4",
|
"@react-email/render": "^2.0.4",
|
||||||
"@tailwindcss/postcss": "^4.1.18",
|
"@release-it/bumper": "^7.0.5",
|
||||||
|
"@release-it/conventional-changelog": "^10.0.6",
|
||||||
|
"@tailwindcss/postcss": "^4.2.2",
|
||||||
"@types/eslint-plugin-tailwindcss": "^3.17.0",
|
"@types/eslint-plugin-tailwindcss": "^3.17.0",
|
||||||
"@types/node": "^22.19.11",
|
"@types/node": "^22.19.15",
|
||||||
"@types/node-forge": "^1.3.14",
|
"@types/node-forge": "^1.3.14",
|
||||||
"@types/pg": "^8.16.0",
|
"@types/pg": "^8.20.0",
|
||||||
"@types/react": "^19.2.14",
|
"@types/react": "^19.2.14",
|
||||||
"@types/react-dom": "^19.2.3",
|
"@types/react-dom": "^19.2.3",
|
||||||
"@zenstackhq/openapi": "^2.22.1",
|
"@zenstackhq/openapi": "^2.22.1",
|
||||||
"@zenstackhq/tanstack-query": "^2.22.2",
|
"@zenstackhq/tanstack-query": "^2.22.2",
|
||||||
"baseline-browser-mapping": "^2.9.19",
|
"baseline-browser-mapping": "^2.10.12",
|
||||||
"drizzle-kit": "^0.31.9",
|
"drizzle-kit": "^0.31.10",
|
||||||
"esbuild": "^0.27.3",
|
"esbuild": "^0.27.4",
|
||||||
"eslint": "^9.39.2",
|
"eslint": "^9.39.4",
|
||||||
"eslint-config-next": "^16.1.6",
|
"eslint-config-next": "^16.2.1",
|
||||||
"eslint-plugin-tailwindcss": "^3.18.2",
|
"eslint-plugin-tailwindcss": "^3.18.2",
|
||||||
"framer-motion": "^12.34.0",
|
"framer-motion": "^12.38.0",
|
||||||
"postcss": "^8.5.6",
|
"postcss": "^8.5.8",
|
||||||
"tailwindcss": "^4.1.18",
|
"release-it": "^19.2.4",
|
||||||
|
"tailwindcss": "^4.2.2",
|
||||||
"tsx": "^4.21.0",
|
"tsx": "^4.21.0",
|
||||||
"tw-animate-css": "^1.4.0",
|
"tw-animate-css": "^1.4.0",
|
||||||
"typescript": "^5.9.3",
|
"typescript": "^5.9.3",
|
||||||
"zenstack": "2.14.2"
|
"zenstack": "2.14.2"
|
||||||
},
|
},
|
||||||
"packageManager": "pnpm@10.29.2+sha512.bef43fa759d91fd2da4b319a5a0d13ef7a45bb985a3d7342058470f9d2051a3ba8674e629672654686ef9443ad13a82da2beb9eeb3e0221c87b8154fff9d74b8"
|
"packageManager": "pnpm@10.33.0"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,79 @@
|
|||||||
|
import { defineConfig, devices } from '@playwright/test';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Read environment variables from file.
|
||||||
|
* https://github.com/motdotla/dotenv
|
||||||
|
*/
|
||||||
|
// import dotenv from 'dotenv';
|
||||||
|
// import path from 'path';
|
||||||
|
// dotenv.config({ path: path.resolve(__dirname, '.env') });
|
||||||
|
|
||||||
|
/**
|
||||||
|
* See https://playwright.dev/docs/test-configuration.
|
||||||
|
*/
|
||||||
|
export default defineConfig({
|
||||||
|
testDir: './e2e',
|
||||||
|
/* Run tests in files in parallel */
|
||||||
|
fullyParallel: true,
|
||||||
|
/* Fail the build on CI if you accidentally left test.only in the source code. */
|
||||||
|
forbidOnly: !!process.env.CI,
|
||||||
|
/* Retry on CI only */
|
||||||
|
retries: process.env.CI ? 2 : 0,
|
||||||
|
/* Opt out of parallel tests on CI. */
|
||||||
|
workers: process.env.CI ? 1 : undefined,
|
||||||
|
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
|
||||||
|
reporter: 'html',
|
||||||
|
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
|
||||||
|
use: {
|
||||||
|
/* Base URL to use in actions like `await page.goto('')`. */
|
||||||
|
baseURL: process.env.PROJECT_URL,
|
||||||
|
|
||||||
|
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
|
||||||
|
trace: 'on-first-retry',
|
||||||
|
},
|
||||||
|
|
||||||
|
/* Configure projects for major browsers */
|
||||||
|
projects: [
|
||||||
|
{
|
||||||
|
name: 'chromium',
|
||||||
|
use: { ...devices['Desktop Chrome'] },
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
name: 'firefox',
|
||||||
|
use: { ...devices['Desktop Firefox'] },
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
name: 'webkit',
|
||||||
|
use: { ...devices['Desktop Safari'] },
|
||||||
|
},
|
||||||
|
|
||||||
|
/* Test against mobile viewports. */
|
||||||
|
{
|
||||||
|
name: 'Mobile Chrome',
|
||||||
|
use: { ...devices['Pixel 5'] },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Mobile Safari',
|
||||||
|
use: { ...devices['iPhone 12'] },
|
||||||
|
},
|
||||||
|
|
||||||
|
/* Test against branded browsers. */
|
||||||
|
// {
|
||||||
|
// name: 'Microsoft Edge',
|
||||||
|
// use: { ...devices['Desktop Edge'], channel: 'msedge' },
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// name: 'Google Chrome',
|
||||||
|
// use: { ...devices['Desktop Chrome'], channel: 'chrome' },
|
||||||
|
// },
|
||||||
|
],
|
||||||
|
|
||||||
|
/* Run your local dev server before starting the tests */
|
||||||
|
// webServer: {
|
||||||
|
// command: 'docker compose up -d && make up',
|
||||||
|
// url: 'http://localhost:8887',
|
||||||
|
// reuseExistingServer: !process.env.CI,
|
||||||
|
// },
|
||||||
|
});
|
||||||
Generated
+3252
-1523
File diff suppressed because it is too large
Load Diff
+1
-45
@@ -1,17 +1,5 @@
|
|||||||
|
|
||||||
|
|
||||||
export interface AuthProviderConfig {
|
|
||||||
id: "google" | "github" | "credential";
|
|
||||||
isActive: boolean;
|
|
||||||
icon: string;
|
|
||||||
isManual?: boolean;
|
|
||||||
credentials?: {
|
|
||||||
clientId: string;
|
|
||||||
clientSecret: string;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
export const PORTABASE_DEFAULT_SETTINGS = {
|
export const PORTABASE_DEFAULT_SETTINGS = {
|
||||||
SECURITY: {
|
SECURITY: {
|
||||||
CSP: {
|
CSP: {
|
||||||
@@ -64,7 +52,7 @@ export const PORTABASE_DEFAULT_SETTINGS = {
|
|||||||
FORM_ACTION: ["'self'"],
|
FORM_ACTION: ["'self'"],
|
||||||
FRAME_ANCESTORS: ["'none'"],
|
FRAME_ANCESTORS: ["'none'"],
|
||||||
BLOCK_ALL_MIXED_CONTENT: false,
|
BLOCK_ALL_MIXED_CONTENT: false,
|
||||||
UPGRADE_INSECURE_REQUESTS: true,
|
UPGRADE_INSECURE_REQUESTS: process.env.PROJECT_URL?.startsWith("https://") || false,
|
||||||
},
|
},
|
||||||
PERMISSIONS_POLICY: {
|
PERMISSIONS_POLICY: {
|
||||||
CAMERA: ["()"],
|
CAMERA: ["()"],
|
||||||
@@ -76,35 +64,3 @@ export const PORTABASE_DEFAULT_SETTINGS = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
export const SUPPORTED_PROVIDERS: AuthProviderConfig[] = [
|
|
||||||
{
|
|
||||||
id: "google",
|
|
||||||
icon: "hugeicons:chrome",
|
|
||||||
isActive: Boolean(process.env.AUTH_GOOGLE_METHOD),
|
|
||||||
// isManual: true,
|
|
||||||
credentials: {
|
|
||||||
clientId: process.env.AUTH_GOOGLE_ID || "",
|
|
||||||
clientSecret: process.env.AUTH_GOOGLE_SECRET || "",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "github",
|
|
||||||
icon: "iconoir:github",
|
|
||||||
isActive: Boolean(process.env.AUTH_GITHUB_METHOD),
|
|
||||||
credentials: {
|
|
||||||
clientId: process.env.AUTH_GITHUB_ID || "",
|
|
||||||
clientSecret: process.env.AUTH_GITHUB_SECRET || "",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "credential",
|
|
||||||
isActive: true,
|
|
||||||
icon: "proicons:key",
|
|
||||||
isManual: true,
|
|
||||||
credentials: {
|
|
||||||
clientId: "",
|
|
||||||
clientSecret: "",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|||||||
@@ -1,75 +1,81 @@
|
|||||||
import {NextRequest, NextResponse} from "next/server";
|
import { NextRequest, NextResponse } from "next/server";
|
||||||
import {loggingMiddleware} from "@/middleware/loggingMiddleware";
|
import { loggingMiddleware } from "@/middleware/loggingMiddleware";
|
||||||
import {errorHandler} from "@/middleware/errorHandler";
|
import { errorHandler } from "@/middleware/errorHandler";
|
||||||
import {auth} from "@/lib/auth/auth";
|
import { auth } from "@/lib/auth/auth";
|
||||||
import {headers} from "next/headers";
|
import { headers } from "next/headers";
|
||||||
|
|
||||||
export async function proxy(request: NextRequest) {
|
export async function proxy(request: NextRequest) {
|
||||||
const url = request.nextUrl.clone();
|
const url = request.nextUrl.clone();
|
||||||
const redirectUrl = encodeURIComponent(request.nextUrl.pathname)
|
const redirectUrl = encodeURIComponent(request.nextUrl.pathname);
|
||||||
|
|
||||||
if (url.pathname.startsWith("/dashboard")) {
|
if (url.pathname.startsWith("/dashboard")) {
|
||||||
const session = await auth.api.getSession({
|
const session = await auth.api.getSession({
|
||||||
headers: await headers(),
|
headers: await headers(),
|
||||||
});
|
});
|
||||||
if (!session) {
|
if (!session) {
|
||||||
return NextResponse.redirect(new URL(`/login?redirect=${redirectUrl}`, request.url));
|
return NextResponse.redirect(
|
||||||
}
|
new URL(`/login?redirect=${redirectUrl}`, request.url),
|
||||||
if (session.user.banned) {
|
);
|
||||||
await auth.api.signOut({headers: await headers()});
|
|
||||||
return NextResponse.redirect(new URL("/login?error=banned", request.url));
|
|
||||||
}
|
|
||||||
if (session.user.role === "pending") {
|
|
||||||
await auth.api.signOut({headers: await headers()});
|
|
||||||
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();
|
|
||||||
}
|
}
|
||||||
|
if (session.user.banned) {
|
||||||
|
await auth.api.signOut({ headers: await headers() });
|
||||||
|
return NextResponse.redirect(new URL("/login?error=banned", request.url));
|
||||||
|
}
|
||||||
|
if (session.user.role === "pending") {
|
||||||
|
await auth.api.signOut({ headers: await headers() });
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
|
||||||
if (url.pathname.startsWith("/api/auth")) {
|
if (url.pathname.startsWith("/api/auth")) {
|
||||||
return NextResponse.next();
|
return NextResponse.next();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (url.pathname.startsWith("/api")) {
|
if (url.pathname.startsWith("/api")) {
|
||||||
const routeExists = checkRouteExists(url.pathname);
|
const routeExists = checkRouteExists(url.pathname);
|
||||||
if (!routeExists) {
|
if (!routeExists) {
|
||||||
return new NextResponse(JSON.stringify({message: "This API route does not exist.", status: 404}), {
|
return new NextResponse(
|
||||||
status: 404,
|
JSON.stringify({
|
||||||
headers: {"Content-Type": "application/json"},
|
message: "This API route does not exist.",
|
||||||
});
|
status: 404,
|
||||||
}
|
}),
|
||||||
}
|
{
|
||||||
try {
|
status: 404,
|
||||||
loggingMiddleware(request);
|
headers: { "Content-Type": "application/json" },
|
||||||
} catch (err) {
|
},
|
||||||
errorHandler(err);
|
);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
loggingMiddleware(request);
|
||||||
|
} catch (err) {
|
||||||
|
errorHandler(err);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function checkRouteExists(pathname: string) {
|
function checkRouteExists(pathname: string) {
|
||||||
const routePatterns = [
|
const routePatterns = [
|
||||||
/^\/api\/agent\/[^/]+\/status\/?$/,
|
/^\/api\/agent\/[^/]+\/status\/?$/,
|
||||||
/^\/api\/agent\/[^/]+\/backup\/?$/,
|
/^\/api\/agent\/[^/]+\/backup\/?$/,
|
||||||
/^\/api\/agent\/[^/]+\/backup\/upload\/init\/?$/,
|
/^\/api\/agent\/[^/]+\/backup\/upload\/init\/?$/,
|
||||||
/^\/api\/agent\/[^/]+\/backup\/upload\/status\/?$/,
|
/^\/api\/agent\/[^/]+\/backup\/upload\/status\/?$/,
|
||||||
/^\/api\/agent\/[^/]+\/restore\/?$/,
|
/^\/api\/agent\/[^/]+\/restore\/?$/,
|
||||||
/^\/api\/files\/images\/[^/]+\/?$/,
|
/^\/api\/files\/images\/[^/]+\/?$/,
|
||||||
/^\/api\/files\/backups\/?$/,
|
/^\/api\/files\/backups\/?$/,
|
||||||
/^\/api\/tus\/hooks\/?$/,
|
/^\/api\/tus\/hooks\/?$/,
|
||||||
/^\/api\/events\/?$/,
|
/^\/api\/events\/?$/,
|
||||||
/^\/api\/config\/?$/,
|
/^\/api\/config\/?$/,
|
||||||
/^\/api\/google\/drive\/callback\/?$/,
|
/^\/api\/google\/drive\/callback\/?$/,
|
||||||
];
|
];
|
||||||
return routePatterns.some((pattern) => pattern.test(pathname));
|
return routePatterns.some((pattern) => pattern.test(pathname));
|
||||||
}
|
}
|
||||||
|
|
||||||
export const config = {
|
export const config = {
|
||||||
matcher: [
|
matcher: ["/api/:path*", "/dashboard/:path*", "/dashboard"],
|
||||||
"/api/:path*",
|
|
||||||
"/dashboard/:path*",
|
|
||||||
"/dashboard",
|
|
||||||
]
|
|
||||||
};
|
};
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 16 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 18 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 40 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 31 KiB |
@@ -1,95 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
|
|
||||||
set -e
|
|
||||||
|
|
||||||
if [ -z "$1" ]; then
|
|
||||||
echo "Usage: ./release <version>"
|
|
||||||
echo "Example: ./release v1.0.0"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
VERSION=$1
|
|
||||||
CURRENT_BRANCH=$(git rev-parse --abbrev-ref HEAD)
|
|
||||||
|
|
||||||
if [ "$CURRENT_BRANCH" = "main" ]; then
|
|
||||||
if [[ ! "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
|
|
||||||
echo "Error: On 'main' branch, only release tags (X.Y.Z) are allowed."
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
if [[ ! "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+-rc\.[0-9]+$ ]]; then
|
|
||||||
echo "Error: On branch '$CURRENT_BRANCH', only RC tags matching X.Y.Z-rc.W are allowed (e.g., 1.0.0-rc.1)."
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
CLEAN_VERSION=${VERSION#v}
|
|
||||||
CURRENT_DATE=$(date +%Y-%m-%d)
|
|
||||||
|
|
||||||
echo "Preparing release $VERSION..."
|
|
||||||
|
|
||||||
|
|
||||||
# package.json
|
|
||||||
if [ -f package.json ]; then
|
|
||||||
echo "Updating package.json..."
|
|
||||||
if sed --version >/dev/null 2>&1; then
|
|
||||||
sed -i "s/\"version\": \".*\"/\"version\": \"$CLEAN_VERSION\"/" package.json
|
|
||||||
else
|
|
||||||
sed -i '' "s/\"version\": \".*\"/\"version\": \"$CLEAN_VERSION\"/" package.json
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
# pyproject.toml
|
|
||||||
if [ -f pyproject.toml ]; then
|
|
||||||
echo "Updating pyproject.toml..."
|
|
||||||
if sed --version >/dev/null 2>&1; then
|
|
||||||
sed -i "s/^version = \".*\"/version = \"$CLEAN_VERSION\"/" pyproject.toml
|
|
||||||
else
|
|
||||||
sed -i '' "s/^version = \".*\"/version = \"$CLEAN_VERSION\"/" pyproject.toml
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Cargo.toml
|
|
||||||
if [ -f Cargo.toml ]; then
|
|
||||||
echo "Updating Cargo.toml..."
|
|
||||||
if sed --version >/dev/null 2>&1; then
|
|
||||||
sed -i "s/^version = \".*\"/version = \"$CLEAN_VERSION\"/" Cargo.toml
|
|
||||||
else
|
|
||||||
sed -i '' "s/^version = \".*\"/version = \"$CLEAN_VERSION\"/" Cargo.toml
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
# CITATION.cff
|
|
||||||
if [ -f CITATION.cff ]; then
|
|
||||||
echo "Updating CITATION.cff..."
|
|
||||||
if sed --version >/dev/null 2>&1; then
|
|
||||||
sed -i "s/^version: .*/version: $CLEAN_VERSION/" CITATION.cff
|
|
||||||
sed -i "s/^date-released: .*/date-released: \"$CURRENT_DATE\"/" CITATION.cff
|
|
||||||
else
|
|
||||||
sed -i '' "s/^version: .*/version: $CLEAN_VERSION/" CITATION.cff
|
|
||||||
sed -i '' "s/^date-released: .*/date-released: \"$CURRENT_DATE\"/" CITATION.cff
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
git add .
|
|
||||||
|
|
||||||
if ! git diff-index --quiet HEAD --; then
|
|
||||||
echo "Committing changes..."
|
|
||||||
git commit -m "chore(release): $VERSION"
|
|
||||||
else
|
|
||||||
echo "No changes to commit. Proceeding to tag..."
|
|
||||||
fi
|
|
||||||
|
|
||||||
if git rev-parse "$VERSION" >/dev/null 2>&1; then
|
|
||||||
echo "Tag $VERSION already exists. Aborting."
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "Creating tag $VERSION..."
|
|
||||||
git tag -a "$VERSION" -m "Release $VERSION"
|
|
||||||
|
|
||||||
echo "Pushing changes and tags to remote..."
|
|
||||||
git push
|
|
||||||
git push origin "$VERSION"
|
|
||||||
|
|
||||||
echo "Successfully released $VERSION!"
|
|
||||||
File diff suppressed because it is too large
Load Diff
Binary file not shown.
@@ -1,6 +0,0 @@
|
|||||||
import express from "express";
|
|
||||||
import router from "../src/features/api/router";
|
|
||||||
|
|
||||||
export function mountApi(app: express.Express) {
|
|
||||||
app.use("/services/v1", router);
|
|
||||||
}
|
|
||||||
@@ -1,52 +0,0 @@
|
|||||||
import next from "next";
|
|
||||||
import express from "express";
|
|
||||||
import {mountApi} from "./api";
|
|
||||||
import http from "http";
|
|
||||||
|
|
||||||
const port = Number(process.env.PORT) || 8887;
|
|
||||||
const dev = process.env.NODE_ENV !== "production";
|
|
||||||
|
|
||||||
async function start() {
|
|
||||||
const nextApp = next({
|
|
||||||
dev,
|
|
||||||
hostname: "0.0.0.0",
|
|
||||||
turbopack: dev,
|
|
||||||
port
|
|
||||||
});
|
|
||||||
|
|
||||||
const handle = nextApp.getRequestHandler();
|
|
||||||
|
|
||||||
await nextApp.prepare();
|
|
||||||
|
|
||||||
const app = express();
|
|
||||||
|
|
||||||
app.use((req, res, next) => {
|
|
||||||
req.setTimeout(0);
|
|
||||||
res.setTimeout(0);
|
|
||||||
next();
|
|
||||||
});
|
|
||||||
|
|
||||||
mountApi(app);
|
|
||||||
|
|
||||||
app.use((req, res, next) => {
|
|
||||||
if (req.path.startsWith('/services/v1')) return next();
|
|
||||||
handle(req, res).catch((err) => {
|
|
||||||
console.error('Next.js App Router error:', err);
|
|
||||||
res.status(500).send('Internal Server Error');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
const server = http.createServer(app);
|
|
||||||
|
|
||||||
server.setTimeout(0);
|
|
||||||
server.headersTimeout = 0;
|
|
||||||
server.requestTimeout = 0;
|
|
||||||
server.keepAliveTimeout = 0;
|
|
||||||
|
|
||||||
server.listen(port, "0.0.0.0", () => {
|
|
||||||
console.log(`NEXT APP → http://localhost:${port}`);
|
|
||||||
console.log(`API → http://localhost:${port}/services/v1`);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
start();
|
|
||||||
@@ -6,7 +6,7 @@ import {env} from "@/env.mjs";
|
|||||||
|
|
||||||
interface EmailCreateUserProps {
|
interface EmailCreateUserProps {
|
||||||
email: string;
|
email: string;
|
||||||
password: string;
|
password?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const EmailCreateUser = ({email, password}: EmailCreateUserProps) => {
|
export const EmailCreateUser = ({email, password}: EmailCreateUserProps) => {
|
||||||
@@ -23,9 +23,15 @@ export const EmailCreateUser = ({email, password}: EmailCreateUserProps) => {
|
|||||||
<strong>Email: </strong>{email}
|
<strong>Email: </strong>{email}
|
||||||
</Text>
|
</Text>
|
||||||
|
|
||||||
<Text className="text-[14px] text-black leading-[24px]">
|
{password ? (
|
||||||
<strong>Default password: </strong>{password}
|
<Text className="text-[14px] text-black leading-[24px]">
|
||||||
</Text>
|
<strong>Default password: </strong>{password}
|
||||||
|
</Text>
|
||||||
|
) : (
|
||||||
|
<Text className="text-[14px] text-black leading-[24px]">
|
||||||
|
You can log in using one of the single sign-on (SSO) providers configured by your administrator.
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
|
||||||
<Section className="mt-[32px] mb-[32px] text-center">
|
<Section className="mt-[32px] mb-[32px] text-center">
|
||||||
<Button
|
<Button
|
||||||
|
|||||||
@@ -208,13 +208,26 @@ type UseZodFormProps<Z extends ZodSchema> = Exclude<
|
|||||||
schema: Z;
|
schema: Z;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
// const useZodForm = <Z extends ZodSchema>({
|
||||||
|
// schema,
|
||||||
|
// ...formProps
|
||||||
|
// }: UseZodFormProps<Z>) =>
|
||||||
|
// useForm({
|
||||||
|
// ...formProps,
|
||||||
|
// // @ts-ignore
|
||||||
|
// resolver: zodResolver(schema),
|
||||||
|
// });
|
||||||
|
|
||||||
|
|
||||||
const useZodForm = <Z extends ZodSchema>({
|
const useZodForm = <Z extends ZodSchema>({
|
||||||
schema,
|
schema,
|
||||||
...formProps
|
...formProps
|
||||||
}: UseZodFormProps<Z>) =>
|
}: UseZodFormProps<Z>): UseFormReturn<TypeOf<Z>> =>
|
||||||
useForm({
|
useForm<TypeOf<Z>>({
|
||||||
...formProps,
|
...formProps,
|
||||||
resolver: zodResolver(schema),
|
// @ts-ignore
|
||||||
|
resolver: zodResolver(schema),
|
||||||
});
|
});
|
||||||
|
|
||||||
export {
|
export {
|
||||||
|
|||||||
@@ -1,121 +1,133 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import {useEffect, useState} from "react";
|
import { useMutation } from "@tanstack/react-query";
|
||||||
import {useMutation} from "@tanstack/react-query";
|
|
||||||
import {toast} from "sonner";
|
|
||||||
import {FormControl, FormField, FormItem, FormLabel, FormMessage, useZodForm} from "@/components/ui/form";
|
|
||||||
import {Input} from "@/components/ui/input";
|
|
||||||
import {Form} from "@/components/ui/form";
|
|
||||||
import {LoginSchema, LoginType} from "@/components/wrappers/auth/login/login-form/login-form.schema";
|
|
||||||
import {signIn} from "@/lib/auth/auth-client";
|
|
||||||
import {ButtonWithLoading} from "@/components/wrappers/common/button/button-with-loading";
|
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import {useRouter} from "next/navigation";
|
import { useRouter, useSearchParams } from "next/navigation";
|
||||||
import {PasswordInput} from "@/components/ui/password-input";
|
import { useEffect } from "react";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
import {
|
||||||
|
Form,
|
||||||
|
FormControl,
|
||||||
|
FormField,
|
||||||
|
FormItem,
|
||||||
|
FormLabel,
|
||||||
|
FormMessage,
|
||||||
|
useZodForm,
|
||||||
|
} from "@/components/ui/form";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { PasswordInput } from "@/components/ui/password-input";
|
||||||
|
import {
|
||||||
|
LoginSchema,
|
||||||
|
type LoginType,
|
||||||
|
} from "@/components/wrappers/auth/login/login-form/login-form.schema";
|
||||||
|
import { ButtonWithLoading } from "@/components/wrappers/common/button/button-with-loading";
|
||||||
|
import { signIn } from "@/lib/auth/auth-client";
|
||||||
|
|
||||||
export type loginFormProps = {
|
export type loginFormProps = {
|
||||||
defaultValues?: LoginType;
|
defaultValues?: LoginType;
|
||||||
|
isPasskeyEnabled?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const LoginForm = (props: loginFormProps) => {
|
export const LoginForm = (props: loginFormProps) => {
|
||||||
|
const { isPasskeyEnabled = false } = props;
|
||||||
|
|
||||||
|
const url = useSearchParams();
|
||||||
|
|
||||||
const [urlParams, setUrlParams] = useState<URLSearchParams>();
|
const form = useZodForm({
|
||||||
|
schema: LoginSchema,
|
||||||
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
const router = useRouter();
|
||||||
const urlParams = new URLSearchParams(window.location.search);
|
|
||||||
setUrlParams(urlParams);
|
|
||||||
const error = urlParams.get("error");
|
|
||||||
if (error?.includes("pending")) {
|
|
||||||
toast.error("Your account is not active.");
|
|
||||||
urlParams.delete("error");
|
|
||||||
window.history.replaceState({}, document.title, window.location.pathname + "?" + urlParams.toString());
|
|
||||||
}
|
|
||||||
if (error?.includes("invalid_or_expired_token")) {
|
|
||||||
toast.error("Password reset invalid token.");
|
|
||||||
urlParams.delete("error");
|
|
||||||
window.history.replaceState({}, document.title, window.location.pathname + "?" + urlParams.toString());
|
|
||||||
}
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const form = useZodForm({
|
const mutation = useMutation({
|
||||||
schema: LoginSchema,
|
mutationFn: async (values: LoginType) => {
|
||||||
});
|
await signIn.email(
|
||||||
|
{
|
||||||
const router = useRouter();
|
password: values.password,
|
||||||
|
email: values.email,
|
||||||
const mutation = useMutation({
|
callbackURL: url?.get("redirect") ?? "/dashboard",
|
||||||
mutationFn: async (values: LoginType) => {
|
|
||||||
await signIn.email(
|
|
||||||
{
|
|
||||||
password: values.password,
|
|
||||||
email: values.email,
|
|
||||||
callbackURL: urlParams?.get("redirect") ?? "/dashboard",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
onSuccess: (context) => {
|
|
||||||
if (context.data.twoFactorRedirect) {
|
|
||||||
//@ts-ignore
|
|
||||||
router.push("/guard?redirect=" + encodeURIComponent(context.data.callbackURL || "/dashboard"));
|
|
||||||
}
|
|
||||||
|
|
||||||
toast.success("Login success");
|
|
||||||
},
|
|
||||||
onError: (error) => {
|
|
||||||
toast.error(error.error.message);
|
|
||||||
},
|
|
||||||
}
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
});
|
{
|
||||||
|
onSuccess: (context) => {
|
||||||
|
if (context.data.twoFactorRedirect) {
|
||||||
|
router.push(
|
||||||
|
"/guard?redirect=" +
|
||||||
|
encodeURIComponent(context.data.callbackURL || "/dashboard"),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
toast.success("Login success");
|
||||||
<Form
|
},
|
||||||
form={form}
|
onError: (error) => {
|
||||||
className="flex flex-col gap-4 mb-1"
|
toast.error(error.error.message);
|
||||||
onSubmit={async (values) => {
|
},
|
||||||
await mutation.mutateAsync(values);
|
},
|
||||||
}}
|
);
|
||||||
>
|
},
|
||||||
<FormField
|
});
|
||||||
control={form.control}
|
|
||||||
name="email"
|
return (
|
||||||
defaultValue=""
|
<Form
|
||||||
render={({field}) => (
|
form={form}
|
||||||
<FormItem>
|
className="flex flex-col gap-4 mb-1"
|
||||||
<FormLabel>Email Address</FormLabel>
|
onSubmit={async (values) => {
|
||||||
<FormControl>
|
await mutation.mutateAsync(values);
|
||||||
<Input autoComplete="email" autoFocus placeholder="example@portabase.io" {...field} />
|
}}
|
||||||
</FormControl>
|
>
|
||||||
<FormMessage/>
|
<FormField
|
||||||
</FormItem>
|
control={form.control}
|
||||||
)}
|
name="email"
|
||||||
/>
|
defaultValue=""
|
||||||
<FormField
|
render={({ field }) => (
|
||||||
control={form.control}
|
<FormItem>
|
||||||
name="password"
|
<FormLabel>Email Address</FormLabel>
|
||||||
defaultValue=""
|
<FormControl>
|
||||||
render={({field}) => (
|
<Input
|
||||||
<FormItem>
|
autoComplete="email"
|
||||||
<div className="flex items-center justify-between">
|
autoFocus
|
||||||
<FormLabel>Password</FormLabel>
|
placeholder="example@portabase.io"
|
||||||
<div className="text-center text-sm">
|
{...field}
|
||||||
<Link href={"/forgot-password"} className="hover:underline ml-1">
|
/>
|
||||||
Forgot your password ?
|
</FormControl>
|
||||||
</Link>
|
<FormMessage />
|
||||||
</div>
|
</FormItem>
|
||||||
</div>
|
)}
|
||||||
<FormControl>
|
/>
|
||||||
<PasswordInput autoComplete="current-password webauthn"
|
<FormField
|
||||||
placeholder={"Enter your password"} {...field} />
|
control={form.control}
|
||||||
</FormControl>
|
name="password"
|
||||||
<FormMessage/>
|
defaultValue=""
|
||||||
</FormItem>
|
render={({ field }) => (
|
||||||
)}
|
<FormItem>
|
||||||
/>
|
<div className="flex items-center justify-between">
|
||||||
<ButtonWithLoading className="mt-2 h-11" isPending={mutation.isPending}>
|
<FormLabel>Password</FormLabel>
|
||||||
Login
|
<div className="text-center text-sm">
|
||||||
</ButtonWithLoading>
|
<Link
|
||||||
</Form>
|
href={"/forgot-password"}
|
||||||
);
|
className="hover:underline ml-1"
|
||||||
|
>
|
||||||
|
Forgot your password ?
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<FormControl>
|
||||||
|
<PasswordInput
|
||||||
|
autoComplete={
|
||||||
|
isPasskeyEnabled
|
||||||
|
? "current-password webauthn"
|
||||||
|
: "current-password"
|
||||||
|
}
|
||||||
|
placeholder={"Enter your password"}
|
||||||
|
{...field}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<ButtonWithLoading className="mt-2 h-11" isPending={mutation.isPending}>
|
||||||
|
Login
|
||||||
|
</ButtonWithLoading>
|
||||||
|
</Form>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,156 +1,169 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import {useRouter} from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
import {useMutation} from "@tanstack/react-query";
|
import { useMutation } from "@tanstack/react-query";
|
||||||
import {toast} from "sonner";
|
import { toast } from "sonner";
|
||||||
import {CardContent, CardHeader} from "@/components/ui/card";
|
import { CardContent, CardHeader } from "@/components/ui/card";
|
||||||
import {Form} from "@/components/ui/form";
|
import { Form } from "@/components/ui/form";
|
||||||
import {TooltipProvider, TooltipTrigger, Tooltip, TooltipContent} from "@/components/ui/tooltip";
|
import {
|
||||||
import {RegisterSchema, RegisterType} from "@/components/wrappers/auth/register/register-form/register-form.schema";
|
TooltipProvider,
|
||||||
import {signUp} from "@/lib/auth/auth-client";
|
TooltipTrigger,
|
||||||
import {useZodForm} from "@/components/ui/form";
|
Tooltip,
|
||||||
import {CardAuth} from "@/features/layout/card-auth";
|
TooltipContent,
|
||||||
import {FormField} from "@/components/ui/form";
|
} from "@/components/ui/tooltip";
|
||||||
import {FormItem} from "@/components/ui/form";
|
import {
|
||||||
import {FormLabel} from "@/components/ui/form";
|
RegisterSchema,
|
||||||
import {FormControl} from "@/components/ui/form";
|
RegisterType,
|
||||||
import {FormMessage} from "@/components/ui/form";
|
} from "@/components/wrappers/auth/register/register-form/register-form.schema";
|
||||||
import {Input} from "@/components/ui/input";
|
import { signUp } from "@/lib/auth/auth-client";
|
||||||
import {PasswordInput} from "@/components/ui/password-input";
|
import { useZodForm } from "@/components/ui/form";
|
||||||
import {Button} from "@/components/ui/button";
|
import { CardAuth } from "@/features/layout/card-auth";
|
||||||
|
import { FormField } from "@/components/ui/form";
|
||||||
|
import { FormItem } from "@/components/ui/form";
|
||||||
|
import { FormLabel } from "@/components/ui/form";
|
||||||
|
import { FormControl } from "@/components/ui/form";
|
||||||
|
import { FormMessage } from "@/components/ui/form";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { PasswordInput } from "@/components/ui/password-input";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import {Info} from "lucide-react";
|
import { Info } from "lucide-react";
|
||||||
|
|
||||||
export type registerFormProps = {
|
export type registerFormProps = {
|
||||||
defaultValues?: RegisterType;
|
defaultValues?: RegisterType;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const RegisterForm = (props: registerFormProps) => {
|
export const RegisterForm = (props: registerFormProps) => {
|
||||||
|
const form = useZodForm({
|
||||||
|
schema: RegisterSchema,
|
||||||
|
});
|
||||||
|
|
||||||
const form = useZodForm({
|
const router = useRouter();
|
||||||
schema: RegisterSchema,
|
const mutation = useMutation({
|
||||||
});
|
mutationFn: async (values: RegisterType) => {
|
||||||
|
await signUp.email(values, {
|
||||||
const router = useRouter();
|
onSuccess: () => {
|
||||||
const mutation = useMutation({
|
toast.success(`Account successfully created`);
|
||||||
mutationFn: async (values: RegisterType) => {
|
router.refresh();
|
||||||
// @ts-ignore
|
router.push(`/login`);
|
||||||
await signUp.email(values, {
|
|
||||||
onSuccess: () => {
|
|
||||||
toast.success(`Account successfully created`);
|
|
||||||
router.refresh();
|
|
||||||
router.push(`/login`);
|
|
||||||
},
|
|
||||||
onError: (error) => {
|
|
||||||
toast.error(error.error.message);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
});
|
onError: (error) => {
|
||||||
|
toast.error(error.error.message);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
return (
|
<TooltipProvider>
|
||||||
<TooltipProvider>
|
<CardAuth>
|
||||||
<CardAuth>
|
<CardHeader>
|
||||||
<CardHeader>
|
<div className="grid gap-2 text-center mb-2">
|
||||||
<div className="grid gap-2 text-center mb-2">
|
<h1 className="text-3xl font-bold">Create an account</h1>
|
||||||
<h1 className="text-3xl font-bold">Create an account</h1>
|
<p className="text-balance text-muted-foreground">
|
||||||
<p className="text-balance text-muted-foreground">Enter your informations below to register</p>
|
Enter your informations below to register
|
||||||
</div>
|
</p>
|
||||||
</CardHeader>
|
</div>
|
||||||
<CardContent>
|
</CardHeader>
|
||||||
<Form
|
<CardContent>
|
||||||
form={form}
|
<Form
|
||||||
className="flex flex-col gap-4"
|
form={form}
|
||||||
onSubmit={async (values) => {
|
className="flex flex-col gap-4"
|
||||||
await mutation.mutateAsync(values);
|
onSubmit={async (values) => {
|
||||||
}}
|
await mutation.mutateAsync(values);
|
||||||
>
|
}}
|
||||||
<FormField
|
>
|
||||||
control={form.control}
|
<FormField
|
||||||
name="name"
|
control={form.control}
|
||||||
defaultValue=""
|
name="name"
|
||||||
render={({field}) => (
|
defaultValue=""
|
||||||
<FormItem>
|
render={({ field }) => (
|
||||||
<FormLabel>Name</FormLabel>
|
<FormItem>
|
||||||
<FormControl>
|
<FormLabel>Name</FormLabel>
|
||||||
<Input placeholder="Your name" {...field} />
|
<FormControl>
|
||||||
</FormControl>
|
<Input placeholder="Your name" {...field} />
|
||||||
<FormMessage/>
|
</FormControl>
|
||||||
</FormItem>
|
<FormMessage />
|
||||||
)}
|
</FormItem>
|
||||||
/>
|
)}
|
||||||
<FormField
|
/>
|
||||||
control={form.control}
|
<FormField
|
||||||
name="email"
|
control={form.control}
|
||||||
defaultValue=""
|
name="email"
|
||||||
render={({field}) => (
|
defaultValue=""
|
||||||
<FormItem>
|
render={({ field }) => (
|
||||||
<FormLabel>Email</FormLabel>
|
<FormItem>
|
||||||
<FormControl>
|
<FormLabel>Email</FormLabel>
|
||||||
<Input placeholder="example@portabase.io" {...field} />
|
<FormControl>
|
||||||
</FormControl>
|
<Input placeholder="example@portabase.io" {...field} />
|
||||||
<FormMessage/>
|
</FormControl>
|
||||||
</FormItem>
|
<FormMessage />
|
||||||
)}
|
</FormItem>
|
||||||
/>
|
)}
|
||||||
<FormField
|
/>
|
||||||
control={form.control}
|
<FormField
|
||||||
name="password"
|
control={form.control}
|
||||||
defaultValue=""
|
name="password"
|
||||||
render={({field}) => (
|
defaultValue=""
|
||||||
<FormItem>
|
render={({ field }) => (
|
||||||
<FormLabel className="flex">
|
<FormItem>
|
||||||
Password
|
<FormLabel className="flex">
|
||||||
<TooltipProvider>
|
Password
|
||||||
<Tooltip>
|
<TooltipProvider>
|
||||||
<TooltipTrigger asChild>
|
<Tooltip>
|
||||||
<Info className="ml-3" size="15"/>
|
<TooltipTrigger asChild>
|
||||||
</TooltipTrigger>
|
<Info size="15" />
|
||||||
<TooltipContent>
|
</TooltipTrigger>
|
||||||
<p>
|
<TooltipContent>
|
||||||
Min. 8 characters, 1 uppercase (A-Z), 1 lowercase (a-z), 1
|
<p>
|
||||||
number (0-9), 1 special character (!, @,
|
Min. 8 characters, 1 uppercase (A-Z), 1 lowercase
|
||||||
etc.)
|
(a-z), 1 number (0-9), 1 special character (!, @,
|
||||||
</p>
|
etc.)
|
||||||
</TooltipContent>
|
</p>
|
||||||
</Tooltip>
|
</TooltipContent>
|
||||||
</TooltipProvider>
|
</Tooltip>
|
||||||
</FormLabel>
|
</TooltipProvider>
|
||||||
<FormControl>
|
</FormLabel>
|
||||||
<PasswordInput placeholder="Your password" {...field} />
|
<FormControl>
|
||||||
</FormControl>
|
<PasswordInput placeholder="Your password" {...field} />
|
||||||
<FormMessage/>
|
</FormControl>
|
||||||
</FormItem>
|
<FormMessage />
|
||||||
)}
|
</FormItem>
|
||||||
/>
|
)}
|
||||||
<FormField
|
/>
|
||||||
control={form.control}
|
<FormField
|
||||||
name="confirmPassword"
|
control={form.control}
|
||||||
defaultValue=""
|
name="confirmPassword"
|
||||||
render={({field}) => (
|
defaultValue=""
|
||||||
<FormItem>
|
render={({ field }) => (
|
||||||
<FormLabel>Password Confirmation</FormLabel>
|
<FormItem>
|
||||||
<FormControl>
|
<FormLabel>Password Confirmation</FormLabel>
|
||||||
<PasswordInput placeholder="Conform your password" {...field} />
|
<FormControl>
|
||||||
</FormControl>
|
<PasswordInput
|
||||||
<FormMessage/>
|
placeholder="Conform your password"
|
||||||
</FormItem>
|
{...field}
|
||||||
)}
|
/>
|
||||||
/>
|
</FormControl>
|
||||||
<Button type="submit" className="h-11" disabled={mutation.isPending}>
|
<FormMessage />
|
||||||
Sign up
|
</FormItem>
|
||||||
</Button>
|
)}
|
||||||
<div className="mt-4 text-center text-sm">
|
/>
|
||||||
Already have an account ?{" "}
|
<Button
|
||||||
<Link href="/login" className="underline">
|
type="submit"
|
||||||
Sign in
|
className="h-11"
|
||||||
</Link>
|
disabled={mutation.isPending}
|
||||||
</div>
|
>
|
||||||
|
Sign up
|
||||||
</Form>
|
</Button>
|
||||||
</CardContent>
|
<div className="mt-4 text-center text-sm">
|
||||||
</CardAuth>
|
Already have an account ?{" "}
|
||||||
</TooltipProvider>
|
<Link href="/login" className="underline">
|
||||||
);
|
Sign in
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</Form>
|
||||||
|
</CardContent>
|
||||||
|
</CardAuth>
|
||||||
|
</TooltipProvider>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,29 +1,54 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import React, { useState } from "react";
|
import { useState } from "react";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { authClient } from "@/lib/auth/auth-client";
|
import { authClient, passkey } from "@/lib/auth/auth-client";
|
||||||
import { Loader2 } from "lucide-react";
|
import { Loader2 } from "lucide-react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import {AuthProviderConfig} from "../../../../portabase.config";
|
import { Icon } from "@iconify/react";
|
||||||
import {Icon} from "@iconify/react";
|
import { AuthProviderConfig } from "@/lib/auth/config";
|
||||||
|
import Image from "next/image";
|
||||||
|
import { useRouter } from "next/navigation";
|
||||||
|
|
||||||
export function SocialAuthButtons({ providers }: { providers: AuthProviderConfig[] }) {
|
export function SocialAuthButtons({ providers }: { providers: AuthProviderConfig[] }) {
|
||||||
const socialProviders = providers.filter(p => p.isActive && !p.isManual);
|
const socialProviders = providers.filter((p) => p.isActive && p.type !== "credential");
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
const [isLoading, setIsLoading] = useState<string | null>(null);
|
const [isLoading, setIsLoading] = useState<string | null>(null);
|
||||||
|
|
||||||
const handleSocialSignIn = async (providerId: string) => {
|
const handleSocialSignIn = async (provider: AuthProviderConfig) => {
|
||||||
setIsLoading(providerId);
|
setIsLoading(provider.id);
|
||||||
try {
|
try {
|
||||||
const { error } = await authClient.signIn.social({
|
let result;
|
||||||
provider: providerId as "google" | "github",
|
if (provider.id === "passkey") {
|
||||||
callbackURL: "/dashboard",
|
result = await authClient.signIn.passkey({
|
||||||
});
|
fetchOptions: {
|
||||||
|
onSuccess() {
|
||||||
if (error) {
|
router.push("/dashboard");
|
||||||
toast.error("An error occurred while signing in with the provider. Please try again.");
|
},
|
||||||
|
onError() {
|
||||||
|
toast.error("An error occurred during passkey authentication. Please try again.");
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} else if (provider.type === "sso") {
|
||||||
|
result = await authClient.signIn.sso({
|
||||||
|
providerId: provider.id,
|
||||||
|
providerType: "oidc",
|
||||||
|
callbackURL: "/dashboard",
|
||||||
|
});
|
||||||
|
console.log(result);
|
||||||
} else {
|
} else {
|
||||||
|
result = await authClient.signIn.social({
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
provider: provider.id as any,
|
||||||
|
callbackURL: "/dashboard",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result?.error) {
|
||||||
|
toast.error("An error occurred while signing in with the provider. Please try again.");
|
||||||
|
} else if (provider.id !== "passkey") {
|
||||||
toast.success("Redirecting to provider...");
|
toast.success("Redirecting to provider...");
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -38,29 +63,24 @@ export function SocialAuthButtons({ providers }: { providers: AuthProviderConfig
|
|||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-2 w-full">
|
<div className="flex flex-col gap-2 w-full">
|
||||||
{socialProviders.map((provider) => (
|
{socialProviders.map((provider) => (
|
||||||
<Button key={provider.id} variant="outline" className="w-full gap-2" onClick={() => handleSocialSignIn(provider.id)} disabled={!!isLoading}>
|
<Button key={provider.id} variant="outline" className="w-full gap-2" onClick={() => handleSocialSignIn(provider)} disabled={!!isLoading}>
|
||||||
{isLoading === provider.id ? <Loader2 className="h-4 w-4 animate-spin" /> :
|
{isLoading === provider.id ? (
|
||||||
<Icon icon={provider.icon} className="h-4 w-4"/>
|
<Loader2 className="h-4 w-4 animate-spin" />
|
||||||
}
|
) : provider.icon.startsWith("/") || provider.icon.startsWith("http") ? (
|
||||||
<span>{PROVIDERS_TEXT[provider.id].title}</span>
|
<Image
|
||||||
|
src={provider.icon}
|
||||||
|
alt={provider.id || "icon"}
|
||||||
|
width={16}
|
||||||
|
height={16}
|
||||||
|
className="h-4 w-4"
|
||||||
|
unoptimized={provider.icon.startsWith("http")}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<Icon icon={provider.icon} className="h-4 w-4" />
|
||||||
|
)}
|
||||||
|
<span>{provider.title || provider.name}</span>
|
||||||
</Button>
|
</Button>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
const PROVIDERS_TEXT = {
|
|
||||||
credential: {
|
|
||||||
title: "Password",
|
|
||||||
description: "Use your email address and password to sign in."
|
|
||||||
},
|
|
||||||
google: {
|
|
||||||
title: "Google",
|
|
||||||
description: "Sign in with your Google account."
|
|
||||||
},
|
|
||||||
github: {
|
|
||||||
title: "GitHub",
|
|
||||||
description: "Sign in with your GitHub account."
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,161 +1,198 @@
|
|||||||
"use client"
|
"use client";
|
||||||
import {Button, ButtonVariantsProps} from "@/components/ui/button";
|
import { Button, ButtonVariantsProps } from "@/components/ui/button";
|
||||||
import {ReactNode, useState} from "react";
|
import { ReactNode, useState } from "react";
|
||||||
import {Loader2} from "lucide-react";
|
import { Loader2 } from "lucide-react";
|
||||||
import {Popover, PopoverContent, PopoverTrigger} from "@/components/ui/popover";
|
import {
|
||||||
import {cn} from "@/lib/utils";
|
Popover,
|
||||||
import {Tooltip, TooltipContent, TooltipProvider, TooltipTrigger} from "@/components/ui/tooltip";
|
PopoverContent,
|
||||||
|
PopoverTrigger,
|
||||||
|
} from "@/components/ui/popover";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
import {
|
||||||
|
Tooltip,
|
||||||
|
TooltipContent,
|
||||||
|
TooltipProvider,
|
||||||
|
TooltipTrigger,
|
||||||
|
} from "@/components/ui/tooltip";
|
||||||
|
|
||||||
export type ButtonWithConfirmProps = {
|
export type ButtonWithConfirmProps = {
|
||||||
title: string;
|
title: string;
|
||||||
description: string;
|
description: string;
|
||||||
button?: {
|
button?: {
|
||||||
main: {
|
main: {
|
||||||
className?: string;
|
className?: string;
|
||||||
type?: "button" | "submit" | "reset" | undefined;
|
type?: "button" | "submit" | "reset" | undefined;
|
||||||
text?: string;
|
text?: string;
|
||||||
icon?: any;
|
icon?: any;
|
||||||
variant?: ButtonVariantsProps["variant"];
|
variant?: ButtonVariantsProps["variant"];
|
||||||
size?: ButtonVariantsProps["size"];
|
size?: ButtonVariantsProps["size"];
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
tooltipText?: string;
|
tooltipText?: string;
|
||||||
};
|
|
||||||
confirm: {
|
|
||||||
className?: string;
|
|
||||||
text: string;
|
|
||||||
icon?: any;
|
|
||||||
variant?: ButtonVariantsProps["variant"];
|
|
||||||
size?: ButtonVariantsProps["size"];
|
|
||||||
onClick?: () => void;
|
|
||||||
};
|
|
||||||
cancel: {
|
|
||||||
className?: string;
|
|
||||||
text: string;
|
|
||||||
icon?: any;
|
|
||||||
variant?: ButtonVariantsProps["variant"];
|
|
||||||
size?: ButtonVariantsProps["size"];
|
|
||||||
onClick?: () => void;
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
children?: ReactNode;
|
confirm: {
|
||||||
onConfirm?: (e: React.MouseEvent) => void;
|
className?: string;
|
||||||
onCancel?: (e: React.MouseEvent) => void;
|
text: string;
|
||||||
confirmButtonText?: string;
|
icon?: any;
|
||||||
cancelButtonText?: string;
|
variant?: ButtonVariantsProps["variant"];
|
||||||
isPending?: boolean;
|
size?: ButtonVariantsProps["size"];
|
||||||
|
onClick?: () => void;
|
||||||
|
};
|
||||||
|
cancel: {
|
||||||
|
className?: string;
|
||||||
|
text: string;
|
||||||
|
icon?: any;
|
||||||
|
variant?: ButtonVariantsProps["variant"];
|
||||||
|
size?: ButtonVariantsProps["size"];
|
||||||
|
onClick?: () => void;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
children?: ReactNode;
|
||||||
|
onConfirm?: (e: React.MouseEvent) => void;
|
||||||
|
onCancel?: (e: React.MouseEvent) => void;
|
||||||
|
confirmButtonText?: string;
|
||||||
|
cancelButtonText?: string;
|
||||||
|
isPending?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
export const ButtonWithConfirm = (props: ButtonWithConfirmProps) => {
|
export const ButtonWithConfirm = (props: ButtonWithConfirmProps) => {
|
||||||
const [isConfirming, setIsConfirming] = useState(false);
|
const [isConfirming, setIsConfirming] = useState(false);
|
||||||
|
|
||||||
const isLegacy = !!props.button;
|
const isLegacy = !!props.button;
|
||||||
const isDisabled = isLegacy ? !!props.button?.main.disabled : false;
|
const isDisabled = isLegacy ? !!props.button?.main.disabled : false;
|
||||||
|
|
||||||
const handleConfirm = (e: React.MouseEvent) => {
|
const handleConfirm = (e: React.MouseEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
if (isLegacy) {
|
||||||
|
props.button?.confirm.onClick?.();
|
||||||
|
} else {
|
||||||
|
props.onConfirm?.(e);
|
||||||
|
}
|
||||||
|
setIsConfirming(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCancel = (e: React.MouseEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
if (isLegacy) {
|
||||||
|
props.button?.cancel.onClick?.();
|
||||||
|
} else {
|
||||||
|
props.onCancel?.(e);
|
||||||
|
}
|
||||||
|
setIsConfirming(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const triggerContent = isLegacy ? (
|
||||||
|
<Button
|
||||||
|
type={props.button?.main.type}
|
||||||
|
disabled={isDisabled}
|
||||||
|
variant={props.button?.main.variant ?? "default"}
|
||||||
|
size={props.button?.main.size ?? "default"}
|
||||||
|
className={props.button?.main.className}
|
||||||
|
onClick={(e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
if (isLegacy) {
|
if (!isDisabled) setIsConfirming(true);
|
||||||
props.button?.confirm.onClick?.();
|
}}
|
||||||
} else {
|
>
|
||||||
props.onConfirm?.(e);
|
{props.isPending && <Loader2 className="animate-spin mr-4" size={16} />}
|
||||||
}
|
{props.button?.main.icon}
|
||||||
setIsConfirming(false);
|
{props.button?.main.text && <span>{props.button.main.text}</span>}
|
||||||
};
|
</Button>
|
||||||
|
) : (
|
||||||
const handleCancel = (e: React.MouseEvent) => {
|
<div
|
||||||
|
onClick={(e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
if (isLegacy) {
|
setIsConfirming(true);
|
||||||
props.button?.cancel.onClick?.();
|
}}
|
||||||
} else {
|
>
|
||||||
props.onCancel?.(e);
|
{props.children}
|
||||||
}
|
</div>
|
||||||
setIsConfirming(false);
|
);
|
||||||
};
|
|
||||||
|
|
||||||
const triggerContent = isLegacy ? (
|
const withTooltip =
|
||||||
<Button
|
isLegacy && props.button?.main.tooltipText && isDisabled ? (
|
||||||
type={props.button?.main.type}
|
<TooltipProvider>
|
||||||
disabled={isDisabled}
|
<Tooltip>
|
||||||
variant={props.button?.main.variant ?? "default"}
|
<TooltipTrigger asChild>
|
||||||
size={props.button?.main.size ?? "default"}
|
<div>{triggerContent}</div>
|
||||||
className={props.button?.main.className}
|
</TooltipTrigger>
|
||||||
onClick={(e) => {
|
<TooltipContent>
|
||||||
e.preventDefault();
|
<p>{props.button?.main.tooltipText}</p>
|
||||||
e.stopPropagation();
|
</TooltipContent>
|
||||||
if (!isDisabled) setIsConfirming(true);
|
</Tooltip>
|
||||||
}}
|
</TooltipProvider>
|
||||||
>
|
|
||||||
{props.isPending && <Loader2 className="animate-spin mr-4" size={16}/>}
|
|
||||||
{props.button?.main.icon}
|
|
||||||
{props.button?.main.text && <span>{props.button.main.text}</span>}
|
|
||||||
</Button>
|
|
||||||
) : (
|
) : (
|
||||||
<div onClick={(e) => {
|
triggerContent
|
||||||
e.preventDefault();
|
|
||||||
e.stopPropagation();
|
|
||||||
setIsConfirming(true);
|
|
||||||
}}>
|
|
||||||
{props.children}
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const withTooltip = isLegacy && props.button?.main.tooltipText && isDisabled ? (
|
return (
|
||||||
<TooltipProvider>
|
<Popover open={isConfirming} onOpenChange={setIsConfirming}>
|
||||||
<Tooltip>
|
<PopoverTrigger asChild>{withTooltip}</PopoverTrigger>
|
||||||
<TooltipTrigger asChild>
|
<PopoverContent
|
||||||
<div>{triggerContent}</div>
|
className="w-80"
|
||||||
</TooltipTrigger>
|
onPointerDownOutside={(e) => e.preventDefault()}
|
||||||
<TooltipContent>
|
onInteractOutside={(e) => e.preventDefault()}
|
||||||
<p>{props.button?.main.tooltipText}</p>
|
onClick={(e) => e.stopPropagation()}
|
||||||
</TooltipContent>
|
onMouseMove={(e) => e.stopPropagation()}
|
||||||
</Tooltip>
|
onMouseEnter={(e) => e.stopPropagation()}
|
||||||
</TooltipProvider>
|
onMouseLeave={(e) => e.stopPropagation()}
|
||||||
) : triggerContent;
|
onOpenAutoFocus={(e) => e.preventDefault()}
|
||||||
|
>
|
||||||
return (
|
<div className="grid gap-4">
|
||||||
<Popover open={isConfirming} onOpenChange={setIsConfirming}>
|
<div className="space-y-2">
|
||||||
<PopoverTrigger asChild>
|
<h4 className="font-medium leading-none">{props.title}</h4>
|
||||||
{withTooltip}
|
<p className="text-sm text-muted-foreground">{props.description}</p>
|
||||||
</PopoverTrigger>
|
</div>
|
||||||
<PopoverContent
|
<div className="grid gap-2">
|
||||||
className="w-80"
|
<Button
|
||||||
onPointerDownOutside={(e) => e.preventDefault()}
|
onClick={handleConfirm}
|
||||||
onInteractOutside={(e) => e.preventDefault()}
|
variant={
|
||||||
onClick={(e) => e.stopPropagation()}
|
isLegacy
|
||||||
onMouseMove={(e) => e.stopPropagation()}
|
? (props.button?.confirm.variant ?? "default")
|
||||||
onMouseEnter={(e) => e.stopPropagation()}
|
: "default"
|
||||||
onMouseLeave={(e) => e.stopPropagation()}
|
}
|
||||||
onOpenAutoFocus={(e) => e.preventDefault()}
|
size={
|
||||||
|
isLegacy ? (props.button?.confirm.size ?? "default") : "default"
|
||||||
|
}
|
||||||
|
className={cn(
|
||||||
|
isLegacy ? props.button?.confirm.className : "",
|
||||||
|
"w-full",
|
||||||
|
)}
|
||||||
>
|
>
|
||||||
<div className="grid gap-4">
|
{props.isPending && (
|
||||||
<div className="space-y-2">
|
<Loader2 className="animate-spin mr-4" size={16} />
|
||||||
<h4 className="font-medium leading-none">{props.title}</h4>
|
)}
|
||||||
<p className="text-sm text-muted-foreground">{props.description}</p>
|
{isLegacy && props.button?.confirm.icon}
|
||||||
</div>
|
<span>
|
||||||
<div className="grid gap-2">
|
{isLegacy
|
||||||
<Button
|
? props.button?.confirm.text
|
||||||
onClick={handleConfirm}
|
: (props.confirmButtonText ?? "Confirm")}
|
||||||
variant={isLegacy ? (props.button?.confirm.variant ?? "default") : "default"}
|
</span>
|
||||||
size={isLegacy ? (props.button?.main.size ?? "default") : "default"}
|
</Button>
|
||||||
className={cn(isLegacy ? props.button?.main.className : "", "w-full")}
|
<Button
|
||||||
>
|
variant={
|
||||||
{props.isPending && <Loader2 className="animate-spin mr-4" size={16}/>}
|
isLegacy
|
||||||
{isLegacy && props.button?.confirm.icon}
|
? (props.button?.cancel.variant ?? "outline")
|
||||||
<span>{isLegacy ? props.button?.confirm.text : (props.confirmButtonText ?? "Confirm")}</span>
|
: "outline"
|
||||||
</Button>
|
}
|
||||||
<Button
|
onClick={handleCancel}
|
||||||
variant={isLegacy ? (props.button?.cancel.variant ?? "outline") : "outline"}
|
className={cn(
|
||||||
onClick={handleCancel}
|
isLegacy ? props.button?.cancel.className : "",
|
||||||
className={cn(isLegacy ? props.button?.main.className : "", "w-full")}
|
"w-full",
|
||||||
size={isLegacy ? (props.button?.main.size ?? "default") : "default"}
|
)}
|
||||||
>
|
size={
|
||||||
{isLegacy ? (props.button?.cancel.text ?? "Cancel") : (props.cancelButtonText ?? "Cancel")}
|
isLegacy ? (props.button?.cancel.size ?? "default") : "default"
|
||||||
</Button>
|
}
|
||||||
</div>
|
>
|
||||||
</div>
|
{isLegacy
|
||||||
</PopoverContent>
|
? (props.button?.cancel.text ?? "Cancel")
|
||||||
</Popover>
|
: (props.cancelButtonText ?? "Cancel")}
|
||||||
);
|
</Button>
|
||||||
};
|
</div>
|
||||||
|
</div>
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|||||||
@@ -0,0 +1,94 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useSearchParams } from "next/navigation";
|
||||||
|
import type React from "react";
|
||||||
|
import { useEffect } from "react";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
export const ErrorLayout = ({ children }: { children: React.ReactNode }) => {
|
||||||
|
const url = useSearchParams();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const error = url.get("error");
|
||||||
|
|
||||||
|
if (!error) return;
|
||||||
|
|
||||||
|
const errorMessages: Record<string, string> = {
|
||||||
|
pending: "Your account is not active.",
|
||||||
|
invalid_or_expired_token: "Password reset invalid token.",
|
||||||
|
account_already_linked_to_different_user:
|
||||||
|
"An account already exists with the same email address but different sign-in credentials. Sign in using a provider associated with this email address.",
|
||||||
|
USER_NOT_FOUND: "User not found",
|
||||||
|
FAILED_TO_CREATE_USER: "Failed to create user",
|
||||||
|
FAILED_TO_CREATE_SESSION: "Failed to create session",
|
||||||
|
FAILED_TO_UPDATE_USER: "Failed to update user",
|
||||||
|
FAILED_TO_GET_SESSION: "Failed to get session",
|
||||||
|
INVALID_PASSWORD: "Invalid password",
|
||||||
|
INVALID_EMAIL: "Invalid email",
|
||||||
|
INVALID_EMAIL_OR_PASSWORD: "Invalid email or password",
|
||||||
|
INVALID_USER: "Invalid user",
|
||||||
|
SOCIAL_ACCOUNT_ALREADY_LINKED: "Social account already linked",
|
||||||
|
PROVIDER_NOT_FOUND: "Provider not found",
|
||||||
|
INVALID_TOKEN: "Invalid token",
|
||||||
|
TOKEN_EXPIRED: "Token expired",
|
||||||
|
ID_TOKEN_NOT_SUPPORTED: "id_token not supported",
|
||||||
|
FAILED_TO_GET_USER_INFO: "Failed to get user info",
|
||||||
|
USER_EMAIL_NOT_FOUND: "User email not found",
|
||||||
|
EMAIL_NOT_VERIFIED: "Email not verified",
|
||||||
|
PASSWORD_TOO_SHORT: "Password too short",
|
||||||
|
PASSWORD_TOO_LONG: "Password too long",
|
||||||
|
USER_ALREADY_EXISTS: "User already exists.",
|
||||||
|
USER_ALREADY_EXISTS_USE_ANOTHER_EMAIL:
|
||||||
|
"User already exists. Use another email.",
|
||||||
|
EMAIL_CAN_NOT_BE_UPDATED: "Email can not be updated",
|
||||||
|
CREDENTIAL_ACCOUNT_NOT_FOUND: "Credential account not found",
|
||||||
|
SESSION_EXPIRED:
|
||||||
|
"Session expired. Re-authenticate to perform this action.",
|
||||||
|
FAILED_TO_UNLINK_LAST_ACCOUNT: "You can't unlink your last account",
|
||||||
|
ACCOUNT_NOT_FOUND: "Account not found",
|
||||||
|
USER_ALREADY_HAS_PASSWORD:
|
||||||
|
"User already has a password. Provide that to delete the account.",
|
||||||
|
CROSS_SITE_NAVIGATION_LOGIN_BLOCKED:
|
||||||
|
"Cross-site navigation login blocked. This request appears to be a CSRF attack.",
|
||||||
|
VERIFICATION_EMAIL_NOT_ENABLED: "Verification email isn't enabled",
|
||||||
|
EMAIL_ALREADY_VERIFIED: "Email is already verified",
|
||||||
|
EMAIL_MISMATCH: "Email mismatch",
|
||||||
|
SESSION_NOT_FRESH: "Session is not fresh",
|
||||||
|
LINKED_ACCOUNT_ALREADY_EXISTS: "Linked account already exists",
|
||||||
|
INVALID_ORIGIN: "Invalid origin",
|
||||||
|
INVALID_CALLBACK_URL: "Invalid callbackURL",
|
||||||
|
INVALID_REDIRECT_URL: "Invalid redirectURL",
|
||||||
|
INVALID_ERROR_CALLBACK_URL: "Invalid errorCallbackURL",
|
||||||
|
INVALID_NEW_USER_CALLBACK_URL: "Invalid newUserCallbackURL",
|
||||||
|
MISSING_OR_NULL_ORIGIN: "Missing or null Origin",
|
||||||
|
CALLBACK_URL_REQUIRED: "callbackURL is required",
|
||||||
|
FAILED_TO_CREATE_VERIFICATION: "Unable to create verification",
|
||||||
|
FIELD_NOT_ALLOWED: "Field not allowed to be set",
|
||||||
|
ASYNC_VALIDATION_NOT_SUPPORTED: "Async validation is not supported",
|
||||||
|
VALIDATION_ERROR: "Validation Error",
|
||||||
|
MISSING_FIELD: "Field is required",
|
||||||
|
METHOD_NOT_ALLOWED_DEFER_SESSION_REQUIRED:
|
||||||
|
"POST method requires deferSessionRefresh to be enabled in session config",
|
||||||
|
BODY_MUST_BE_AN_OBJECT: "Body must be an object",
|
||||||
|
PASSWORD_ALREADY_SET: "User already has a password set",
|
||||||
|
Sign_up_is_disabled: "Sign up is disabled by the administrator",
|
||||||
|
};
|
||||||
|
|
||||||
|
const match = Object.entries(errorMessages).find(([key]) =>
|
||||||
|
error.includes(key),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!match) return;
|
||||||
|
|
||||||
|
toast.error(match[1]);
|
||||||
|
|
||||||
|
const params = new URLSearchParams(url.toString());
|
||||||
|
params.delete("error");
|
||||||
|
const newUrl =
|
||||||
|
window.location.pathname +
|
||||||
|
(params.toString() ? `?${params.toString()}` : "");
|
||||||
|
|
||||||
|
window.history.replaceState({}, document.title, newUrl);
|
||||||
|
}, [url]);
|
||||||
|
|
||||||
|
return <>{children}</>;
|
||||||
|
};
|
||||||
+55
-12
@@ -1,26 +1,69 @@
|
|||||||
import {z} from "zod";
|
import {z} from "zod";
|
||||||
|
import {SlackChannelConfigSchema} from "./providers/notifications/forms/slack.schema";
|
||||||
|
import {SmtpChannelConfigSchema} from "./providers/notifications/forms/smtp.schema";
|
||||||
|
import {DiscordChannelConfigSchema} from "./providers/notifications/forms/discord.schema";
|
||||||
|
import {TelegramChannelConfigSchema} from "./providers/notifications/forms/telegram.schema";
|
||||||
|
import {GotifyChannelConfigSchema} from "./providers/notifications/forms/gotify.schema";
|
||||||
|
import {NtfyChannelConfigSchema} from "./providers/notifications/forms/ntfy.schema";
|
||||||
|
import {WebhookChannelConfigSchema} from "./providers/notifications/forms/webhook.schema";
|
||||||
|
import {S3ChannelConfigSchema} from "./providers/storages/forms/s3.schema";
|
||||||
|
import {GoogleDriveChannelConfigSchema} from "./providers/storages/forms/google-drive.schema";
|
||||||
|
import {LocalChannelConfigSchema} from "./providers/storages/forms/local.schema";
|
||||||
|
|
||||||
|
|
||||||
export const ChannelFormSchema = z.object({
|
const BaseChannelFormSchema = z.object({
|
||||||
name: z
|
name: z
|
||||||
.string()
|
.string()
|
||||||
.min(5, "Name must be at least 5 characters long")
|
.min(5, "Name must be at least 5 characters long")
|
||||||
.max(40, "Name must be at most 40 characters long"),
|
.max(40, "Name must be at most 40 characters long"),
|
||||||
config: z.record(z.union([z.string(), z.number(), z.boolean(), z.null(), z.undefined()])).optional(),
|
|
||||||
enabled: z.boolean().default(true),
|
enabled: z.boolean().default(true),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const NotificationChannelFormSchema = z.discriminatedUnion("provider", [
|
||||||
|
BaseChannelFormSchema.extend({
|
||||||
|
provider: z.literal("slack"),
|
||||||
|
config: SlackChannelConfigSchema,
|
||||||
|
}),
|
||||||
|
BaseChannelFormSchema.extend({
|
||||||
|
provider: z.literal("smtp"),
|
||||||
|
config: SmtpChannelConfigSchema,
|
||||||
|
}),
|
||||||
|
BaseChannelFormSchema.extend({
|
||||||
|
provider: z.literal("discord"),
|
||||||
|
config: DiscordChannelConfigSchema,
|
||||||
|
}),
|
||||||
|
BaseChannelFormSchema.extend({
|
||||||
|
provider: z.literal("telegram"),
|
||||||
|
config: TelegramChannelConfigSchema,
|
||||||
|
}),
|
||||||
|
BaseChannelFormSchema.extend({
|
||||||
|
provider: z.literal("gotify"),
|
||||||
|
config: GotifyChannelConfigSchema,
|
||||||
|
}),
|
||||||
|
BaseChannelFormSchema.extend({
|
||||||
|
provider: z.literal("ntfy"),
|
||||||
|
config: NtfyChannelConfigSchema,
|
||||||
|
}),
|
||||||
|
BaseChannelFormSchema.extend({
|
||||||
|
provider: z.literal("webhook"),
|
||||||
|
config: WebhookChannelConfigSchema,
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
|
||||||
export const NotificationChannelFormSchema = ChannelFormSchema.extend({
|
export const StorageChannelFormSchema = z.discriminatedUnion("provider", [
|
||||||
provider: z.enum(
|
BaseChannelFormSchema.extend({
|
||||||
["slack", "smtp", "discord", "telegram", "gotify", "ntfy", "webhook"],
|
provider: z.literal("s3"),
|
||||||
{required_error: "Provider is required"}
|
config: S3ChannelConfigSchema,
|
||||||
),
|
}),
|
||||||
});
|
BaseChannelFormSchema.extend({
|
||||||
|
provider: z.literal("google-drive"),
|
||||||
export const StorageChannelFormSchema = ChannelFormSchema.extend({
|
config: GoogleDriveChannelConfigSchema,
|
||||||
provider: z.enum(["local", "s3", "google-drive"], {required_error: "Provider is required"}),
|
}),
|
||||||
});
|
BaseChannelFormSchema.extend({
|
||||||
|
provider: z.literal("local"),
|
||||||
|
config: LocalChannelConfigSchema
|
||||||
|
})
|
||||||
|
]);
|
||||||
|
|
||||||
|
|
||||||
export type NotificationChannelFormType = z.infer<typeof NotificationChannelFormSchema>;
|
export type NotificationChannelFormType = z.infer<typeof NotificationChannelFormSchema>;
|
||||||
|
|||||||
+1
-1
@@ -224,7 +224,7 @@ export const ChannelForm = ({onSuccessAction, organization, defaultValues, kind}
|
|||||||
|
|
||||||
<div className="flex justify-between mt-4">
|
<div className="flex justify-between mt-4">
|
||||||
<div>
|
<div>
|
||||||
{kind == "storage" && (
|
{(!isCreate || kind == "storage") && (
|
||||||
<ChannelTestButton
|
<ChannelTestButton
|
||||||
kind={kind}
|
kind={kind}
|
||||||
organizationId={organization?.id}
|
organizationId={organization?.id}
|
||||||
|
|||||||
+3
-2
@@ -34,7 +34,7 @@ export const ChannelTestButton = ({channel, organizationId, kind}: NotifierTestC
|
|||||||
message: `We are testing channel ${channel.name}`,
|
message: `We are testing channel ${channel.name}`,
|
||||||
level: 'info',
|
level: 'info',
|
||||||
};
|
};
|
||||||
const result = await dispatchNotification(payload, undefined, channel, organizationId);
|
const result = await dispatchNotification(payload, undefined, channel.id, organizationId);
|
||||||
|
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
toast.success(result.message);
|
toast.success(result.message);
|
||||||
@@ -51,7 +51,8 @@ export const ChannelTestButton = ({channel, organizationId, kind}: NotifierTestC
|
|||||||
toast.success("Successfully connected to storage channel");
|
toast.success("Successfully connected to storage channel");
|
||||||
} else {
|
} else {
|
||||||
console.error(result);
|
console.error(result);
|
||||||
toast.error("An error occurred while testing the storage channel, check your configuration");
|
const responseText = result.response ? `(${result.response})` : "";
|
||||||
|
toast.error(`An error occurred while testing the storage channel, check your configuration ${responseText}`);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
toast.error("Not yet supported");
|
toast.error("Not yet supported");
|
||||||
|
|||||||
+35
-27
@@ -1,30 +1,38 @@
|
|||||||
import {UseFormReturn} from "react-hook-form";
|
import { UseFormReturn } from "react-hook-form";
|
||||||
import {FormControl, FormField, FormItem, FormLabel, FormMessage} from "@/components/ui/form";
|
import {
|
||||||
import {Input} from "@/components/ui/input";
|
FormControl,
|
||||||
import {Separator} from "@/components/ui/separator";
|
FormField,
|
||||||
|
FormItem,
|
||||||
|
FormLabel,
|
||||||
|
FormMessage,
|
||||||
|
} from "@/components/ui/form";
|
||||||
|
import { Separator } from "@/components/ui/separator";
|
||||||
|
import { PasswordInput } from "@/components/ui/password-input";
|
||||||
|
|
||||||
type NotifierDiscordFormProps = {
|
type NotifierDiscordFormProps = {
|
||||||
form: UseFormReturn<any, any, any>
|
form: UseFormReturn<any, any, any>;
|
||||||
}
|
};
|
||||||
|
|
||||||
export const NotifierDiscordForm = ({form}: NotifierDiscordFormProps) => {
|
export const NotifierDiscordForm = ({ form }: NotifierDiscordFormProps) => {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Separator className="my-1"/>
|
<Separator className="my-1" />
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="config.discordWebhook"
|
name="config.discordWebhook"
|
||||||
render={({field}) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>Discord Webhook URL</FormLabel>
|
<FormLabel>Discord Webhook URL</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input {...field} placeholder="https://discord.com/api/webhooks/..."/>
|
<PasswordInput
|
||||||
</FormControl>
|
{...field}
|
||||||
<FormMessage/>
|
placeholder="https://discord.com/api/webhooks/..."
|
||||||
</FormItem>
|
/>
|
||||||
)}
|
</FormControl>
|
||||||
/>
|
<FormMessage />
|
||||||
</>
|
</FormItem>
|
||||||
)
|
)}
|
||||||
}
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|||||||
+5
@@ -0,0 +1,5 @@
|
|||||||
|
import {z} from "zod";
|
||||||
|
|
||||||
|
export const DiscordChannelConfigSchema = z.object({
|
||||||
|
discordWebhook: z.string().url("Must be a valid URL"),
|
||||||
|
});
|
||||||
+46
-40
@@ -1,43 +1,49 @@
|
|||||||
import {UseFormReturn} from "react-hook-form";
|
import type { UseFormReturn } from "react-hook-form";
|
||||||
import {FormControl, FormField, FormItem, FormLabel, FormMessage} from "@/components/ui/form";
|
import {
|
||||||
import {Input} from "@/components/ui/input";
|
FormControl,
|
||||||
import {Separator} from "@/components/ui/separator";
|
FormField,
|
||||||
|
FormItem,
|
||||||
|
FormLabel,
|
||||||
|
FormMessage,
|
||||||
|
} from "@/components/ui/form";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { Separator } from "@/components/ui/separator";
|
||||||
|
import { PasswordInput } from "@/components/ui/password-input";
|
||||||
|
|
||||||
type NotifierGotifyFormProps = {
|
type NotifierGotifyFormProps = {
|
||||||
form: UseFormReturn<any, any, any>
|
form: UseFormReturn<any, any, any>;
|
||||||
}
|
};
|
||||||
|
|
||||||
export const NotifierGotifyForm = ({form}: NotifierGotifyFormProps) => {
|
export const NotifierGotifyForm = ({ form }: NotifierGotifyFormProps) => {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Separator className="my-1"/>
|
<Separator className="my-1" />
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="config.gotifyServerUrl"
|
name="config.gotifyServerUrl"
|
||||||
render={({field}) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>Gotify Server URL</FormLabel>
|
<FormLabel>Gotify Server URL</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input {...field} placeholder="https://gotify.example.com"/>
|
<Input {...field} placeholder="https://gotify.example.com" />
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormMessage/>
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="config.gotifyAppToken"
|
name="config.gotifyAppToken"
|
||||||
render={({field}) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>Application Token</FormLabel>
|
<FormLabel>Application Token</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input {...field} placeholder="A1b2C3d4E5f6G7h"/>
|
<PasswordInput {...field} placeholder="A1b2C3d4E5f6G7h" />
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormMessage/>
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|||||||
+6
@@ -0,0 +1,6 @@
|
|||||||
|
import {z} from "zod";
|
||||||
|
|
||||||
|
export const GotifyChannelConfigSchema = z.object({
|
||||||
|
gotifyServerUrl: z.string().url("Must be a valid URL"),
|
||||||
|
gotifyAppToken: z.string().min(1, "App token is required"),
|
||||||
|
});
|
||||||
+3
-2
@@ -2,6 +2,7 @@ import {UseFormReturn} from "react-hook-form";
|
|||||||
import {FormControl, FormField, FormItem, FormLabel, FormMessage} from "@/components/ui/form";
|
import {FormControl, FormField, FormItem, FormLabel, FormMessage} from "@/components/ui/form";
|
||||||
import {Input} from "@/components/ui/input";
|
import {Input} from "@/components/ui/input";
|
||||||
import {Separator} from "@/components/ui/separator";
|
import {Separator} from "@/components/ui/separator";
|
||||||
|
import {PasswordInput} from "@/components/ui/password-input";
|
||||||
|
|
||||||
type NotifierNtfyFormProps = {
|
type NotifierNtfyFormProps = {
|
||||||
form: UseFormReturn<any, any, any>
|
form: UseFormReturn<any, any, any>
|
||||||
@@ -48,7 +49,7 @@ export const NotifierNtfyForm = ({form}: NotifierNtfyFormProps) => {
|
|||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>Access Token (Optional)</FormLabel>
|
<FormLabel>Access Token (Optional)</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input {...field} placeholder="tk_..."/>
|
<PasswordInput {...field} placeholder="tk_..."/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<p className="text-xs text-muted-foreground">Only required for protected topics or self-hosted instances with auth.</p>
|
<p className="text-xs text-muted-foreground">Only required for protected topics or self-hosted instances with auth.</p>
|
||||||
<FormMessage/>
|
<FormMessage/>
|
||||||
@@ -79,7 +80,7 @@ export const NotifierNtfyForm = ({form}: NotifierNtfyFormProps) => {
|
|||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>Basic Auth Password (Optional)</FormLabel>
|
<FormLabel>Basic Auth Password (Optional)</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input {...field} type="password" placeholder="password"/>
|
<PasswordInput placeholder="password" {...field} />
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormMessage/>
|
<FormMessage/>
|
||||||
</FormItem>
|
</FormItem>
|
||||||
|
|||||||
+9
@@ -0,0 +1,9 @@
|
|||||||
|
import {z} from "zod";
|
||||||
|
|
||||||
|
export const NtfyChannelConfigSchema = z.object({
|
||||||
|
ntfyTopic: z.string().min(1, "Topic is required"),
|
||||||
|
ntfyServerUrl: z.string().url("Must be a valid URL").optional().or(z.literal('')),
|
||||||
|
ntfyToken: z.string().optional(),
|
||||||
|
ntfyUsername: z.string().optional(),
|
||||||
|
ntfyPassword: z.string().optional(),
|
||||||
|
});
|
||||||
+35
-28
@@ -1,31 +1,38 @@
|
|||||||
|
import { UseFormReturn } from "react-hook-form";
|
||||||
import {UseFormReturn} from "react-hook-form";
|
import {
|
||||||
import {FormControl, FormField, FormItem, FormLabel, FormMessage} from "@/components/ui/form";
|
FormControl,
|
||||||
import {Input} from "@/components/ui/input";
|
FormField,
|
||||||
import {Separator} from "@/components/ui/separator";
|
FormItem,
|
||||||
|
FormLabel,
|
||||||
|
FormMessage,
|
||||||
|
} from "@/components/ui/form";
|
||||||
|
import { Separator } from "@/components/ui/separator";
|
||||||
|
import { PasswordInput } from "@/components/ui/password-input";
|
||||||
|
|
||||||
type NotifierSmtpFormProps = {
|
type NotifierSmtpFormProps = {
|
||||||
form: UseFormReturn<any, any, any>
|
form: UseFormReturn<any, any, any>;
|
||||||
}
|
};
|
||||||
|
|
||||||
export const NotifierSlackForm = ({form}: NotifierSmtpFormProps) => {
|
export const NotifierSlackForm = ({ form }: NotifierSmtpFormProps) => {
|
||||||
return(
|
return (
|
||||||
<>
|
<>
|
||||||
<Separator className="my-1"/>
|
<Separator className="my-1" />
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="config.slackWebhook"
|
name="config.slackWebhook"
|
||||||
render={({field}) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>Slack Webhook URL</FormLabel>
|
<FormLabel>Slack Webhook URL</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input {...field} placeholder="https://hooks.slack.com/services/..."/>
|
<PasswordInput
|
||||||
</FormControl>
|
{...field}
|
||||||
<FormMessage/>
|
placeholder="https://hooks.slack.com/services/..."
|
||||||
</FormItem>
|
/>
|
||||||
)}
|
</FormControl>
|
||||||
/>
|
<FormMessage />
|
||||||
</>
|
</FormItem>
|
||||||
)
|
)}
|
||||||
}
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|||||||
+5
@@ -0,0 +1,5 @@
|
|||||||
|
import {z} from "zod";
|
||||||
|
|
||||||
|
export const SlackChannelConfigSchema = z.object({
|
||||||
|
slackWebhook: z.string().url("Must be a valid URL"),
|
||||||
|
});
|
||||||
+10
@@ -0,0 +1,10 @@
|
|||||||
|
import {z} from "zod";
|
||||||
|
|
||||||
|
export const SmtpChannelConfigSchema = z.object({
|
||||||
|
host: z.string().min(1, "Host is required"),
|
||||||
|
port: z.coerce.number().min(1, "Port is required"),
|
||||||
|
user: z.string().min(1, "User is required"),
|
||||||
|
password: z.string().min(1, "Password is required"),
|
||||||
|
from: z.string().min(1, "From is required"),
|
||||||
|
to: z.string().min(1, "To is required"),
|
||||||
|
});
|
||||||
+81
-45
@@ -1,48 +1,84 @@
|
|||||||
import {UseFormReturn} from "react-hook-form";
|
import { UseFormReturn } from "react-hook-form";
|
||||||
import {FormControl, FormField, FormItem, FormLabel, FormMessage} from "@/components/ui/form";
|
import {
|
||||||
import {Input} from "@/components/ui/input";
|
FormControl,
|
||||||
import {Separator} from "@/components/ui/separator";
|
FormField,
|
||||||
|
FormItem,
|
||||||
|
FormLabel,
|
||||||
|
FormMessage,
|
||||||
|
} from "@/components/ui/form";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { Separator } from "@/components/ui/separator";
|
||||||
|
import { PasswordInput } from "@/components/ui/password-input";
|
||||||
|
|
||||||
type NotifierTelegramFormProps = {
|
type NotifierTelegramFormProps = {
|
||||||
form: UseFormReturn<any, any, any>
|
form: UseFormReturn<any, any, any>;
|
||||||
}
|
};
|
||||||
|
|
||||||
export const NotifierTelegramForm = ({form}: NotifierTelegramFormProps) => {
|
export const NotifierTelegramForm = ({ form }: NotifierTelegramFormProps) => {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Separator className="my-1"/>
|
<Separator className="my-1" />
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="config.telegramBotToken"
|
name="config.telegramBotToken"
|
||||||
render={({field}) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>Telegram Bot Token</FormLabel>
|
<FormLabel>Telegram Bot Token</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input {...field} placeholder="123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11"/>
|
<PasswordInput
|
||||||
</FormControl>
|
{...field}
|
||||||
<FormMessage/>
|
placeholder="123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11"
|
||||||
</FormItem>
|
/>
|
||||||
)}
|
</FormControl>
|
||||||
/>
|
<FormMessage />
|
||||||
<FormField
|
</FormItem>
|
||||||
control={form.control}
|
)}
|
||||||
name="config.telegramChatId"
|
/>
|
||||||
render={({field}) => (
|
<FormField
|
||||||
<FormItem>
|
control={form.control}
|
||||||
<FormLabel>Telegram Chat ID</FormLabel>
|
name="config.telegramChatId"
|
||||||
<FormControl>
|
render={({ field }) => (
|
||||||
<Input {...field} placeholder="-100123456789 or 123456789"/>
|
<FormItem>
|
||||||
</FormControl>
|
<FormLabel>Telegram Chat ID</FormLabel>
|
||||||
<p className="text-xs text-muted-foreground">
|
<FormControl>
|
||||||
You must start the conversation with the bot first (<strong>/start</strong>). <br/>
|
<Input {...field} placeholder="-100123456789 or 123456789" />
|
||||||
For groups, add the bot to the group. <br/>
|
</FormControl>
|
||||||
You can use <a href="https://t.me/userinfobot" target="_blank" rel="noopener noreferrer" className="underline">@userinfobot</a> to find your ID.
|
<p className="text-xs text-muted-foreground">
|
||||||
</p>
|
You must start the conversation with the bot first (
|
||||||
<FormMessage/>
|
<strong>/start</strong>). <br />
|
||||||
</FormItem>
|
For groups, add the bot to the group. <br />
|
||||||
)}
|
You can use{" "}
|
||||||
/>
|
<a
|
||||||
</>
|
href="https://t.me/userinfobot"
|
||||||
)
|
target="_blank"
|
||||||
}
|
rel="noopener noreferrer"
|
||||||
|
className="underline"
|
||||||
|
>
|
||||||
|
@userinfobot
|
||||||
|
</a>{" "}
|
||||||
|
to find your ID.
|
||||||
|
</p>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="config.telegramTopicId"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Telegram Topic ID</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input {...field} placeholder="123456" />
|
||||||
|
</FormControl>
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
Unique identifier for the target message thread (topic) of the
|
||||||
|
forum; for forum supergroups only
|
||||||
|
</p>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|||||||
+20
@@ -0,0 +1,20 @@
|
|||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
export const TelegramChannelConfigSchema = z
|
||||||
|
.object({
|
||||||
|
telegramBotToken: z.string().min(1, "Bot token is required"),
|
||||||
|
telegramChatId: z.string().min(1, "Chat ID is required"),
|
||||||
|
telegramTopicId: z.string().optional(),
|
||||||
|
})
|
||||||
|
.refine(
|
||||||
|
(data) => {
|
||||||
|
if (data.telegramTopicId && !data.telegramChatId) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
{
|
||||||
|
message: "Chat ID is required when a Topic ID is provided",
|
||||||
|
path: ["telegramChatId"],
|
||||||
|
},
|
||||||
|
);
|
||||||
+69
-62
@@ -1,65 +1,72 @@
|
|||||||
import {UseFormReturn} from "react-hook-form";
|
import { UseFormReturn } from "react-hook-form";
|
||||||
import {FormControl, FormField, FormItem, FormLabel, FormMessage} from "@/components/ui/form";
|
import {
|
||||||
import {Input} from "@/components/ui/input";
|
FormControl,
|
||||||
import {Separator} from "@/components/ui/separator";
|
FormField,
|
||||||
|
FormItem,
|
||||||
|
FormLabel,
|
||||||
|
FormMessage,
|
||||||
|
} from "@/components/ui/form";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { Separator } from "@/components/ui/separator";
|
||||||
|
import { PasswordInput } from "@/components/ui/password-input";
|
||||||
|
|
||||||
type NotifierWebhookFormProps = {
|
type NotifierWebhookFormProps = {
|
||||||
form: UseFormReturn<any, any, any>
|
form: UseFormReturn<any, any, any>;
|
||||||
}
|
};
|
||||||
|
|
||||||
export const NotifierWebhookForm = ({form}: NotifierWebhookFormProps) => {
|
export const NotifierWebhookForm = ({ form }: NotifierWebhookFormProps) => {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Separator className="my-1"/>
|
<Separator className="my-1" />
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="config.webhookUrl"
|
name="config.webhookUrl"
|
||||||
render={({field}) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>Webhook URL</FormLabel>
|
<FormLabel>Webhook URL</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input {...field} placeholder="https://example.com/api/webhook"/>
|
<Input {...field} placeholder="https://example.com/api/webhook" />
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormMessage/>
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<div className="flex gap-4">
|
<div className="flex gap-4">
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="config.webhookSecretHeader"
|
name="config.webhookSecretHeader"
|
||||||
render={({field}) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>Header Name (Optional)</FormLabel>
|
<FormLabel>Header Name (Optional)</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input {...field} placeholder="X-Webhook-Secret"/>
|
<Input {...field} placeholder="X-Webhook-Secret" />
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormMessage/>
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="config.webhookSecret"
|
name="config.webhookSecret"
|
||||||
render={({field}) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>Secret Value (Optional)</FormLabel>
|
<FormLabel>Secret Value (Optional)</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input {...field} placeholder="Secret value..."/>
|
<PasswordInput {...field} placeholder="Secret value..." />
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormMessage/>
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-xs text-muted-foreground mt-2">
|
<p className="text-xs text-muted-foreground mt-2">
|
||||||
If provided, the secret will be sent in the specified header (defaults to <code>X-Webhook-Secret</code>).
|
If provided, the secret will be sent in the specified header (defaults
|
||||||
</p>
|
to <code>X-Webhook-Secret</code>).
|
||||||
</>
|
</p>
|
||||||
)
|
</>
|
||||||
}
|
);
|
||||||
|
};
|
||||||
|
|||||||
+7
@@ -0,0 +1,7 @@
|
|||||||
|
import {z} from "zod";
|
||||||
|
|
||||||
|
export const WebhookChannelConfigSchema = z.object({
|
||||||
|
webhookUrl: z.string().url("Must be a valid URL"),
|
||||||
|
webhookSecretHeader: z.string().optional(),
|
||||||
|
webhookSecret: z.string().optional(),
|
||||||
|
});
|
||||||
+8
@@ -0,0 +1,8 @@
|
|||||||
|
import {z} from "zod";
|
||||||
|
|
||||||
|
export const GoogleDriveChannelConfigSchema = z.object({
|
||||||
|
clientId: z.string().min(1, "Client ID is required"),
|
||||||
|
clientSecret: z.string().min(1, "Client Secret is required"),
|
||||||
|
folderId: z.string().min(1, "Folder ID is required"),
|
||||||
|
refreshToken: z.string().optional(),
|
||||||
|
});
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user