mirror of
https://github.com/Portabase/portabase.git
synced 2026-07-14 11:16:13 +02:00
Compare commits
146
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e13c5ddf51 | ||
|
|
90be3ab731 | ||
|
|
85c0445a64 | ||
|
|
6a227ccd7c | ||
|
|
3d9a21f854 | ||
|
|
d4229a985d | ||
|
|
c6e0921dea | ||
|
|
346a8a4375 | ||
|
|
4799faa827 | ||
|
|
2c6f5c38ed | ||
|
|
4a884ab5cc | ||
|
|
e59beaf749 | ||
|
|
0d4e1229ab | ||
|
|
8810fd91c9 | ||
|
|
7cf43c5e7c | ||
|
|
77183af08d | ||
|
|
3a2c5be5dd | ||
|
|
11c764c2f3 | ||
|
|
d128234ae6 | ||
|
|
ee31ee225d | ||
|
|
e2195616d3 | ||
|
|
2ca818b996 | ||
|
|
131f2b4538 | ||
|
|
6731c3fea9 | ||
|
|
d3da5e8c13 | ||
|
|
3b0d4225ce | ||
|
|
66f0930164 | ||
|
|
3979b83a0a | ||
|
|
b65e432564 | ||
|
|
340e7c3e00 | ||
|
|
dcdce63e15 | ||
|
|
c2eaed757d | ||
|
|
920743bea7 | ||
|
|
0970f5baae | ||
|
|
291aa13e9a | ||
|
|
e644cde514 | ||
|
|
2350e68f4f | ||
|
|
cce3b681e3 | ||
|
|
755799f19d | ||
|
|
ec02dc8579 | ||
|
|
7028982ba1 | ||
|
|
f04503e38e | ||
|
|
f1edffed17 | ||
|
|
fac87acee7 | ||
|
|
c46dd91ede | ||
|
|
648753f8cf | ||
|
|
76b3bda313 | ||
|
|
8102dd1a8f | ||
|
|
aceb4e7f4c | ||
|
|
4224389e96 | ||
|
|
7c66c10b6d | ||
|
|
bf06a2e50c | ||
|
|
8c69fdef95 | ||
|
|
883c1d2f57 | ||
|
|
e7f1652b4e | ||
|
|
ea07224a52 | ||
|
|
b655154182 | ||
|
|
a173892163 | ||
|
|
14f48c4a35 | ||
|
|
88aa4daf74 | ||
|
|
25326dc9be | ||
|
|
030cd13f7c | ||
|
|
d557620c81 | ||
|
|
3273f8dd11 | ||
|
|
646719557c | ||
|
|
1640b326be | ||
|
|
cc19c9ec19 | ||
|
|
bb84ab9697 | ||
|
|
0f7664201a | ||
|
|
0b8a8213e6 | ||
|
|
476b3da66b | ||
|
|
41c632ccfa | ||
|
|
dbf5addf12 | ||
|
|
8d414080a4 | ||
|
|
c8d390726c | ||
|
|
31285dd0d6 | ||
|
|
b9ef58044e | ||
|
|
e320f74dfc | ||
|
|
b9deb3cdf0 | ||
|
|
5dbf293e1e | ||
|
|
47f2308acc | ||
|
|
496be692e7 | ||
|
|
5df6dd169a | ||
|
|
7232f17cb7 | ||
|
|
5b1c3d0606 | ||
|
|
8e6ca64fa2 | ||
|
|
9116b4d126 | ||
|
|
4ce38d9857 | ||
|
|
8811c48384 | ||
|
|
078d0d1ee2 | ||
|
|
56d755afa6 | ||
|
|
9e3abb366f | ||
|
|
4ebac1c46f | ||
|
|
1476df7a1f | ||
|
|
833872603a | ||
|
|
737834310b | ||
|
|
4474c8ee5e | ||
|
|
ebe5c63a7b | ||
|
|
afd98ae652 | ||
|
|
8f905c8128 | ||
|
|
f43f30c9b9 | ||
|
|
6b1bea9b17 | ||
|
|
cdaed9766e | ||
|
|
d31a9ab2d2 | ||
|
|
07d49749fb | ||
|
|
a1f1da8a07 | ||
|
|
1b791fd287 | ||
|
|
2a4af58a51 | ||
|
|
b12f8ca044 | ||
|
|
519b3abd1d | ||
|
|
e08a5bd6b6 | ||
|
|
f6c597b42d | ||
|
|
f3b9032174 | ||
|
|
44b4ee2953 | ||
|
|
b77f79f42d | ||
|
|
8017bf7c8a | ||
|
|
85f4cda752 | ||
|
|
1fd2f553ed | ||
|
|
d68170eda8 | ||
|
|
51f27226e7 | ||
|
|
d54c366323 | ||
|
|
0e7cb9fbab | ||
|
|
63e83032f1 | ||
|
|
b4da6fdd9f | ||
|
|
12535a59e8 | ||
|
|
b345eda7aa | ||
|
|
660c1dcadf | ||
|
|
be66856034 | ||
|
|
80e3027107 | ||
|
|
3bf731055f | ||
|
|
362ca53efb | ||
|
|
7ab213e5e4 | ||
|
|
f0eafb1162 | ||
|
|
ad5baa7d6f | ||
|
|
5840e30afb | ||
|
|
8f95606dd7 | ||
|
|
ca9bc7ae94 | ||
|
|
1c97c97fa1 | ||
|
|
6f6c1c7c52 | ||
|
|
7b9cb2ecb9 | ||
|
|
fc029af654 | ||
|
|
110a69dc4b | ||
|
|
a61791fdb2 | ||
|
|
d9badeacba | ||
|
|
d24e534828 | ||
|
|
80bdca138e |
+38
-8
@@ -1,10 +1,10 @@
|
|||||||
# 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_DESCRIPTION="Portabase is a powerful database manager"
|
||||||
PROJECT_URL=http://localhost:8887
|
PROJECT_URL=http://localhost:8887
|
||||||
@@ -21,11 +21,41 @@ 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="* * * * *"
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,46 @@
|
|||||||
|
name: Auto Release bot
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
types: [closed]
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
release:
|
||||||
|
if: github.event.pull_request.merged == true
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/create-github-app-token@v1
|
||||||
|
id: app-token
|
||||||
|
with:
|
||||||
|
app-id: ${{ vars.APP_ID }}
|
||||||
|
private-key: ${{ secrets.APP_PRIVATE_KEY }}
|
||||||
|
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
token: ${{ steps.app-token.outputs.token }}
|
||||||
|
ref: ${{ github.base_ref }}
|
||||||
|
|
||||||
|
- uses: pnpm/action-setup@v4
|
||||||
|
|
||||||
|
- uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: "lts/*"
|
||||||
|
cache: "pnpm"
|
||||||
|
|
||||||
|
- run: pnpm install --frozen-lockfile
|
||||||
|
|
||||||
|
- run: git config --global user.name 'github-actions[bot]'
|
||||||
|
- run: git config --global user.email 'github-actions[bot]@users.noreply.github.com'
|
||||||
|
|
||||||
|
- name: Run release-it
|
||||||
|
run: |
|
||||||
|
git pull origin main
|
||||||
|
pnpm exec release-it --ci
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ steps.app-token.outputs.token }}
|
||||||
@@ -1,16 +1,8 @@
|
|||||||
name: GitHub Release
|
name: Discord Notification
|
||||||
|
|
||||||
on:
|
on:
|
||||||
workflow_call:
|
workflow_call:
|
||||||
inputs:
|
inputs:
|
||||||
prerelease:
|
|
||||||
required: false
|
|
||||||
type: boolean
|
|
||||||
default: false
|
|
||||||
make_latest:
|
|
||||||
required: false
|
|
||||||
type: boolean
|
|
||||||
default: false
|
|
||||||
discord_title:
|
discord_title:
|
||||||
required: true
|
required: true
|
||||||
type: string
|
type: string
|
||||||
@@ -27,59 +19,9 @@ on:
|
|||||||
required: true
|
required: true
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
create-release:
|
notify-discord:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
permissions:
|
|
||||||
contents: write
|
|
||||||
steps:
|
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
|
- name: Send Discord Notification
|
||||||
env:
|
env:
|
||||||
DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }}
|
DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }}
|
||||||
@@ -6,7 +6,7 @@ on:
|
|||||||
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 +14,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,34 +26,32 @@ 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:
|
||||||
platform: [ linux/amd64, linux/arm64 ]
|
platform: [linux/amd64, linux/arm64]
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- 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 }}:${GITHUB_REF#refs/tags/}-$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 +60,55 @@ 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: Extract Docker metadata
|
||||||
|
id: meta
|
||||||
|
uses: docker/metadata-action@v5
|
||||||
|
with:
|
||||||
|
images: ${{ inputs.image_name }}
|
||||||
|
tags: |
|
||||||
|
type=semver,pattern={{version}}
|
||||||
|
type=semver,pattern={{major}}.{{minor}}
|
||||||
|
type=semver,pattern={{major}}
|
||||||
|
type=raw,value=latest,enable=${{ inputs.add_latest }}
|
||||||
|
|
||||||
- name: Create and push manifest list
|
- 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"
|
docker buildx imagetools create $DOCKER_IMAGES $TAG_ARGS
|
||||||
|
|
||||||
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,47 @@
|
|||||||
|
name: PR Checker
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
- dev
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
validate-code:
|
||||||
|
name: Code Integrity Check
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Setup pnpm
|
||||||
|
uses: pnpm/action-setup@v4
|
||||||
|
|
||||||
|
- name: Setup Node.js
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: "lts/*"
|
||||||
|
cache: "pnpm"
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: pnpm install --frozen-lockfile
|
||||||
|
|
||||||
|
- name: Check code formatting
|
||||||
|
run: pnpm run format:check
|
||||||
|
continue-on-error: true
|
||||||
|
|
||||||
|
- name: Run Linter
|
||||||
|
run: pnpm run lint
|
||||||
|
continue-on-error: true
|
||||||
|
|
||||||
|
- name: Type Check
|
||||||
|
run: pnpm run typecheck
|
||||||
|
continue-on-error: true
|
||||||
|
|
||||||
|
- name: Run Tests
|
||||||
|
run: pnpm run test
|
||||||
|
continue-on-error: true
|
||||||
|
|
||||||
|
- name: Test Application Build
|
||||||
|
run: pnpm run build
|
||||||
@@ -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 }}
|
||||||
|
|||||||
@@ -3,11 +3,11 @@ name: Publish Docker image for release
|
|||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
tags:
|
tags:
|
||||||
- '*.*.*'
|
- "*.*.*"
|
||||||
- '!*-*'
|
- "!*-*"
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
contents: write
|
contents: read
|
||||||
packages: write
|
packages: write
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
@@ -19,14 +19,12 @@ jobs:
|
|||||||
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: false
|
discord_title: "New Release: ${{ github.ref_name }}"
|
||||||
make_latest: true
|
discord_color: 3066993
|
||||||
discord_title: "||@release-dashboard|| New release published"
|
|
||||||
discord_color: 5814783
|
|
||||||
discord_footer: "Portabase"
|
discord_footer: "Portabase"
|
||||||
secrets:
|
secrets:
|
||||||
DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }}
|
DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ on:
|
|||||||
branches: [ main ]
|
branches: [ main ]
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
sca-deps: # Dependency & container scan
|
sca-deps:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
@@ -17,15 +17,16 @@ jobs:
|
|||||||
ignore-unfixed: true
|
ignore-unfixed: true
|
||||||
|
|
||||||
|
|
||||||
secrets-gitleaks: # Secrets exposure
|
secrets-gitleaks:
|
||||||
|
if: github.event.pull_request.head.repo.fork == false
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0 # Fetch full history for gitleaks
|
fetch-depth: 0
|
||||||
- uses: gitleaks/gitleaks-action@v2
|
- uses: gitleaks/gitleaks-action@v2
|
||||||
env:
|
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
|
||||||
@@ -44,4 +44,6 @@ private/keys/*
|
|||||||
|
|
||||||
/.env
|
/.env
|
||||||
|
|
||||||
|
seeds/pocket-id/*.db
|
||||||
|
|
||||||
certificates
|
certificates
|
||||||
+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,26 @@
|
|||||||
|
{
|
||||||
|
"git": {
|
||||||
|
"commit": true,
|
||||||
|
"commitMessage": "chore: release v${version}",
|
||||||
|
"requireCleanWorkingDir": true,
|
||||||
|
"tag": true,
|
||||||
|
"tagName": "${version}",
|
||||||
|
"push": true
|
||||||
|
},
|
||||||
|
"github": {
|
||||||
|
"release": true,
|
||||||
|
"tokenRef": "GITHUB_TOKEN"
|
||||||
|
},
|
||||||
|
"plugins": {
|
||||||
|
"@release-it/conventional-changelog": {
|
||||||
|
"preset": "angular"
|
||||||
|
},
|
||||||
|
"@release-it/bumper": {
|
||||||
|
"out": {
|
||||||
|
"file": "CITATION.cff",
|
||||||
|
"path": "version",
|
||||||
|
"type": "text/plain"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+2
-2
@@ -26,5 +26,5 @@ keywords:
|
|||||||
- web-ui
|
- web-ui
|
||||||
- agent
|
- agent
|
||||||
license: Apache-2.0
|
license: Apache-2.0
|
||||||
version: 1.2.5-rc.16
|
version: 1.4.3
|
||||||
date-released: "2026-02-12"
|
date-released: "2026-03-02"
|
||||||
|
|||||||
@@ -186,7 +186,7 @@
|
|||||||
same "printed page" as the copyright notice for easier
|
same "printed page" as the copyright notice for easier
|
||||||
identification within third-party archives.
|
identification within third-party archives.
|
||||||
|
|
||||||
Copyright 2024 Soluce Technologies
|
Copyright 2024 Portabase
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
|||||||
@@ -2,3 +2,47 @@ 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"
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
<h3 align="center">Portabase</h3>
|
<h3 align="center">Portabase</h3>
|
||||||
|
|
||||||
<p align="center" style="margin-top: 20px; font-style: italic;">
|
<p align="center" style="margin-top: 20px; font-style: italic;">
|
||||||
<i>Portabase is a tool designed to simplify the backup and restoration of your database instances. It integrates seamlessly with <a href="https://github.com/Portabase/agent">Portabase agents</a> for managing operations securely and efficiently.</i>
|
<i>Portabase is a tool designed to simplify the backup and restoration of your database instances. It integrates seamlessly with <a href="https://github.com/Portabase/agent-rust">Portabase agents</a> for managing operations securely and efficiently.</i>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
|
||||||
@@ -19,11 +19,11 @@
|
|||||||
[](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://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 +34,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,14 +51,13 @@
|
|||||||
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)
|
||||||
|
- Docker Compose setup - [details](https://portabase.io/docs/dashboard/setup#docker-compose)
|
||||||
- Kubernetes with Helm (soon)
|
- Kubernetes with Helm (soon)
|
||||||
- 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.**
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## Contributors
|
## Contributors
|
||||||
|
|
||||||
[](https://github.com/Portabase/portabase/graphs/contributors)
|
[](https://github.com/Portabase/portabase/graphs/contributors)
|
||||||
@@ -89,4 +88,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>
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ export default async function GuardPage() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<TooltipProvider>
|
<TooltipProvider>
|
||||||
<CardAuth className="w-full max-w-md">
|
<CardAuth className="w-full">
|
||||||
<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">Two-factor verification</h1>
|
<h1 className="text-3xl font-bold">Two-factor verification</h1>
|
||||||
|
|||||||
+33
-20
@@ -1,25 +1,38 @@
|
|||||||
import React from "react";
|
import React, { Suspense } 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 { ErrorLayout } from "@/components/wrappers/common/error-layout";
|
||||||
|
|
||||||
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-full flex-1 flex-col justify-center py-12 sm:px-6 lg:px-8 ">
|
<div className="mt-4">{children}</div>
|
||||||
<div className="mx-auto w-full max-w-md">
|
|
||||||
<AuthLogoSection/>
|
|
||||||
<div>{children}</div>
|
|
||||||
</div>
|
|
||||||
<footer className="py-4 text-center text-xs justify-items-end text-muted-foreground">
|
|
||||||
Powered by <span className="font-medium">Soluce Technologies</span>
|
|
||||||
</footer>
|
|
||||||
</div>
|
</div>
|
||||||
)
|
</div>
|
||||||
|
<footer className="mt-8 text-center text-xs text-muted-foreground flex flex-col gap-1">
|
||||||
|
<p className="flex items-center justify-center gap-1">
|
||||||
|
Made with{" "}
|
||||||
|
<Heart className="size-3 fill-red-500 text-red-500 animate-pulse" />{" "}
|
||||||
|
by <span className="font-medium text-foreground">Portabase</span>
|
||||||
|
</p>
|
||||||
|
<p>v{env.NEXT_PUBLIC_PROJECT_VERSION}</p>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
+50
-42
@@ -1,52 +1,60 @@
|
|||||||
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";
|
||||||
|
import { en } from "zod/v4/locales";
|
||||||
|
|
||||||
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>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
@@ -1,11 +1,16 @@
|
|||||||
import { Card, 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 { ResetPasswordForm } from "@/components/wrappers/auth/login/reset-password-form/reset-password-form";
|
import { ResetPasswordForm } from "@/components/wrappers/auth/login/reset-password-form/reset-password-form";
|
||||||
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 { 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;
|
||||||
|
|
||||||
@@ -23,7 +28,7 @@ export default async function RoutePage(props: { searchParams: Promise<{ token:
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<TooltipProvider>
|
<TooltipProvider>
|
||||||
<Card className="w-full max-w-md shadow-lg">
|
<CardAuth className="w-full">
|
||||||
<CardHeader className="space-y-4">
|
<CardHeader className="space-y-4">
|
||||||
<div className="space-y-1 text-center">
|
<div className="space-y-1 text-center">
|
||||||
<h1 className="text-2xl font-bold tracking-tight">Set a new password</h1>
|
<h1 className="text-2xl font-bold tracking-tight">Set a new password</h1>
|
||||||
@@ -53,7 +58,7 @@ export default async function RoutePage(props: { searchParams: Promise<{ token:
|
|||||||
<CardContent>
|
<CardContent>
|
||||||
<ResetPasswordForm />
|
<ResetPasswordForm />
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</CardAuth>
|
||||||
</TooltipProvider>
|
</TooltipProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
+2
-10
@@ -6,8 +6,6 @@ import {eq, and, inArray} from "drizzle-orm";
|
|||||||
import * as drizzleDb from "@/db";
|
import * as drizzleDb from "@/db";
|
||||||
import {getOrganizationProjectDatabases} from "@/lib/services";
|
import {getOrganizationProjectDatabases} from "@/lib/services";
|
||||||
import {getActiveMember, getOrganization} from "@/lib/auth/auth";
|
import {getActiveMember, getOrganization} from "@/lib/auth/auth";
|
||||||
import {getOrganizationChannels} from "@/db/services/notification-channel";
|
|
||||||
import {getOrganizationStorageChannels} from "@/db/services/storage-channel";
|
|
||||||
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";
|
||||||
|
|
||||||
@@ -85,12 +83,6 @@ export default async function RoutePage(props: PageParams<{
|
|||||||
notFound();
|
notFound();
|
||||||
}
|
}
|
||||||
|
|
||||||
const organizationChannels = await getOrganizationChannels(organization.id);
|
|
||||||
const activeOrganizationChannels = organizationChannels.filter(channel => channel.enabled);
|
|
||||||
|
|
||||||
const organizationStorageChannels = await getOrganizationStorageChannels(organization.id);
|
|
||||||
const activeOrganizationStorageChannels = organizationStorageChannels.filter(channel => channel.enabled);
|
|
||||||
|
|
||||||
|
|
||||||
const successRate = totalBackups > 0 ? (successfulBackups / totalBackups) * 100 : null;
|
const successRate = totalBackups > 0 ? (successfulBackups / totalBackups) * 100 : null;
|
||||||
|
|
||||||
@@ -110,8 +102,8 @@ export default async function RoutePage(props: PageParams<{
|
|||||||
availableBackups={availableBackups}
|
availableBackups={availableBackups}
|
||||||
successRate={successRate}
|
successRate={successRate}
|
||||||
organizationId={organization.id}
|
organizationId={organization.id}
|
||||||
activeOrganizationChannels={activeOrganizationChannels}
|
activeOrganizationChannels={[]}
|
||||||
activeOrganizationStorageChannels={activeOrganizationStorageChannels}
|
activeOrganizationStorageChannels={[]}
|
||||||
/>
|
/>
|
||||||
</BackupModalProvider>
|
</BackupModalProvider>
|
||||||
</Page>
|
</Page>
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ export default async function RoutePage(props: PageParams<{}>) {
|
|||||||
organizationSlug={organization.slug}
|
organizationSlug={organization.slug}
|
||||||
data={projects}
|
data={projects}
|
||||||
cardItem={ProjectCard}
|
cardItem={ProjectCard}
|
||||||
cardsPerPage={6}
|
cardsPerPage={9}
|
||||||
numberOfColumns={3}
|
numberOfColumns={3}
|
||||||
/>
|
/>
|
||||||
) : isMember ? (
|
) : isMember ? (
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -102,6 +102,7 @@ export const PATCH = withAgentCheck(async (request: Request, {params, agent}: {
|
|||||||
}) => {
|
}) => {
|
||||||
try {
|
try {
|
||||||
const body: BodyPatch = await request.json();
|
const body: BodyPatch = await request.json();
|
||||||
|
console.log("body", body);
|
||||||
|
|
||||||
const status = body.status
|
const status = body.status
|
||||||
const backupId = body.backupId
|
const backupId = body.backupId
|
||||||
@@ -131,7 +132,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(
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -63,8 +63,7 @@ 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,
|
message: true,
|
||||||
|
|||||||
@@ -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,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import {NextResponse} from "next/server";
|
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";
|
||||||
|
|
||||||
|
|
||||||
export async function POST(request: Request) {
|
export async function POST(request: Request) {
|
||||||
@@ -29,7 +30,8 @@ export async function POST(request: Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
const uploadDir = path.join(process.cwd(), "/private/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);
|
||||||
|
|||||||
+26
-78
@@ -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();
|
||||||
|
|
||||||
const errorMap = {
|
useEffect(() => {
|
||||||
[Error.Configuration]: (
|
if (isPending) {
|
||||||
<p>
|
return;
|
||||||
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 error = searchParams.get("error");
|
||||||
const search = useSearchParams()
|
const params = new URLSearchParams();
|
||||||
const error = search.get("error") as Error
|
if (error) {
|
||||||
|
params.set("error", error);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
const destination = session ? "/dashboard/home" : "/login";
|
||||||
<div className="flex h-screen w-full flex-col items-center justify-center">
|
router.replace(`${destination}?${params.toString()}`);
|
||||||
<a
|
}, [isPending, session, router, searchParams]);
|
||||||
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 (
|
||||||
return (
|
<div className="flex h-screen w-full items-center justify-center">
|
||||||
<Suspense fallback={<div>Loading...</div>}>
|
<Loader2 className="h-8 w-8 animate-spin" />
|
||||||
<AuthErrorPageContent />
|
</div>
|
||||||
</Suspense>
|
);
|
||||||
)
|
|
||||||
}
|
}
|
||||||
@@ -145,4 +145,13 @@
|
|||||||
h1, h2, h3, h4, h5, h6 {
|
h1, h2, h3, h4, h5, h6 {
|
||||||
@apply font-title;
|
@apply font-title;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.scrollbar-hide {
|
||||||
|
-ms-overflow-style: none;
|
||||||
|
scrollbar-width: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scrollbar-hide::-webkit-scrollbar {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+32
-28
@@ -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 ?? "App Title";
|
||||||
|
|
||||||
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
-22
@@ -1,31 +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 {BackupModalProvider} from "@/components/wrappers/dashboard/database/backup/backup-modal-context";
|
|
||||||
|
|
||||||
|
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:
|
||||||
@@ -1,19 +1,18 @@
|
|||||||
name: portabase-prod
|
|
||||||
|
|
||||||
services:
|
services:
|
||||||
|
|
||||||
app:
|
app:
|
||||||
build:
|
# build:
|
||||||
context: .
|
# context: .
|
||||||
dockerfile: docker/dockerfile/Dockerfile
|
# dockerfile: docker/dockerfile/Dockerfile
|
||||||
target: prod
|
# target: prod
|
||||||
image: portabase/portabase:1.2.5-rc.9
|
image: portabase/portabase:1.4.0
|
||||||
ports:
|
ports:
|
||||||
- '8887:80'
|
- '8887:80'
|
||||||
environment:
|
environment:
|
||||||
TZ: "Europe/Paris"
|
TZ: "Europe/Paris"
|
||||||
env_file:
|
env_file:
|
||||||
- .env
|
- .env
|
||||||
|
volumes:
|
||||||
|
- portabase-data:/data
|
||||||
depends_on:
|
depends_on:
|
||||||
db:
|
db:
|
||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
@@ -36,4 +35,5 @@ services:
|
|||||||
retries: 5
|
retries: 5
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
|
portabase-data:
|
||||||
postgres-data:
|
postgres-data:
|
||||||
|
|||||||
+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 \
|
||||||
@@ -18,6 +17,21 @@ RUN apt-get update && apt-get install -y \
|
|||||||
&& rm -rf /var/lib/apt/lists/*
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
|
||||||
|
RUN mkdir -p /etc/apt/keyrings \
|
||||||
|
&& curl -fsSL https://www.postgresql.org/media/keys/ACCC4CF8.asc \
|
||||||
|
| gpg --dearmor -o /etc/apt/keyrings/postgresql.gpg \
|
||||||
|
&& echo "deb [signed-by=/etc/apt/keyrings/postgresql.gpg] http://apt.postgresql.org/pub/repos/apt bullseye-pgdg main" \
|
||||||
|
> /etc/apt/sources.list.d/pgdg.list
|
||||||
|
|
||||||
|
RUN apt-get update && apt-get install -y \
|
||||||
|
postgresql-18 \
|
||||||
|
postgresql-client-18 \
|
||||||
|
postgresql-contrib-18 \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
ENV PATH="/usr/lib/postgresql/18/bin:${PATH}"
|
||||||
|
|
||||||
|
|
||||||
FROM --platform=$BUILDPLATFORM golang:1.23-bookworm AS tusd-base
|
FROM --platform=$BUILDPLATFORM golang:1.23-bookworm AS tusd-base
|
||||||
|
|
||||||
ARG TUSD_VERSION=2.8.0
|
ARG TUSD_VERSION=2.8.0
|
||||||
@@ -76,6 +90,9 @@ ENV NODE_ENV=production
|
|||||||
ENV NEXT_TELEMETRY_DISABLED=1
|
ENV NEXT_TELEMETRY_DISABLED=1
|
||||||
ENV PORT=80
|
ENV PORT=80
|
||||||
ENV HOSTNAME="0.0.0.0"
|
ENV HOSTNAME="0.0.0.0"
|
||||||
|
ENV PGDATA=/data/postgres
|
||||||
|
ENV PRIVATE_PATH=/data/private
|
||||||
|
|
||||||
|
|
||||||
RUN addgroup --system --gid 1001 nodejs
|
RUN addgroup --system --gid 1001 nodejs
|
||||||
RUN adduser --system --uid 1001 nextjs
|
RUN adduser --system --uid 1001 nextjs
|
||||||
@@ -95,13 +112,13 @@ 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 \
|
RUN mkdir -p .next /data/private/uploads \
|
||||||
&& chown -R nextjs:nodejs .next /app/private /app/public
|
&& chown -R nextjs:nodejs .next /data/private /app/public
|
||||||
|
|
||||||
|
|
||||||
USER root
|
USER root
|
||||||
COPY ./docker/entrypoints/app-prod-entrypoint.sh /app/app-prod-entrypoint.sh
|
COPY ./docker/entrypoints/app-prod-entrypoint.sh /app/app-prod-entrypoint.sh
|
||||||
RUN chmod +x /app/app-prod-entrypoint.sh
|
RUN chmod +x /app/app-prod-entrypoint.sh
|
||||||
|
|
||||||
EXPOSE 80
|
EXPOSE 80
|
||||||
#USER nextjs
|
|
||||||
ENTRYPOINT ["sh","/app/app-prod-entrypoint.sh"]
|
ENTRYPOINT ["sh","/app/app-prod-entrypoint.sh"]
|
||||||
|
|||||||
@@ -7,9 +7,72 @@ else
|
|||||||
echo "[WARN] No TZ provided, using default container timezone"
|
echo "[WARN] No TZ provided, using default container timezone"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
mkdir -p /app/private/uploads/tmp
|
POSTGRES_BIN=$(ls -d /usr/lib/postgresql/*/bin | head -n 1)
|
||||||
|
|
||||||
|
if [ -z "$POSTGRES_BIN" ]; then
|
||||||
|
echo "PostgreSQL binaries not found"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
export PATH="$POSTGRES_BIN:$PATH"
|
||||||
|
|
||||||
|
if [ -z "$DATABASE_URL" ]; then
|
||||||
|
echo "[INFO] No DATABASE_URL provided, starting internal Postgres..."
|
||||||
|
|
||||||
|
mkdir -p "$PGDATA"
|
||||||
|
chown -R postgres:postgres "$PGDATA"
|
||||||
|
|
||||||
|
if [ ! -f "$PGDATA/PG_VERSION" ]; then
|
||||||
|
echo "[INFO] Initializing database cluster..."
|
||||||
|
if ! su postgres -c "initdb -D '$PGDATA'" > /dev/null 2>&1; then
|
||||||
|
echo "[ERROR] initdb failed"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! su postgres -c "pg_ctl -D '$PGDATA' \
|
||||||
|
-o \"-c listen_addresses='localhost' -c logging_collector=on\" \
|
||||||
|
-l $PGDATA/postgres.log -w start" > /dev/null 2>&1; then
|
||||||
|
echo "[ERROR] PostgreSQL failed to start"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
until su postgres -c "pg_isready -h 127.0.0.1 -p 5432" > /dev/null 2>&1; do
|
||||||
|
sleep 1
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "[INFO] PostgreSQL server is up and accepting connections"
|
||||||
|
|
||||||
|
DB_USER="${POSTGRES_USER:-portabase_user}"
|
||||||
|
DB_PASS="${POSTGRES_PASSWORD:-JaB6b1SUtIWYvt7srnOt}"
|
||||||
|
DB_NAME="${POSTGRES_DB:-portabase_db}"
|
||||||
|
|
||||||
|
USER_EXISTS=$(su postgres -c "psql -tAc \"SELECT 1 FROM pg_roles WHERE rolname='$DB_USER'\"" 2>/dev/null)
|
||||||
|
if [ "$USER_EXISTS" != "1" ]; then
|
||||||
|
if ! su postgres -c "psql -c \"CREATE USER $DB_USER WITH PASSWORD '$DB_PASS';\"" > /dev/null 2>&1; then
|
||||||
|
echo "[ERROR] Failed creating user"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
DB_EXISTS=$(su postgres -c "psql -tAc \"SELECT 1 FROM pg_database WHERE datname='$DB_NAME'\"" 2>/dev/null)
|
||||||
|
if [ "$DB_EXISTS" != "1" ]; then
|
||||||
|
if ! su postgres -c "psql -c \"CREATE DATABASE $DB_NAME OWNER $DB_USER;\"" > /dev/null 2>&1; then
|
||||||
|
echo "[ERROR] Failed creating database"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
export DATABASE_URL="postgres://$DB_USER:$DB_PASS@127.0.0.1:5432/$DB_NAME"
|
||||||
|
|
||||||
|
echo "[SUCCESS] Internal PostgreSQL started successfully"
|
||||||
|
echo "[SUCCESS] Database: $DB_NAME | User: $DB_USER | Host: 127.0.0.1:5432"
|
||||||
|
fi
|
||||||
|
|
||||||
|
|
||||||
|
mkdir -p /data/private/uploads/tmp
|
||||||
echo "▶ Starting tusd server..."
|
echo "▶ Starting tusd server..."
|
||||||
tusd --base-path /tus/files/ --upload-dir /app/private/uploads/tmp --hooks-http http://127.0.0.1:3000/api/tus/hooks --port 1080 --max-size 21474836480 &
|
tusd --base-path /tus/files/ --upload-dir /data/private/uploads/tmp --hooks-http http://127.0.0.1:3000/api/tus/hooks --port 1080 --max-size 21474836480 &
|
||||||
|
|
||||||
echo "▶ Starting Next.js server..."
|
echo "▶ Starting Next.js server..."
|
||||||
PORT=3000 node server.js &
|
PORT=3000 node server.js &
|
||||||
|
|||||||
+88
-83
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "portabase",
|
"name": "portabase",
|
||||||
"version": "1.2.5-rc.16",
|
"version": "1.4.11",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "next dev --turbopack -p 8887",
|
"dev": "next dev --turbopack -p 8887",
|
||||||
@@ -11,121 +11,126 @@
|
|||||||
"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": {
|
||||||
"@hookform/resolvers": "^5.0.1",
|
"@better-auth/passkey": "^1.4.19",
|
||||||
"@radix-ui/react-accordion": "^1.2.10",
|
"@better-auth/sso": "^1.4.19",
|
||||||
"@radix-ui/react-alert-dialog": "^1.1.13",
|
"@hookform/resolvers": "^5.2.2",
|
||||||
"@radix-ui/react-aspect-ratio": "^1.1.6",
|
"@radix-ui/react-accordion": "^1.2.12",
|
||||||
"@radix-ui/react-avatar": "^1.1.9",
|
"@radix-ui/react-alert-dialog": "^1.1.15",
|
||||||
"@radix-ui/react-checkbox": "^1.3.1",
|
"@radix-ui/react-aspect-ratio": "^1.1.8",
|
||||||
"@radix-ui/react-collapsible": "^1.1.10",
|
"@radix-ui/react-avatar": "^1.1.11",
|
||||||
"@radix-ui/react-context-menu": "^2.2.14",
|
"@radix-ui/react-checkbox": "^1.3.3",
|
||||||
"@radix-ui/react-dialog": "^1.1.13",
|
"@radix-ui/react-collapsible": "^1.1.12",
|
||||||
"@radix-ui/react-dropdown-menu": "^2.1.14",
|
"@radix-ui/react-context-menu": "^2.2.16",
|
||||||
"@radix-ui/react-hover-card": "^1.1.13",
|
"@radix-ui/react-dialog": "^1.1.15",
|
||||||
|
"@radix-ui/react-dropdown-menu": "^2.1.16",
|
||||||
|
"@radix-ui/react-hover-card": "^1.1.15",
|
||||||
"@radix-ui/react-icons": "^1.3.2",
|
"@radix-ui/react-icons": "^1.3.2",
|
||||||
"@radix-ui/react-label": "^2.1.6",
|
"@radix-ui/react-label": "^2.1.8",
|
||||||
"@radix-ui/react-menubar": "^1.1.14",
|
"@radix-ui/react-menubar": "^1.1.16",
|
||||||
"@radix-ui/react-navigation-menu": "^1.2.12",
|
"@radix-ui/react-navigation-menu": "^1.2.14",
|
||||||
"@radix-ui/react-popover": "^1.1.13",
|
"@radix-ui/react-popover": "^1.1.15",
|
||||||
"@radix-ui/react-progress": "^1.1.6",
|
"@radix-ui/react-progress": "^1.1.8",
|
||||||
"@radix-ui/react-radio-group": "^1.3.6",
|
"@radix-ui/react-radio-group": "^1.3.8",
|
||||||
"@radix-ui/react-scroll-area": "^1.2.8",
|
"@radix-ui/react-scroll-area": "^1.2.10",
|
||||||
"@radix-ui/react-select": "^2.2.4",
|
"@radix-ui/react-select": "^2.2.6",
|
||||||
"@radix-ui/react-separator": "^1.1.6",
|
"@radix-ui/react-separator": "^1.1.8",
|
||||||
"@radix-ui/react-slider": "^1.3.4",
|
"@radix-ui/react-slider": "^1.3.6",
|
||||||
"@radix-ui/react-slot": "^1.2.2",
|
"@radix-ui/react-slot": "^1.2.4",
|
||||||
"@radix-ui/react-switch": "^1.2.4",
|
"@radix-ui/react-switch": "^1.2.6",
|
||||||
"@radix-ui/react-tabs": "^1.1.11",
|
"@radix-ui/react-tabs": "^1.1.13",
|
||||||
"@radix-ui/react-toast": "^1.2.13",
|
"@radix-ui/react-toast": "^1.2.15",
|
||||||
"@radix-ui/react-toggle": "^1.1.8",
|
"@radix-ui/react-toggle": "^1.1.10",
|
||||||
"@radix-ui/react-toggle-group": "^1.1.9",
|
"@radix-ui/react-toggle-group": "^1.1.11",
|
||||||
"@radix-ui/react-tooltip": "^1.2.6",
|
"@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.4",
|
"@t3-oss/env-nextjs": "^0.13.10",
|
||||||
"@tanstack/react-query": "^5.76.1",
|
"@tanstack/react-query": "^5.90.21",
|
||||||
"@tanstack/react-table": "^8.21.3",
|
"@tanstack/react-table": "^8.21.3",
|
||||||
"@types/express": "^5.0.6",
|
"@types/express": "^5.0.6",
|
||||||
"@types/nodemailer": "^6.4.17",
|
"@types/nodemailer": "^6.4.23",
|
||||||
"@types/ws": "^8.18.1",
|
"@types/ws": "^8.18.1",
|
||||||
"@zenstackhq/runtime": "2.14.2",
|
"@zenstackhq/runtime": "2.14.2",
|
||||||
"argon2": "^0.43.0",
|
"argon2": "^0.43.1",
|
||||||
"bcrypt": "^6.0.0",
|
"bcrypt": "^6.0.0",
|
||||||
"better-auth": "1.4.5",
|
"better-auth": "1.4.18",
|
||||||
"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.6",
|
"dockerode": "^4.0.9",
|
||||||
"dotenv": "^16.5.0",
|
"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.5",
|
"minio": "^8.0.6",
|
||||||
"motion": "^12.23.24",
|
"motion": "^12.34.3",
|
||||||
"next": "16.1.5",
|
"next": "16.1.5",
|
||||||
"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.1",
|
"node-forge": "^1.3.3",
|
||||||
"nodemailer": "^7.0.3",
|
"nodemailer": "^7.0.13",
|
||||||
"npm-check-updates": "^18.0.1",
|
"npm-check-updates": "^18.3.1",
|
||||||
"pg": "^8.16.0",
|
"pg": "^8.18.0",
|
||||||
"prettier": "^3.8.0",
|
"prettier": "^3.8.1",
|
||||||
"react": "^19.2.0",
|
"react": "^19.2.4",
|
||||||
"react-day-picker": "9.7.0",
|
"react-day-picker": "9.7.0",
|
||||||
"react-dom": "^19.2.0",
|
"react-dom": "^19.2.4",
|
||||||
"react-dropzone": "^14.3.8",
|
"react-dropzone": "^14.4.1",
|
||||||
"react-email": "^4.0.13",
|
"react-email": "^4.3.2",
|
||||||
"react-hook-form": "^7.56.3",
|
"react-hook-form": "^7.71.2",
|
||||||
"react-qr-code": "^2.0.18",
|
"react-qr-code": "^2.0.18",
|
||||||
"react-resizable-panels": "^3.0.2",
|
"react-resizable-panels": "^3.0.6",
|
||||||
"react-twc": "^1.4.2",
|
"react-twc": "^1.5.1",
|
||||||
"react-use-measure": "^2.1.7",
|
"react-use-measure": "^2.1.7",
|
||||||
"recharts": "^2.15.3",
|
"recharts": "^2.15.4",
|
||||||
"sharp": "^0.34.5",
|
"sharp": "^0.34.5",
|
||||||
"socket.io": "^4.8.1",
|
"socket.io": "^4.8.3",
|
||||||
"socket.io-client": "^4.8.1",
|
"socket.io-client": "^4.8.3",
|
||||||
"sonner": "^2.0.3",
|
"sonner": "^2.0.7",
|
||||||
"swiper": "^12.0.3",
|
"swiper": "^12.1.2",
|
||||||
"tailwind-merge": "^3.3.0",
|
"tailwind-merge": "^3.5.0",
|
||||||
"uuid": "^11.1.0",
|
"uuid": "^11.1.0",
|
||||||
"vaul": "^1.1.2",
|
"vaul": "^1.1.2",
|
||||||
"ws": "^8.18.2",
|
"ws": "^8.19.0",
|
||||||
"zod": "^3.24.4"
|
"zod": "^3.25.76"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@iconify/react": "^6.0.0",
|
"@iconify/react": "^6.0.2",
|
||||||
"@react-email/preview-server": "4.3.2",
|
"@react-email/preview-server": "4.3.2",
|
||||||
"@react-email/render": "^2.0.1",
|
"@react-email/render": "^2.0.4",
|
||||||
"@tailwindcss/postcss": "^4.1.7",
|
"@release-it/bumper": "^7.0.5",
|
||||||
|
"@release-it/conventional-changelog": "^10.0.5",
|
||||||
|
"@tailwindcss/postcss": "^4.2.1",
|
||||||
"@types/eslint-plugin-tailwindcss": "^3.17.0",
|
"@types/eslint-plugin-tailwindcss": "^3.17.0",
|
||||||
"@types/node": "^22.15.18",
|
"@types/node": "^22.19.11",
|
||||||
"@types/node-forge": "^1",
|
"@types/node-forge": "^1.3.14",
|
||||||
"@types/pg": "^8.15.2",
|
"@types/pg": "^8.16.0",
|
||||||
"@types/react": "^19.1.4",
|
"@types/react": "^19.2.14",
|
||||||
"@types/react-dom": "^19.1.5",
|
"@types/react-dom": "^19.2.3",
|
||||||
"@zenstackhq/openapi": "^2.14.2",
|
"@zenstackhq/openapi": "^2.22.1",
|
||||||
"@zenstackhq/tanstack-query": "^2.14.2",
|
"@zenstackhq/tanstack-query": "^2.22.2",
|
||||||
"baseline-browser-mapping": "^2.9.19",
|
"baseline-browser-mapping": "^2.10.0",
|
||||||
"drizzle-kit": "^0.31.1",
|
"drizzle-kit": "^0.31.9",
|
||||||
"esbuild": "^0.27.2",
|
"esbuild": "^0.27.3",
|
||||||
"eslint": "^9.39.0",
|
"eslint": "^9.39.3",
|
||||||
"eslint-config-next": "^16.0.1",
|
"eslint-config-next": "^16.1.6",
|
||||||
"eslint-plugin-tailwindcss": "^3.18.0",
|
"eslint-plugin-tailwindcss": "^3.18.2",
|
||||||
"framer-motion": "^12.24.7",
|
"framer-motion": "^12.34.3",
|
||||||
"postcss": "^8.5.3",
|
"postcss": "^8.5.6",
|
||||||
"tailwindcss": "^4.1.7",
|
"release-it": "^19.2.4",
|
||||||
"tsx": "^4.19.4",
|
"tailwindcss": "^4.2.1",
|
||||||
"tw-animate-css": "^1.2.9",
|
"tsx": "^4.21.0",
|
||||||
"typescript": "^5.8.3",
|
"tw-animate-css": "^1.4.0",
|
||||||
|
"typescript": "^5.9.3",
|
||||||
"zenstack": "2.14.2"
|
"zenstack": "2.14.2"
|
||||||
},
|
},
|
||||||
"packageManager": "pnpm@10.29.2+sha512.bef43fa759d91fd2da4b319a5a0d13ef7a45bb985a3d7342058470f9d2051a3ba8674e629672654686ef9443ad13a82da2beb9eeb3e0221c87b8154fff9d74b8"
|
"packageManager": "pnpm@10.30.1+sha512.3590e550d5384caa39bd5c7c739f72270234b2f6059e13018f975c313b1eb9fefcc09714048765d4d9efe961382c312e624572c0420762bdc5d5940cdf9be73a"
|
||||||
}
|
}
|
||||||
|
|||||||
Generated
+3225
-1536
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: 40 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!"
|
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1 @@
|
|||||||
|
|
||||||
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,12 +208,14 @@ type UseZodFormProps<Z extends ZodSchema> = Exclude<
|
|||||||
schema: Z;
|
schema: Z;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
const useZodForm = <Z extends ZodSchema>({
|
const useZodForm = <Z extends ZodSchema>({
|
||||||
schema,
|
schema,
|
||||||
...formProps
|
...formProps
|
||||||
}: UseZodFormProps<Z>) =>
|
}: UseZodFormProps<Z>) =>
|
||||||
useForm({
|
useForm({
|
||||||
...formProps,
|
...formProps,
|
||||||
|
// @ts-ignore
|
||||||
resolver: zodResolver(schema),
|
resolver: zodResolver(schema),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import { motion, useInView, type SpringOptions, type UseInViewOptions } from 'mo
|
|||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
|
|
||||||
const githubButtonVariants = cva(
|
const githubButtonVariants = cva(
|
||||||
'cursor-pointer relative overflow-hidden will-change-transform backface-visibility-hidden transform-gpu transition-transform duration-200 ease-out hover:scale-105 group whitespace-nowrap focus-visible:outline-hidden inline-flex items-center justify-center whitespace-nowrap font-medium ring-offset-background disabled:pointer-events-none disabled:opacity-60 [&_svg]:shrink-0',
|
'cursor-pointer relative overflow-hidden will-change-transform backface-visibility-hidden transform-gpu group whitespace-nowrap focus-visible:outline-hidden inline-flex items-center justify-center whitespace-nowrap font-medium ring-offset-background disabled:pointer-events-none disabled:opacity-60 [&_svg]:shrink-0',
|
||||||
{
|
{
|
||||||
variants: {
|
variants: {
|
||||||
variant: {
|
variant: {
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ function Input({ className, type, ...props }: React.ComponentProps<"input">) {
|
|||||||
type={type}
|
type={type}
|
||||||
data-slot="input"
|
data-slot="input"
|
||||||
className={cn(
|
className={cn(
|
||||||
"file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input flex h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
|
"file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input flex w-full min-w-0 rounded-md border bg-transparent px-4 py-2 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
|
||||||
"focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
|
"focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
|
||||||
"aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
|
"aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
|
||||||
className
|
className
|
||||||
|
|||||||
@@ -374,7 +374,7 @@ function SidebarContent({ className, ...props }: React.ComponentProps<"div">) {
|
|||||||
data-slot="sidebar-content"
|
data-slot="sidebar-content"
|
||||||
data-sidebar="content"
|
data-sidebar="content"
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex min-h-0 flex-1 flex-col gap-2 overflow-auto group-data-[collapsible=icon]:overflow-hidden",
|
"flex min-h-0 flex-1 flex-col gap-2 overflow-auto scrollbar-hide group-data-[collapsible=icon]:overflow-hidden",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
|
|||||||
@@ -41,9 +41,6 @@ export const AuthLogoSection = () => {
|
|||||||
|
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<span className="absolute bottom-8 right-5 text-sm text-muted-foreground" style={style}>
|
|
||||||
v{env.NEXT_PUBLIC_PROJECT_VERSION}
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -68,7 +68,7 @@ export const ForgotPasswordForm = (props: ForgotPasswordFormProps) => {
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="flex flex-col items-center gap-y-6 w-full">
|
<div className="flex flex-col items-center gap-y-6 w-full">
|
||||||
<ButtonWithLoading className="mt-2 w-full" isPending={mutation.isPending}>
|
<ButtonWithLoading className="mt-2 w-full h-11" isPending={mutation.isPending}>
|
||||||
Send reset link
|
Send reset link
|
||||||
</ButtonWithLoading>
|
</ButtonWithLoading>
|
||||||
<Link href="/login" className="group flex items-center text-sm hover:underline">
|
<Link href="/login" className="group flex items-center text-sm hover:underline">
|
||||||
|
|||||||
@@ -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" 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>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -82,7 +82,7 @@ export const ResetPasswordForm = (props: ResetPasswordFormProps) => {
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="flex flex-col items-center gap-y-6 w-full">
|
<div className="flex flex-col items-center gap-y-6 w-full">
|
||||||
<ButtonWithLoading className="mt-2 w-full" isPending={mutation.isPending}>
|
<ButtonWithLoading className="mt-2 w-full h-11" isPending={mutation.isPending}>
|
||||||
Reset
|
Reset
|
||||||
</ButtonWithLoading>
|
</ButtonWithLoading>
|
||||||
<Link href="/login" className="group flex items-center text-sm hover:underline">
|
<Link href="/login" className="group flex items-center text-sm hover:underline">
|
||||||
|
|||||||
@@ -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 bellow 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" 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>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ export const ResetPasswordForm = ({token}: ResetPasswordFormProps) => {
|
|||||||
<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">Fill information bellow to change your
|
<p className="text-balance text-muted-foreground">Fill information below to change your
|
||||||
password</p>
|
password</p>
|
||||||
</div>
|
</div>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
|
|||||||
@@ -1,29 +1,53 @@
|
|||||||
"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",
|
||||||
|
});
|
||||||
} 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 +62,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}</>;
|
||||||
|
};
|
||||||
@@ -34,7 +34,7 @@ export const GitHubStarsButtonCustom = () => {
|
|||||||
<GithubButton
|
<GithubButton
|
||||||
initialStars={0}
|
initialStars={0}
|
||||||
targetStars={stars}
|
targetStars={stars}
|
||||||
label="Github Stars"
|
label=""
|
||||||
size="sm"
|
size="sm"
|
||||||
separator={true}
|
separator={true}
|
||||||
roundStars={true}
|
roundStars={true}
|
||||||
|
|||||||
@@ -15,6 +15,8 @@ export type paginationNavigationProps = {
|
|||||||
export const PaginationNavigation = (props: paginationNavigationProps) => {
|
export const PaginationNavigation = (props: paginationNavigationProps) => {
|
||||||
const { className, totalPages, currentPage, goToPage, goToPrevPage, goToNextPage, maxVisiblePages = 3 } = props;
|
const { className, totalPages, currentPage, goToPage, goToPrevPage, goToNextPage, maxVisiblePages = 3 } = props;
|
||||||
|
|
||||||
|
if (totalPages <= 1) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Pagination className={cn("", className)}>
|
<Pagination className={cn("", className)}>
|
||||||
<PaginationContent>
|
<PaginationContent>
|
||||||
|
|||||||
@@ -14,6 +14,10 @@ interface tablePaginationProps {
|
|||||||
export function TablePagination(props: tablePaginationProps) {
|
export function TablePagination(props: tablePaginationProps) {
|
||||||
const {className, table, maxVisiblePages = 3, pageSizeOptions = [10, 20, 30, 40, 50]} = props;
|
const {className, table, maxVisiblePages = 3, pageSizeOptions = [10, 20, 30, 40, 50]} = props;
|
||||||
|
|
||||||
|
const totalPages = table.getPageCount();
|
||||||
|
|
||||||
|
if (totalPages <= 1) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={cn("flex gap-x-4", className)}
|
className={cn("flex gap-x-4", className)}
|
||||||
|
|||||||
+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(),
|
||||||
|
});
|
||||||
+1
-1
@@ -136,7 +136,7 @@ export const StorageGoogleDriveForm = ({form}: StorageGoogleDriveFormProps) => {
|
|||||||
<input type="hidden" {...form.register("config.refreshToken")} />
|
<input type="hidden" {...form.register("config.refreshToken")} />
|
||||||
|
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<Button type="button" onClick={handleConnect}>
|
<Button type="button" variant={"secondary"} className="hover:cursor-pointer" onClick={handleConnect}>
|
||||||
{isConnected ? "Reconnect Google Drive" : "Connect Google Drive"}
|
{isConnected ? "Reconnect Google Drive" : "Connect Google Drive"}
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
|
|||||||
+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(),
|
||||||
|
});
|
||||||
+3
@@ -0,0 +1,3 @@
|
|||||||
|
import {z} from "zod";
|
||||||
|
|
||||||
|
export const LocalChannelConfigSchema = z.object({});
|
||||||
+112
-111
@@ -1,115 +1,116 @@
|
|||||||
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,
|
||||||
import {PasswordInput} from "@/components/ui/password-input";
|
FormItem,
|
||||||
import {Switch} from "@/components/ui/switch";
|
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";
|
||||||
|
import { Switch } from "@/components/ui/switch";
|
||||||
|
|
||||||
type StorageS3FormProps = {
|
type StorageS3FormProps = {
|
||||||
form: UseFormReturn<any, any, any>
|
form: UseFormReturn<any, any, any>;
|
||||||
}
|
};
|
||||||
|
|
||||||
export const StorageS3Form = ({form}: StorageS3FormProps) => {
|
export const StorageS3Form = ({ form }: StorageS3FormProps) => {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Separator className="my-1"/>
|
<Separator className="my-1" />
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="config.endPointUrl"
|
name="config.endPointUrl"
|
||||||
render={({field}) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>Endpoint URL</FormLabel>
|
<FormLabel>Endpoint URL</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input {...field} placeholder="s3.amazonaws.com"/>
|
<Input {...field} placeholder="s3.amazonaws.com" />
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormMessage/>
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="config.region"
|
name="config.region"
|
||||||
render={({field}) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>Region</FormLabel>
|
<FormLabel>Region</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input {...field} placeholder="us-east-1"/>
|
<Input {...field} placeholder="us-east-1" />
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormMessage/>
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="config.accessKey"
|
name="config.accessKey"
|
||||||
render={({field}) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>Access Key</FormLabel>
|
<FormLabel>Access Key</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input {...field} placeholder="A1b2C3d4E5f6G7h"/>
|
<Input {...field} placeholder="A1b2C3d4E5f6G7h" />
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormMessage/>
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="config.secretKey"
|
name="config.secretKey"
|
||||||
render={({field}) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>Secret Key</FormLabel>
|
<FormLabel>Secret Key</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<PasswordInput {...field} placeholder="A1b2C3d4E5f6G7h"/>
|
<PasswordInput {...field} placeholder="A1b2C3d4E5f6G7h" />
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormMessage/>
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="config.bucketName"
|
name="config.bucketName"
|
||||||
render={({field}) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>Bucket name</FormLabel>
|
<FormLabel>Bucket name</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input {...field} placeholder="portabase-dev"/>
|
<Input {...field} placeholder="portabase-dev" />
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormMessage/>
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="config.port"
|
name="config.port"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>Port</FormLabel>
|
<FormLabel>Port</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input {...field} type="number" placeholder="443" />
|
<Input {...field} type="number" placeholder="443" />
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="config.ssl"
|
name="config.ssl"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>Use SSL</FormLabel>
|
<FormLabel>Use SSL</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Switch
|
<Switch checked={field.value} onCheckedChange={field.onChange} />
|
||||||
checked={field.value}
|
</FormControl>
|
||||||
onCheckedChange={field.onChange}
|
<FormMessage />
|
||||||
/>
|
</FormItem>
|
||||||
</FormControl>
|
)}
|
||||||
<FormMessage />
|
/>
|
||||||
</FormItem>
|
</>
|
||||||
)}
|
);
|
||||||
/>
|
};
|
||||||
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|||||||
+11
@@ -0,0 +1,11 @@
|
|||||||
|
import {z} from "zod";
|
||||||
|
|
||||||
|
export const S3ChannelConfigSchema = z.object({
|
||||||
|
endPointUrl: z.string().min(1, "Endpoint URL is required"),
|
||||||
|
region: z.string().optional(),
|
||||||
|
accessKey: z.string().min(1, "Access Key is required"),
|
||||||
|
secretKey: z.string().min(1, "Secret Key is required"),
|
||||||
|
bucketName: z.string().min(1, "Bucket name is required"),
|
||||||
|
port: z.coerce.number().optional(),
|
||||||
|
ssl: z.boolean().optional().default(true),
|
||||||
|
});
|
||||||
@@ -1,60 +1,65 @@
|
|||||||
"use client"
|
"use client";
|
||||||
import {NotificationChannelWith} from "@/db/schema/09_notification-channel";
|
import { NotificationChannelWith } from "@/db/schema/09_notification-channel";
|
||||||
import {CardsWithPagination} from "@/components/wrappers/common/cards-with-pagination";
|
import { CardsWithPagination } from "@/components/wrappers/common/cards-with-pagination";
|
||||||
import {useState} from "react";
|
import { useState } from "react";
|
||||||
import {EmptyStatePlaceholder} from "@/components/wrappers/common/empty-state-placeholder";
|
import { EmptyStatePlaceholder } from "@/components/wrappers/common/empty-state-placeholder";
|
||||||
import {OrganizationWithMembers} from "@/db/schema/03_organization";
|
import { OrganizationWithMembers } from "@/db/schema/03_organization";
|
||||||
import {StorageChannelWith} from "@/db/schema/12_storage-channel";
|
import { StorageChannelWith } from "@/db/schema/12_storage-channel";
|
||||||
import {ChannelCard} from "@/components/wrappers/dashboard/admin/channels/channel/channel-card/channel-card";
|
import { ChannelCard } from "@/components/wrappers/dashboard/admin/channels/channel/channel-card/channel-card";
|
||||||
import {ChannelAddEditModal} from "@/components/wrappers/dashboard/admin/channels/channel/channel-add-edit-modal";
|
import { ChannelAddEditModal } from "@/components/wrappers/dashboard/admin/channels/channel/channel-add-edit-modal";
|
||||||
import {ChannelKind, getChannelTextBasedOnKind} from "@/components/wrappers/dashboard/admin/channels/helpers/common";
|
import {
|
||||||
|
ChannelKind,
|
||||||
|
getChannelTextBasedOnKind,
|
||||||
|
} from "@/components/wrappers/dashboard/admin/channels/helpers/common";
|
||||||
|
|
||||||
type ChannelsSectionProps = {
|
type ChannelsSectionProps = {
|
||||||
channels: NotificationChannelWith[] | StorageChannelWith[],
|
channels: NotificationChannelWith[] | StorageChannelWith[];
|
||||||
organizations: OrganizationWithMembers[],
|
organizations: OrganizationWithMembers[];
|
||||||
kind: ChannelKind,
|
kind: ChannelKind;
|
||||||
defaultStorageChannelId?: string | null | undefined
|
defaultStorageChannelId?: string | null | undefined;
|
||||||
}
|
};
|
||||||
|
|
||||||
export const ChannelsSection = ({
|
export const ChannelsSection = ({
|
||||||
organizations,
|
organizations,
|
||||||
channels,
|
channels,
|
||||||
kind,
|
kind,
|
||||||
defaultStorageChannelId
|
defaultStorageChannelId,
|
||||||
}: ChannelsSectionProps) => {
|
}: ChannelsSectionProps) => {
|
||||||
|
const [isAddModalOpen, setIsAddModalOpen] = useState(false);
|
||||||
|
const channelText = getChannelTextBasedOnKind(kind);
|
||||||
|
const hasChannels = channels.length > 0;
|
||||||
|
|
||||||
const [isAddModalOpen, setIsAddModalOpen] = useState(false);
|
return (
|
||||||
const channelText = getChannelTextBasedOnKind(kind)
|
<div className="h-full">
|
||||||
const hasChannels = channels.length > 0
|
<ChannelAddEditModal
|
||||||
|
kind={kind}
|
||||||
|
open={isAddModalOpen}
|
||||||
return (
|
onOpenChangeAction={setIsAddModalOpen}
|
||||||
|
adminView={false}
|
||||||
|
trigger={false}
|
||||||
|
/>
|
||||||
|
{hasChannels ? (
|
||||||
<div className="h-full">
|
<div className="h-full">
|
||||||
<ChannelAddEditModal kind={kind} open={isAddModalOpen} onOpenChangeAction={setIsAddModalOpen}
|
<CardsWithPagination
|
||||||
adminView={false}
|
data={channels}
|
||||||
trigger={false}/>
|
cardItem={ChannelCard}
|
||||||
{hasChannels ? (
|
cardsPerPage={8}
|
||||||
<div className="h-full">
|
numberOfColumns={2}
|
||||||
<CardsWithPagination
|
adminView={true}
|
||||||
data={channels}
|
organizations={organizations}
|
||||||
cardItem={ChannelCard}
|
kind={kind}
|
||||||
cardsPerPage={8}
|
defaultStorageChannelId={defaultStorageChannelId}
|
||||||
numberOfColumns={2}
|
/>
|
||||||
adminView={true}
|
|
||||||
organizations={organizations}
|
|
||||||
kind={kind}
|
|
||||||
defaultStorageChannelId={defaultStorageChannelId}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<EmptyStatePlaceholder
|
|
||||||
text={`No ${channelText} channels configured yet`}
|
|
||||||
onClick={() => {
|
|
||||||
setIsAddModalOpen(true)
|
|
||||||
}}
|
|
||||||
className="h-full"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
) : (
|
||||||
}
|
<EmptyStatePlaceholder
|
||||||
|
text={`No ${channelText} channels configured yet`}
|
||||||
|
onClick={() => {
|
||||||
|
setIsAddModalOpen(true);
|
||||||
|
}}
|
||||||
|
className="h-full"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|||||||
+51
-4
@@ -4,7 +4,15 @@ import {Info} from "lucide-react";
|
|||||||
import {ButtonWithLoading} from "@/components/wrappers/common/button/button-with-loading";
|
import {ButtonWithLoading} from "@/components/wrappers/common/button/button-with-loading";
|
||||||
import {useRouter} from "next/navigation";
|
import {useRouter} from "next/navigation";
|
||||||
import {Setting} from "@/db/schema/01_setting";
|
import {Setting} from "@/db/schema/01_setting";
|
||||||
import {Form, FormControl, FormField, FormItem, FormLabel, FormMessage, useZodForm} from "@/components/ui/form";
|
import {
|
||||||
|
Form,
|
||||||
|
FormControl,
|
||||||
|
FormField,
|
||||||
|
FormItem,
|
||||||
|
FormLabel,
|
||||||
|
FormMessage,
|
||||||
|
useZodForm
|
||||||
|
} from "@/components/ui/form";
|
||||||
import {StorageChannelWith} from "@/db/schema/12_storage-channel";
|
import {StorageChannelWith} from "@/db/schema/12_storage-channel";
|
||||||
import {Select, SelectContent, SelectItem, SelectTrigger, SelectValue} from "@/components/ui/select";
|
import {Select, SelectContent, SelectItem, SelectTrigger, SelectValue} from "@/components/ui/select";
|
||||||
import {useMutation} from "@tanstack/react-query";
|
import {useMutation} from "@tanstack/react-query";
|
||||||
@@ -18,6 +26,7 @@ import {
|
|||||||
} from "@/components/wrappers/dashboard/admin/settings/storage/settings-storage.action";
|
} from "@/components/wrappers/dashboard/admin/settings/storage/settings-storage.action";
|
||||||
import {toast} from "sonner";
|
import {toast} from "sonner";
|
||||||
import {Switch} from "@/components/ui/switch";
|
import {Switch} from "@/components/ui/switch";
|
||||||
|
import {downloadMasterKeyAction} from "@/features/keys/keys.action";
|
||||||
|
|
||||||
export type SettingsStorageSectionProps = {
|
export type SettingsStorageSectionProps = {
|
||||||
settings: Setting;
|
settings: Setting;
|
||||||
@@ -50,6 +59,34 @@ export const SettingsStorageSection = ({settings, storageChannels}: SettingsStor
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
const handleDownload = async () => {
|
||||||
|
const result = await downloadMasterKeyAction();
|
||||||
|
|
||||||
|
if (!result?.success) {
|
||||||
|
toast.error(result?.message ?? "Download failed");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const byteCharacters = atob(result.data as string);
|
||||||
|
const byteNumbers = new Array(byteCharacters.length)
|
||||||
|
.fill(null)
|
||||||
|
.map((_, i) => byteCharacters.charCodeAt(i));
|
||||||
|
|
||||||
|
const blob = new Blob([new Uint8Array(byteNumbers)], {
|
||||||
|
type: "application/octet-stream",
|
||||||
|
});
|
||||||
|
|
||||||
|
const url = window.URL.createObjectURL(blob);
|
||||||
|
const a = document.createElement("a");
|
||||||
|
a.href = url;
|
||||||
|
a.download = "master_key.bin";
|
||||||
|
document.body.appendChild(a);
|
||||||
|
a.click();
|
||||||
|
a.remove();
|
||||||
|
window.URL.revokeObjectURL(url);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col h-full">
|
<div className="flex flex-col h-full">
|
||||||
<Alert className="mt-3">
|
<Alert className="mt-3">
|
||||||
@@ -59,7 +96,7 @@ export const SettingsStorageSection = ({settings, storageChannels}: SettingsStor
|
|||||||
The default storage channel will be used by default to store your backups if no storage policy is
|
The default storage channel will be used by default to store your backups if no storage policy is
|
||||||
configured at the database level. </AlertDescription>
|
configured at the database level. </AlertDescription>
|
||||||
</Alert>
|
</Alert>
|
||||||
<div className="flex flex-col h-full py-4 ">
|
<div className="flex flex-col h-full py-4 gap-3">
|
||||||
<Form
|
<Form
|
||||||
className="space-y-4"
|
className="space-y-4"
|
||||||
form={form}
|
form={form}
|
||||||
@@ -96,8 +133,6 @@ export const SettingsStorageSection = ({settings, storageChannels}: SettingsStor
|
|||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="encryption"
|
name="encryption"
|
||||||
@@ -120,6 +155,18 @@ export const SettingsStorageSection = ({settings, storageChannels}: SettingsStor
|
|||||||
Confirm
|
Confirm
|
||||||
</ButtonWithLoading>
|
</ButtonWithLoading>
|
||||||
</Form>
|
</Form>
|
||||||
|
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<span className="text-sm font-medium ">
|
||||||
|
Download instance <button
|
||||||
|
type="button"
|
||||||
|
onClick={handleDownload}
|
||||||
|
className="text-sm hover:text-muted-foreground underline underline-offset-4 transition-colors w-fit"
|
||||||
|
>
|
||||||
|
Master Key
|
||||||
|
</button>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,75 +1,86 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import {useMutation} from "@tanstack/react-query";
|
import { useMutation } from "@tanstack/react-query";
|
||||||
import {User} from "@/db/schema/02_user";
|
import { User } from "@/db/schema/02_user";
|
||||||
import {ButtonWithLoading} from "@/components/wrappers/common/button/button-with-loading";
|
import { ButtonWithLoading } from "@/components/wrappers/common/button/button-with-loading";
|
||||||
import {useRouter} from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
import {
|
import {
|
||||||
AlertDialog,
|
AlertDialog,
|
||||||
AlertDialogCancel,
|
AlertDialogCancel,
|
||||||
AlertDialogContent,
|
AlertDialogContent,
|
||||||
AlertDialogDescription,
|
AlertDialogDescription,
|
||||||
AlertDialogFooter,
|
AlertDialogFooter,
|
||||||
AlertDialogHeader,
|
AlertDialogHeader,
|
||||||
AlertDialogTitle,
|
AlertDialogTitle,
|
||||||
} from "@/components/ui/alert-dialog";
|
} from "@/components/ui/alert-dialog";
|
||||||
import {authClient} from "@/lib/auth/auth-client";
|
import { authClient } from "@/lib/auth/auth-client";
|
||||||
import {toast} from "sonner";
|
import { toast } from "sonner";
|
||||||
import {setSuperAdminOwnerOfOrganizationsOwnedByUser} from "@/components/wrappers/dashboard/admin/users/user.action";
|
import { setSuperAdminOwnerOfOrganizationsOwnedByUser } from "@/components/wrappers/dashboard/admin/users/user.action";
|
||||||
|
|
||||||
type AdminDeleteUserModalProps = {
|
type AdminDeleteUserModalProps = {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
user: User;
|
user: User;
|
||||||
onOpenChange: (open: boolean) => void;
|
onOpenChange: (open: boolean) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const AdminDeleteUserModal = ({user, open, onOpenChange}: AdminDeleteUserModalProps) => {
|
export const AdminDeleteUserModal = ({
|
||||||
const router = useRouter();
|
user,
|
||||||
|
open,
|
||||||
|
onOpenChange,
|
||||||
|
}: AdminDeleteUserModalProps) => {
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
const mutation = useMutation({
|
const mutation = useMutation({
|
||||||
mutationFn: async () => {
|
mutationFn: async () => {
|
||||||
const res = await setSuperAdminOwnerOfOrganizationsOwnedByUser({userId: user.id});
|
const res = await setSuperAdminOwnerOfOrganizationsOwnedByUser({
|
||||||
const result = res?.data;
|
userId: user.id,
|
||||||
if (result?.success) {
|
});
|
||||||
await authClient.admin.removeUser(
|
const result = res?.data;
|
||||||
{
|
if (result?.success) {
|
||||||
userId: user.id,
|
await authClient.admin.removeUser(
|
||||||
},
|
{
|
||||||
{
|
userId: user.id,
|
||||||
onSuccess: async (response) => {
|
},
|
||||||
toast.success(`User ${user.name} successfully deleted`);
|
{
|
||||||
onOpenChange(false);
|
onSuccess: async () => {
|
||||||
router.refresh();
|
toast.success(`User ${user.name} successfully deleted`);
|
||||||
},
|
onOpenChange(false);
|
||||||
onError: async (error) => {
|
router.refresh();
|
||||||
toast.error("An error has occurred while deleting user");
|
},
|
||||||
onOpenChange(false);
|
onError: async (error) => {
|
||||||
},
|
toast.error("An error has occurred while deleting user");
|
||||||
}
|
onOpenChange(false);
|
||||||
);
|
},
|
||||||
} else {
|
},
|
||||||
toast.error("An error has occurred while deleting user");
|
);
|
||||||
}
|
} else {
|
||||||
},
|
toast.error("An error has occurred while deleting user");
|
||||||
onError: (error) => {
|
}
|
||||||
toast.error(error.message);
|
},
|
||||||
},
|
onError: (error) => {
|
||||||
});
|
toast.error(error.message);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AlertDialog open={open} onOpenChange={onOpenChange}>
|
<AlertDialog open={open} onOpenChange={onOpenChange}>
|
||||||
<AlertDialogContent>
|
<AlertDialogContent>
|
||||||
<AlertDialogHeader>
|
<AlertDialogHeader>
|
||||||
<AlertDialogTitle>Are you sure you want to delete {user.name} ?</AlertDialogTitle>
|
<AlertDialogTitle>
|
||||||
<AlertDialogDescription>This action is irreversible: it will lead to the deletion of the user's
|
Are you sure you want to delete {user.name} ?
|
||||||
data.</AlertDialogDescription>
|
</AlertDialogTitle>
|
||||||
</AlertDialogHeader>
|
<AlertDialogDescription>
|
||||||
<AlertDialogFooter>
|
This action is irreversible: it will lead to the deletion of the
|
||||||
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
user's data.
|
||||||
<ButtonWithLoading
|
</AlertDialogDescription>
|
||||||
onClick={async () => await mutation.mutateAsync()}>Confirm</ButtonWithLoading>
|
</AlertDialogHeader>
|
||||||
</AlertDialogFooter>
|
<AlertDialogFooter>
|
||||||
</AlertDialogContent>
|
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||||
</AlertDialog>
|
<ButtonWithLoading onClick={async () => await mutation.mutateAsync()}>
|
||||||
);
|
Confirm
|
||||||
|
</ButtonWithLoading>
|
||||||
|
</AlertDialogFooter>
|
||||||
|
</AlertDialogContent>
|
||||||
|
</AlertDialog>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -5,8 +5,9 @@ import {User} from "@/db/schema/02_user";
|
|||||||
|
|
||||||
type AdminUserListProps = {
|
type AdminUserListProps = {
|
||||||
users: User[];
|
users: User[];
|
||||||
|
isPasswordAuthEnabled: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const AdminUserList = ({ users }: AdminUserListProps) => {
|
export const AdminUserList = ({ users, isPasswordAuthEnabled }: AdminUserListProps) => {
|
||||||
return <DataTable columns={usersListColumns()} data={users} enablePagination={true} enableSelect={false} />;
|
return <DataTable columns={usersListColumns({ isPasswordAuthEnabled })} data={users} enablePagination={true} enableSelect={false} />;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -9,7 +9,11 @@ import {Info} from "lucide-react";
|
|||||||
import {Table, TableBody, TableCell, TableRow} from "@/components/ui/table";
|
import {Table, TableBody, TableCell, TableRow} from "@/components/ui/table";
|
||||||
import {UserActionsCell} from "@/components/wrappers/dashboard/admin/users/user-actions-cell";
|
import {UserActionsCell} from "@/components/wrappers/dashboard/admin/users/user-actions-cell";
|
||||||
|
|
||||||
export function usersListColumns(): ColumnDef<User>[] {
|
type UsersListColumnsProps = {
|
||||||
|
isPasswordAuthEnabled: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function usersListColumns({ isPasswordAuthEnabled }: UsersListColumnsProps): ColumnDef<User>[] {
|
||||||
|
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
@@ -85,7 +89,7 @@ export function usersListColumns(): ColumnDef<User>[] {
|
|||||||
{
|
{
|
||||||
id: "actions",
|
id: "actions",
|
||||||
header: "Actions",
|
header: "Actions",
|
||||||
cell: ({row}) => <UserActionsCell user={row.original}/>,
|
cell: ({row}) => <UserActionsCell user={row.original} isPasswordAuthEnabled={isPasswordAuthEnabled} />,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user