mirror of
https://github.com/Ravinou/borgwarehouse
synced 2024-06-29 10:50:32 +02:00
Compare commits
417 commits
Author | SHA1 | Date | |
---|---|---|---|
26530d6bdc | |||
13aa4055ff | |||
6763cac795 | |||
2085a7bb67 | |||
b7ec34588f | |||
f7de065ee2 | |||
b620cd27b8 | |||
d51103c4f5 | |||
de3fa3f3af | |||
f8b6c46d80 | |||
8a7e30d008 | |||
7dbe181629 | |||
36753d0aa0 | |||
67d959f9fb | |||
81ed817045 | |||
02efa8ad94 | |||
3f8fbb77bb | |||
52ed551200 | |||
ea5d565567 | |||
fa218b0522 | |||
56098d3f8d | |||
08e6b0e6a6 | |||
a79a91ecb0 | |||
779cceacf2 | |||
94fd0c6188 | |||
09224e6f24 | |||
0ed03e9433 | |||
76d11d83f7 | |||
6b43c38cc2 | |||
7687f10b7e | |||
9c28a11320 | |||
5162000e25 | |||
3b5100acee | |||
3332d5ecf6 | |||
e4ef267637 | |||
97904d2cd4 | |||
d3e1c79a4b | |||
5ebd2ea881 | |||
f3f3789b87 | |||
ee244ff0d2 | |||
9b3d2e8698 | |||
8b3969c26e | |||
e824bc815e | |||
938595f86a | |||
26ba1538e9 | |||
6380e94fb8 | |||
02595d6b0d | |||
732b9a5bdc | |||
59246e7b70 | |||
4e462de87a | |||
24a917c28b | |||
f46cd5f52c | |||
0544def9e5 | |||
4c97551ff9 | |||
659cfde44d | |||
a47f339bf0 | |||
69789283f6 | |||
9ae6f21603 | |||
65cfa0f305 | |||
63bbd5cfe8 | |||
55195469d8 | |||
891dbb4db5 | |||
a3275adfd1 | |||
6030502288 | |||
a2460e1225 | |||
e972c6d280 | |||
b9b6f667f9 | |||
77960edc2d | |||
39abbfc540 | |||
986d33308d | |||
d9a6b1072c | |||
042e1e1eba | |||
11dae87bc1 | |||
4265a040ec | |||
00667606d8 | |||
712a67c555 | |||
b9bcde093b | |||
aa8ada68d9 | |||
9f42245de1 | |||
904207e48a | |||
204c7fc384 | |||
e4b8ab2d33 | |||
c6f8f3cf7c | |||
65f3590a5a | |||
dbbbc08d5a | |||
1ae58a7299 | |||
b9ef00e6a0 | |||
d6858ddbd8 | |||
318e773df9 | |||
c4eb68a160 | |||
eccddab276 | |||
3d1275371d | |||
237b9d31a1 | |||
5bd3b79040 | |||
795ce100de | |||
45b211f397 | |||
731be37845 | |||
bbd51f3c06 | |||
660fa112e2 | |||
0c5e23b84c | |||
bdb5a3e711 | |||
a9c3d59b47 | |||
643bf012d8 | |||
fc4655a0c3 | |||
8cf753da8f | |||
2900455700 | |||
b0a6108c93 | |||
5c9e5584da | |||
c3271c33e6 | |||
0c7d6f9290 | |||
22943577fc | |||
50277cf657 | |||
ba204ebfd7 | |||
331bd34cde | |||
2143291075 | |||
8a771df290 | |||
006fa4862a | |||
532d23973e | |||
f90f690bba | |||
9470957639 | |||
15e7859e66 | |||
ac715e9173 | |||
32e9e5a161 | |||
3afb8c0805 | |||
3638cd6d1d | |||
10ad738755 | |||
1f519c50fa | |||
3a3fe992ff | |||
115b60325d | |||
cc586965be | |||
a04337ee2c | |||
fe23e9d874 | |||
90d0c7961d | |||
a188c5114b | |||
900f1b41ea | |||
f4e4cdd4e7 | |||
20b3504b94 | |||
157ad23dd8 | |||
f6929fa7de | |||
7b5744baf2 | |||
8e49af85e0 | |||
ffa56e0882 | |||
e05e478921 | |||
d05a6dc07c | |||
7860f187ae | |||
653c723d31 | |||
81d43c131e | |||
a0fa965527 | |||
93ac2714c3 | |||
b84cf3638c | |||
19236a19fe | |||
1e401484ce | |||
76f1a33817 | |||
468bb3325f | |||
4041f15f74 | |||
193992a399 | |||
93a3d5207e | |||
51450dfdd3 | |||
e48aafd458 | |||
016bdd3529 | |||
fb77a975db | |||
b49bae58b5 | |||
c5444b9d39 | |||
99875a8c9d | |||
78e314d178 | |||
a37e5efb6d | |||
eb9625a542 | |||
8a366d614e | |||
0c8256b24d | |||
dd4a406040 | |||
390dec131f | |||
3e34e20e2c | |||
74dd8d07f6 | |||
ed9086dbdc | |||
4ad9374761 | |||
69b3954a4a | |||
3085c8a1ba | |||
cf00132efc | |||
2777978114 | |||
2c451d125f | |||
a99f7945d9 | |||
c644253f40 | |||
467a200c6d | |||
1652cb7ded | |||
3c1f692bc1 | |||
0377b162ed | |||
69671395ec | |||
cd96e66c45 | |||
6aed841b9f | |||
957bba58dc | |||
9fe80e4f29 | |||
6ea4c7f184 | |||
d3b69fd97d | |||
0d6b5aaf12 | |||
70dbd64aff | |||
1e6b53615f | |||
3d409916be | |||
ddd2972b9c | |||
2642d15f4f | |||
1ef8703991 | |||
bd4ff2f87e | |||
ea68dea83f | |||
3e63f187e8 | |||
5720ff906c | |||
55a33d627d | |||
6db2364ca3 | |||
d55a9c17a0 | |||
608a3b6bf5 | |||
f217d922a1 | |||
2ff969d3c1 | |||
805253491d | |||
8df8c6a100 | |||
ed97e52c60 | |||
9e55b47f77 | |||
f1cfe2459b | |||
1baef885d2 | |||
88e351f18c | |||
b5b4f1a766 | |||
f4479bb2f8 | |||
7e21c7b379 | |||
22eabd165d | |||
941fd93653 | |||
100c62f39b | |||
ddc5229136 | |||
8198c5462b | |||
81682ac2a2 | |||
1e7917041d | |||
28115f3506 | |||
d4c62e8572 | |||
85f30d7ce7 | |||
1bd8f4c880 | |||
4e997fa794 | |||
d083995b51 | |||
0e72f7a4cf | |||
1d1c4e01d6 | |||
ad46df61a3 | |||
2df933dc95 | |||
446efb8696 | |||
6eb1861d1b | |||
2700eab46b | |||
28dd40561f | |||
95126cfa57 | |||
eace07ed9a | |||
a735144a55 | |||
a7beb7b37f | |||
695e360d07 | |||
04038b5b0d | |||
7b04e8dc4c | |||
071e9733d2 | |||
d8e5542aa4 | |||
adb90e2a08 | |||
f5e3262534 | |||
b64843c6c2 | |||
13116f2334 | |||
e84b0ec131 | |||
2b6e83c8fc | |||
b589bc9b15 | |||
0a1ef9140c | |||
67267961a1 | |||
5ae03396e0 | |||
38b2d25f87 | |||
f8d1d15afb | |||
c1d28331a4 | |||
37a475afd1 | |||
c060932749 | |||
0ce98074da | |||
be65bb6321 | |||
8ff7c26ee1 | |||
d4932dcc89 | |||
fa1a142529 | |||
65b495b841 | |||
13adfa1031 | |||
68e24d31a5 | |||
af922219eb | |||
d92dead1bd | |||
08fba83271 | |||
9e4aa30e3a | |||
c4cb47a88c | |||
237d8e6611 | |||
ccebfd54fe | |||
0c4955603e | |||
045f1d1576 | |||
b96bfbff13 | |||
940455101c | |||
182b2821e9 | |||
1f2711ee88 | |||
3ef66e52de | |||
eb95d22e99 | |||
f8e2283d87 | |||
2a0c42d97f | |||
fd7614a843 | |||
99c03a8c2d | |||
98730cbf08 | |||
23e899481b | |||
02c244cb51 | |||
407719e62d | |||
b86f68825f | |||
d7213770c2 | |||
f0d1d72384 | |||
72b854e8d9 | |||
81b4ed929f | |||
df8c5b043d | |||
c36e45e2da | |||
6873db5be5 | |||
e21a0ac896 | |||
2074f90c66 | |||
fb9d84226d | |||
f1fdbc47fc | |||
555a2f052f | |||
9e51a89fb2 | |||
2129ed845b | |||
2e94f37fe4 | |||
15f5773552 | |||
8e54cf66b8 | |||
e886343911 | |||
ea71a2b9a2 | |||
0a74a2e6d5 | |||
2eca125974 | |||
5f0b47019b | |||
3b3b46279c | |||
bbe1de8b8a | |||
fca4560e18 | |||
48e6df7f55 | |||
cc54d0d6ba | |||
07d2047afe | |||
fe89ec9b62 | |||
4d1ad1078f | |||
7186d2d9f6 | |||
54e87fd077 | |||
131cba8826 | |||
987b72e43e | |||
ef6f0e3ec2 | |||
1963dfd18f | |||
4782d018cc | |||
19082b3263 | |||
351337f654 | |||
ccc5495080 | |||
9912eb9b84 | |||
c5eb4b8fbd | |||
90d52e515e | |||
c87a01728d | |||
95451e8bb4 | |||
73a1f454e3 | |||
88b12d04c4 | |||
66bc094a19 | |||
626b11736a | |||
d81a7472be | |||
3433f37a0e | |||
d50f87972c | |||
6cd1740123 | |||
81191c9609 | |||
db42d89dd6 | |||
869665f5dc | |||
20ccb27466 | |||
6da2d97787 | |||
c5267fba03 | |||
aa1dd93c41 | |||
bcaca7db89 | |||
84f6e12c80 | |||
aa4e8bc933 | |||
2013c2af3c | |||
cacaf715e2 | |||
21cd42cefa | |||
e935c8bf7d | |||
38aa8232fa | |||
b03e92ad85 | |||
e75e9b8c4c | |||
4777a6c36f | |||
4161c0ec42 | |||
5b542cb0af | |||
f3a079de27 | |||
25822be036 | |||
06d88380f3 | |||
52f84bdfeb | |||
ff82d87aee | |||
31ec7bae8f | |||
ff7c563a0e | |||
45093e0ee6 | |||
35c8a10197 | |||
c56ecf1d49 | |||
48b4844726 | |||
4fce2ddaab | |||
1d16c5fa54 | |||
3903c00ea7 | |||
70ecbf419b | |||
c2d48f8821 | |||
053626c757 | |||
067f623ec4 | |||
733ea54cd3 | |||
48656647b9 | |||
dd3956c6bb | |||
c9e13316e2 | |||
c1529ea983 | |||
849fdc294b | |||
6409975a6e | |||
46c41f9b66 | |||
155f9b4535 | |||
e71f7aaa83 | |||
cc7d4c77fd | |||
33909db46c | |||
50526f82df | |||
569807d861 | |||
245df95da0 | |||
9235386021 | |||
7878f0d76b | |||
0a9ebb4d41 | |||
7f04378168 | |||
5b1169c4a2 | |||
cb65d9c1d1 | |||
e30a301871 | |||
5dc2fe9d30 | |||
575ae6b35c | |||
52786078c7 | |||
de2e1428a1 | |||
2321c7eaa8 | |||
c33916ddf3 | |||
ef0989c3cd |
11
.dockerignore
Normal file
11
.dockerignore
Normal file
|
@ -0,0 +1,11 @@
|
|||
node_modules
|
||||
.git
|
||||
.gitignore
|
||||
.dockerignore
|
||||
.pre-commit-config.yaml
|
||||
.prettierrc.json
|
||||
.env.local
|
||||
.next
|
||||
Dockerfile
|
||||
docker-compose.yml
|
||||
README.md
|
41
.env.sample
Normal file
41
.env.sample
Normal file
|
@ -0,0 +1,41 @@
|
|||
## Required variables section ##
|
||||
|
||||
# Host port mappings
|
||||
WEB_SERVER_PORT=3000
|
||||
SSH_SERVER_PORT=2222
|
||||
|
||||
# Hostname and URL
|
||||
FQDN=your.domain.com
|
||||
NEXTAUTH_URL=https://your.domain.com
|
||||
|
||||
# Secrects
|
||||
NEXTAUTH_SECRET=your-secret
|
||||
CRONJOB_KEY=your-other-secret
|
||||
|
||||
# UID:GID must match the user and group ID of the host folders and must be > 1000
|
||||
# If you want to use a different user than 1001:1001, you must rebuild the image yourself.
|
||||
UID=1001
|
||||
GID=1001
|
||||
|
||||
# Config and data folders (volume mounts)
|
||||
# The host folders must be owned by the user with UID and GID specified above
|
||||
CONFIG_PATH=./config
|
||||
SSH_PATH=./ssh
|
||||
SSH_HOST=./ssh_host
|
||||
BORG_REPOSITORY_PATH=./repos
|
||||
TMP_PATH=./tmp
|
||||
LOGS_PATH=./logs
|
||||
|
||||
## Optional variables section ##
|
||||
|
||||
# LAN feature
|
||||
FQDN_LAN=
|
||||
SSH_SERVER_PORT_LAN=
|
||||
|
||||
# SMTP server settings
|
||||
MAIL_SMTP_FROM=
|
||||
MAIL_SMTP_HOST=
|
||||
MAIL_SMTP_PORT=
|
||||
MAIL_SMTP_LOGIN=
|
||||
MAIL_SMTP_PWD=
|
||||
MAIL_REJECT_SELFSIGNED_TLS=
|
13
.github/FUNDING.yml
vendored
Normal file
13
.github/FUNDING.yml
vendored
Normal file
|
@ -0,0 +1,13 @@
|
|||
# These are supported funding model platforms
|
||||
|
||||
github: [Ravinou]
|
||||
patreon: # Replace with a single Patreon username
|
||||
open_collective: # Replace with a single Open Collective username
|
||||
ko_fi: # Replace with a single Ko-fi username
|
||||
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
|
||||
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
|
||||
liberapay: R4VEN
|
||||
issuehunt: # Replace with a single IssueHunt username
|
||||
otechie: # Replace with a single Otechie username
|
||||
lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry
|
||||
custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
|
16
.github/dependabot.yml
vendored
Normal file
16
.github/dependabot.yml
vendored
Normal file
|
@ -0,0 +1,16 @@
|
|||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: "docker"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "daily"
|
||||
- package-ecosystem: "npm"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "daily"
|
||||
# Maintain dependencies for GitHub Actions
|
||||
# src: https://github.com/marketplace/actions/build-and-push-docker-images#keep-up-to-date-with-github-dependabot
|
||||
- package-ecosystem: "github-actions"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "daily"
|
29
.github/workflows/docker-image-develop.yml
vendored
Normal file
29
.github/workflows/docker-image-develop.yml
vendored
Normal file
|
@ -0,0 +1,29 @@
|
|||
name: Build and Push Docker Image for Develop Branch
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- 'develop'
|
||||
|
||||
jobs:
|
||||
docker:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: .
|
||||
push: true
|
||||
platforms: linux/amd64,linux/arm64 # linux/arm/v7 arm32 is not supported by node20 https://github.com/nodejs/docker-node/issues/1946
|
||||
tags: borgwarehouse/borgwarehouse:develop
|
29
.github/workflows/docker-image-latest.yml
vendored
Normal file
29
.github/workflows/docker-image-latest.yml
vendored
Normal file
|
@ -0,0 +1,29 @@
|
|||
name: Build and Push Docker Image
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- 'main'
|
||||
|
||||
jobs:
|
||||
docker:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: .
|
||||
push: true
|
||||
platforms: linux/amd64,linux/arm64 # linux/arm/v7 arm32 is not supported by node20 https://github.com/nodejs/docker-node/issues/1946
|
||||
tags: borgwarehouse/borgwarehouse:latest
|
32
.github/workflows/docker-image-release.yml
vendored
Normal file
32
.github/workflows/docker-image-release.yml
vendored
Normal file
|
@ -0,0 +1,32 @@
|
|||
name: Build and Push Docker Image on Release
|
||||
|
||||
on:
|
||||
release:
|
||||
types:
|
||||
- published
|
||||
|
||||
jobs:
|
||||
docker:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
- name: Get Release Tag
|
||||
id: get_release_tag
|
||||
run: echo "::set-output name=TAG::${{ github.event.release.tag_name }}"
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: .
|
||||
push: true
|
||||
platforms: linux/amd64,linux/arm64 # linux/arm/v7 arm32 is not supported by node20 https://github.com/nodejs/docker-node/issues/1946
|
||||
tags: borgwarehouse/borgwarehouse:${{ steps.get_release_tag.outputs.TAG }}
|
21
.github/workflows/docker-image-test.yml
vendored
Normal file
21
.github/workflows/docker-image-test.yml
vendored
Normal file
|
@ -0,0 +1,21 @@
|
|||
name: Test Docker Container Build on Pull Request
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
- develop
|
||||
|
||||
jobs:
|
||||
build-container:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
- name: Build Docker Container
|
||||
run: |
|
||||
docker buildx build --platform linux/amd64,linux/arm64 -t borgwarehouse:pr-${{ github.event.pull_request.number }} .
|
23
.github/workflows/shellcheck.yml
vendored
Normal file
23
.github/workflows/shellcheck.yml
vendored
Normal file
|
@ -0,0 +1,23 @@
|
|||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- develop
|
||||
pull_request:
|
||||
branches: main
|
||||
|
||||
name: "Shellcheck"
|
||||
permissions: {}
|
||||
|
||||
jobs:
|
||||
shellcheck:
|
||||
name: Shellcheck
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Run ShellCheck
|
||||
uses: ludeeus/action-shellcheck@master
|
||||
env:
|
||||
SHELLCHECK_OPTS: -e SC1091
|
3
.gitignore
vendored
3
.gitignore
vendored
|
@ -109,3 +109,6 @@ dist
|
|||
# config file for BorgWarehouse
|
||||
config/repo.json
|
||||
config/users.json
|
||||
|
||||
# docker files
|
||||
docker-compose.yml
|
7
.pre-commit-config.yaml
Normal file
7
.pre-commit-config.yaml
Normal file
|
@ -0,0 +1,7 @@
|
|||
# Run test against shells with "pre-commit run shellcheck --all-files"
|
||||
repos:
|
||||
- repo: https://github.com/jumanjihouse/pre-commit-hooks
|
||||
rev: 3.0.0
|
||||
hooks:
|
||||
- id: shellcheck
|
||||
files: helpers/shells/
|
21
.prettierrc.json
Normal file
21
.prettierrc.json
Normal file
|
@ -0,0 +1,21 @@
|
|||
{
|
||||
"trailingComma": "es5",
|
||||
"tabWidth": 4,
|
||||
"semi": true,
|
||||
"singleQuote": true,
|
||||
"arrowParens": "always",
|
||||
"bracketSpacing": true,
|
||||
"endOfLine": "lf",
|
||||
"htmlWhitespaceSensitivity": "css",
|
||||
"insertPragma": false,
|
||||
"singleAttributePerLine": false,
|
||||
"bracketSameLine": false,
|
||||
"jsxBracketSameLine": false,
|
||||
"jsxSingleQuote": true,
|
||||
"printWidth": 80,
|
||||
"proseWrap": "preserve",
|
||||
"quoteProps": "as-needed",
|
||||
"requirePragma": false,
|
||||
"useTabs": false,
|
||||
"embeddedLanguageFormatting": "auto"
|
||||
}
|
|
@ -2,9 +2,26 @@
|
|||
import React from 'react';
|
||||
import { useState } from 'react';
|
||||
import classes from './QuickCommands.module.css';
|
||||
import { IconSettingsAutomation, IconCopy } from '@tabler/icons';
|
||||
import { IconSettingsAutomation, IconCopy } from '@tabler/icons-react';
|
||||
|
||||
export default function QuickCommands(props) {
|
||||
////Vars
|
||||
const wizardEnv = props.wizardEnv;
|
||||
//Needed to generate command for borg over LAN instead of WAN if env vars are set and option enabled.
|
||||
let FQDN;
|
||||
let SSH_SERVER_PORT;
|
||||
if (
|
||||
props.lanCommand &&
|
||||
wizardEnv.FQDN_LAN &&
|
||||
wizardEnv.SSH_SERVER_PORT_LAN
|
||||
) {
|
||||
FQDN = wizardEnv.FQDN_LAN;
|
||||
SSH_SERVER_PORT = wizardEnv.SSH_SERVER_PORT_LAN;
|
||||
} else {
|
||||
FQDN = wizardEnv.FQDN;
|
||||
SSH_SERVER_PORT = wizardEnv.SSH_SERVER_PORT;
|
||||
}
|
||||
|
||||
//State
|
||||
const [isCopied, setIsCopied] = useState(false);
|
||||
|
||||
|
@ -13,7 +30,7 @@ export default function QuickCommands(props) {
|
|||
// Asynchronously call copy to clipboard
|
||||
navigator.clipboard
|
||||
.writeText(
|
||||
`borg init -e repokey-blake2 ssh://${props.unixUser}@${process.env.NEXT_PUBLIC_HOSTNAME}:${process.env.NEXT_PUBLIC_SSH_SERVER_PORT}/./${props.repository}`
|
||||
`ssh://${wizardEnv.UNIX_USER}@${FQDN}:${SSH_SERVER_PORT}/./${props.repositoryName}`
|
||||
)
|
||||
.then(() => {
|
||||
// If successful, update the isCopied state value
|
||||
|
@ -33,12 +50,13 @@ export default function QuickCommands(props) {
|
|||
<div className={classes.copyValid}>Copied !</div>
|
||||
) : (
|
||||
<div className={classes.tooltip}>
|
||||
borg init -e repokey-blake2 ssh://{props.unixUser}@
|
||||
{process.env.NEXT_PUBLIC_HOSTNAME}:
|
||||
{process.env.NEXT_PUBLIC_SSH_SERVER_PORT}/./
|
||||
{props.repository}
|
||||
ssh://{wizardEnv.UNIX_USER}@{FQDN}:{SSH_SERVER_PORT}/./
|
||||
{props.repositoryName}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{props.lanCommand && <div className={classes.lanBadge}>LAN</div>}
|
||||
|
||||
<div className={classes.icons}>
|
||||
<button onClick={handleCopy} className={classes.copyButton}>
|
||||
<IconCopy color='#65748b' stroke={1.25} />
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
|
||||
.icons {
|
||||
position: relative;
|
||||
bottom: 14px;
|
||||
bottom: 13px;
|
||||
}
|
||||
|
||||
.quickSetting {
|
||||
|
@ -16,6 +16,15 @@
|
|||
opacity: 1;
|
||||
}
|
||||
|
||||
.lanBadge {
|
||||
border-radius: 5px;
|
||||
border: 1px solid #6d4aff;
|
||||
color: #6d4aff;
|
||||
font-size: 0.9em;
|
||||
padding: 2px 5px;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.tooltip {
|
||||
visibility: hidden;
|
||||
opacity: 0;
|
||||
|
@ -91,6 +100,15 @@
|
|||
opacity: 0;
|
||||
}
|
||||
|
||||
.container:hover .lanBadge {
|
||||
visibility: hidden;
|
||||
opacity: 0;
|
||||
width: 0;
|
||||
height: 0;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
@media all and (max-width: 1000px) {
|
||||
.container {
|
||||
display: none;
|
||||
|
|
|
@ -1,28 +1,98 @@
|
|||
//Lib
|
||||
import React from 'react';
|
||||
import { useState } from 'react';
|
||||
import classes from './Repo.module.css';
|
||||
import { IconSettings, IconInfoCircle } from '@tabler/icons';
|
||||
import {
|
||||
IconSettings,
|
||||
IconInfoCircle,
|
||||
IconChevronDown,
|
||||
IconChevronUp,
|
||||
IconBellOff,
|
||||
IconLockPlus,
|
||||
} from '@tabler/icons-react';
|
||||
import timestampConverter from '../../helpers/functions/timestampConverter';
|
||||
import StorageBar from '../UI/StorageBar/StorageBar';
|
||||
import QuickCommands from './QuickCommands/QuickCommands';
|
||||
|
||||
export default function Repo(props) {
|
||||
//Load displayDetails from LocalStorage
|
||||
const displayDetailsFromLS = () => {
|
||||
try {
|
||||
if (
|
||||
localStorage.getItem('displayDetailsRepo' + props.id) === null
|
||||
) {
|
||||
localStorage.setItem(
|
||||
'displayDetailsRepo' + props.id,
|
||||
JSON.stringify(true)
|
||||
);
|
||||
return true;
|
||||
} else {
|
||||
return JSON.parse(
|
||||
localStorage.getItem('displayDetailsRepo' + props.id)
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(
|
||||
'LocalStorage error, key',
|
||||
'displayDetailsRepo' + props.id,
|
||||
'will be removed. Try again.',
|
||||
'Error message on this key : ',
|
||||
error
|
||||
);
|
||||
localStorage.removeItem('displayDetailsRepo' + props.id);
|
||||
}
|
||||
};
|
||||
|
||||
//States
|
||||
const [displayDetails, setDisplayDetails] = useState(displayDetailsFromLS);
|
||||
|
||||
//BUTTON : Display or not repo details for ONE repo
|
||||
const displayDetailsForOneHandler = (boolean) => {
|
||||
//Update localStorage
|
||||
localStorage.setItem(
|
||||
'displayDetailsRepo' + props.id,
|
||||
JSON.stringify(boolean)
|
||||
);
|
||||
setDisplayDetails(boolean);
|
||||
};
|
||||
|
||||
//Status indicator
|
||||
const statusIndicator = () => {
|
||||
return props.status
|
||||
? classes.statusIndicatorGreen
|
||||
: classes.statusIndicatorRed;
|
||||
};
|
||||
|
||||
//Alert indicator
|
||||
const alertIndicator = () => {
|
||||
if (props.alert === 0) {
|
||||
return (
|
||||
<div className={classes.alertIcon}>
|
||||
<IconBellOff size={16} color='grey' />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const appendOnlyModeIndicator = () => {
|
||||
if (props.appendOnlyMode) {
|
||||
return (
|
||||
<div className={classes.appendOnlyModeIcon}>
|
||||
<IconLockPlus size={16} color='grey' />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{props.displayDetails ? (
|
||||
{displayDetails ? (
|
||||
<>
|
||||
<div className={classes.RepoOpen}>
|
||||
<div className={classes.openFlex}>
|
||||
{props.status ? (
|
||||
<div
|
||||
className={classes.statusIndicatorGreen}
|
||||
></div>
|
||||
) : (
|
||||
<div
|
||||
className={classes.statusIndicatorRed}
|
||||
></div>
|
||||
)}
|
||||
<div className={statusIndicator()} />
|
||||
<div className={classes.alias}>{props.alias}</div>
|
||||
{appendOnlyModeIndicator()}
|
||||
{alertIndicator()}
|
||||
{props.comment && (
|
||||
<div className={classes.comment}>
|
||||
<IconInfoCircle size={16} color='grey' />
|
||||
|
@ -32,8 +102,9 @@ export default function Repo(props) {
|
|||
</div>
|
||||
)}
|
||||
<QuickCommands
|
||||
unixUser={props.unixUser}
|
||||
repository={props.repository}
|
||||
repositoryName={props.repositoryName}
|
||||
lanCommand={props.lanCommand}
|
||||
wizardEnv={props.wizardEnv}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
@ -56,8 +127,8 @@ export default function Repo(props) {
|
|||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<th>{props.repository}</th>
|
||||
<th>{props.storageSize}Go</th>
|
||||
<th>{props.repositoryName}</th>
|
||||
<th>{props.storageSize} GB</th>
|
||||
<th style={{ padding: '0 4% 0 4%' }}>
|
||||
<StorageBar
|
||||
storageUsed={props.storageUsed}
|
||||
|
@ -94,16 +165,10 @@ export default function Repo(props) {
|
|||
<>
|
||||
<div className={classes.RepoClose}>
|
||||
<div className={classes.closeFlex}>
|
||||
{props.status ? (
|
||||
<div
|
||||
className={classes.statusIndicatorGreen}
|
||||
></div>
|
||||
) : (
|
||||
<div
|
||||
className={classes.statusIndicatorRed}
|
||||
></div>
|
||||
)}
|
||||
<div className={statusIndicator()} />
|
||||
<div className={classes.alias}>{props.alias}</div>
|
||||
{appendOnlyModeIndicator()}
|
||||
{alertIndicator()}
|
||||
{props.comment && (
|
||||
<div className={classes.comment}>
|
||||
<IconInfoCircle size={16} color='#637381' />
|
||||
|
@ -126,6 +191,27 @@ export default function Repo(props) {
|
|||
</div>
|
||||
</>
|
||||
)}
|
||||
{displayDetails ? (
|
||||
<div className={classes.chevron}>
|
||||
<IconChevronUp
|
||||
color='#494b7a'
|
||||
size={28}
|
||||
onClick={() => {
|
||||
displayDetailsForOneHandler(false);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div className={classes.chevron}>
|
||||
<IconChevronDown
|
||||
color='#494b7a'
|
||||
size={28}
|
||||
onClick={() => {
|
||||
displayDetailsForOneHandler(true);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -3,7 +3,9 @@
|
|||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.12), 0 1px 2px rgba(0, 0, 0, 0.24);
|
||||
box-shadow:
|
||||
0 1px 3px rgba(0, 0, 0, 0.12),
|
||||
0 1px 2px rgba(0, 0, 0, 0.24);
|
||||
width: auto;
|
||||
max-height: 65px;
|
||||
margin: 20px 0px 0px 0px;
|
||||
|
@ -29,13 +31,15 @@
|
|||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.12), 0 1px 2px rgba(0, 0, 0, 0.24);
|
||||
box-shadow:
|
||||
0 1px 3px rgba(0, 0, 0, 0.12),
|
||||
0 1px 2px rgba(0, 0, 0, 0.24);
|
||||
width: auto;
|
||||
max-height: 200px;
|
||||
margin: 20px 0px 0px 0px;
|
||||
padding: 15px;
|
||||
border-radius: 5px;
|
||||
transition: max-height 0.2s linear;
|
||||
transition: max-height 0.1s linear;
|
||||
overflow: visible;
|
||||
/* Need to display comment on hover (which is position : absolute) */
|
||||
position: relative;
|
||||
|
@ -144,6 +148,22 @@
|
|||
}
|
||||
}
|
||||
|
||||
/* Alert icon */
|
||||
|
||||
.alertIcon {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.appendOnlyModeIcon {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
/* GENERAL */
|
||||
.alias {
|
||||
font-weight: bold;
|
||||
|
@ -183,7 +203,9 @@
|
|||
margin: 0px 0 0 20px;
|
||||
opacity: 1;
|
||||
transition: 0.5s opacity;
|
||||
box-shadow: 0 3px 6px rgba(0, 0, 0, 0.16), 0 3px 6px rgba(0, 0, 0, 0.23);
|
||||
box-shadow:
|
||||
0 3px 6px rgba(0, 0, 0, 0.16),
|
||||
0 3px 6px rgba(0, 0, 0, 0.23);
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
|
@ -193,6 +215,17 @@
|
|||
opacity: 1;
|
||||
}
|
||||
|
||||
.chevron {
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
.chevron :focus,
|
||||
.chevron :hover {
|
||||
cursor: pointer;
|
||||
filter: invert(27%) sepia(82%) saturate(2209%) hue-rotate(240deg)
|
||||
brightness(99%) contrast(105%);
|
||||
}
|
||||
|
||||
/* MOBILE */
|
||||
@media all and (max-width: 1000px) {
|
||||
.tabInfo {
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
//Lib
|
||||
import classes from './CopyButton.module.css';
|
||||
import { useState } from 'react';
|
||||
import { IconCopy } from '@tabler/icons';
|
||||
import { IconCopy } from '@tabler/icons-react';
|
||||
|
||||
export default function CopyButton(props) {
|
||||
//State
|
||||
|
|
6
Components/UI/Info/Info.js
Normal file
6
Components/UI/Info/Info.js
Normal file
|
@ -0,0 +1,6 @@
|
|||
//Lib
|
||||
import classes from './Info.module.css';
|
||||
|
||||
export default function Info(props) {
|
||||
return <div className={classes.infoMessage}>{props.message}</div>;
|
||||
}
|
18
Components/UI/Info/Info.module.css
Normal file
18
Components/UI/Info/Info.module.css
Normal file
|
@ -0,0 +1,18 @@
|
|||
.infoMessage {
|
||||
margin: 15px 0px;
|
||||
background-color: rgb(17, 147, 0);
|
||||
color: white;
|
||||
padding: 15px;
|
||||
border-radius: 5px;
|
||||
animation: myAnim 1s ease 0s 1 normal forwards;
|
||||
}
|
||||
|
||||
@keyframes myAnim {
|
||||
0% {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
|
@ -1,5 +1,6 @@
|
|||
//Lib
|
||||
import classes from './Footer.module.css';
|
||||
import packageInfo from '../../../../package.json';
|
||||
|
||||
function Footer() {
|
||||
return (
|
||||
|
@ -10,9 +11,11 @@ function Footer() {
|
|||
className={classes.site}
|
||||
target='_blank'
|
||||
href='https://borgwarehouse.com/'
|
||||
rel='noreferrer'
|
||||
>
|
||||
BorgWarehouse
|
||||
</a>
|
||||
</a>{' '}
|
||||
- v{packageInfo.version}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -7,7 +7,20 @@
|
|||
height: 50px;
|
||||
}
|
||||
|
||||
.footer p {
|
||||
padding-left: 70px;
|
||||
}
|
||||
|
||||
a.site {
|
||||
color: #6d4aff;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
@media all and (max-width: 1000px) {
|
||||
.footer {
|
||||
width: 100%;
|
||||
}
|
||||
.footer p {
|
||||
padding-left: 0;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,23 +1,21 @@
|
|||
//Lib
|
||||
import classes from "./Header.module.css";
|
||||
import classes from './Header.module.css';
|
||||
|
||||
//Components
|
||||
import Nav from "./Nav/Nav";
|
||||
import Nav from './Nav/Nav';
|
||||
|
||||
function Header() {
|
||||
return (
|
||||
<header className={classes.Header}>
|
||||
<div className={[classes.flex, 'container'].join(' ')}>
|
||||
<div className={classes.logo}>
|
||||
BorgWarehouse
|
||||
</div>
|
||||
<div className={classes.logo}>BorgWarehouse</div>
|
||||
|
||||
<nav>
|
||||
<Nav />
|
||||
</nav>
|
||||
</div>
|
||||
</header>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export default Header;
|
|
@ -1,7 +1,9 @@
|
|||
.Header {
|
||||
width: 100%;
|
||||
background: #111827;
|
||||
box-shadow: 0 3px 6px rgba(0, 0, 0, 0.16), 0 3px 6px rgba(0, 0, 0, 0.23);
|
||||
box-shadow:
|
||||
0 3px 6px rgba(0, 0, 0, 0.16),
|
||||
0 3px 6px rgba(0, 0, 0, 0.23);
|
||||
height: 50px;
|
||||
color: white;
|
||||
display: flex;
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
//Lib
|
||||
import classes from './Nav.module.css';
|
||||
import { IconUser, IconLogout } from '@tabler/icons';
|
||||
import { IconUser, IconLogout } from '@tabler/icons-react';
|
||||
import Link from 'next/link';
|
||||
import { useRouter } from 'next/router';
|
||||
import { useSession, signOut } from 'next-auth/react';
|
||||
|
@ -38,7 +38,7 @@ export default function Nav() {
|
|||
<div>
|
||||
<IconUser size={28} />
|
||||
</div>
|
||||
<div>
|
||||
<div className={classes.username}>
|
||||
{status === 'authenticated' && data.user.name}
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -10,6 +10,10 @@
|
|||
align-items: center;
|
||||
}
|
||||
|
||||
.username::first-letter {
|
||||
text-transform: capitalize;
|
||||
}
|
||||
|
||||
.account {
|
||||
background: none;
|
||||
border: none;
|
||||
|
|
|
@ -4,7 +4,7 @@ import {
|
|||
IconServer,
|
||||
IconSettingsAutomation,
|
||||
IconActivityHeartbeat,
|
||||
} from '@tabler/icons';
|
||||
} from '@tabler/icons-react';
|
||||
import Link from 'next/link';
|
||||
import { useRouter } from 'next/router';
|
||||
|
||||
|
|
|
@ -3,7 +3,7 @@ import classes from './StorageBar.module.css';
|
|||
|
||||
export default function StorageBar(props) {
|
||||
//Var
|
||||
//storageUsed is in octet, storageSize is in Go. Round to 1 decimal for %.
|
||||
//storageUsed is in octet, storageSize is in GB. Round to 1 decimal for %.
|
||||
const storageUsedPercent = (
|
||||
((props.storageUsed / 1000000) * 100) /
|
||||
props.storageSize
|
||||
|
@ -23,8 +23,8 @@ export default function StorageBar(props) {
|
|||
</div>
|
||||
<div className={classes.tooltip}>
|
||||
{storageUsedPercent}% (
|
||||
{(props.storageUsed / 1000000).toFixed(1)}Go/
|
||||
{props.storageSize}Go)
|
||||
{(props.storageUsed / 1000000).toFixed(1)} GB /{' '}
|
||||
{props.storageSize} GB)
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
26
Components/UI/Switch/Switch.js
Normal file
26
Components/UI/Switch/Switch.js
Normal file
|
@ -0,0 +1,26 @@
|
|||
//Lib
|
||||
import classes from './Switch.module.css';
|
||||
|
||||
export default function Switch(props) {
|
||||
return (
|
||||
<>
|
||||
<div className={classes.switchWrapper}>
|
||||
<div className={classes.switch}>
|
||||
<label className={classes.pureMaterialSwitch}>
|
||||
<input
|
||||
checked={props.checked}
|
||||
disabled={props.disabled}
|
||||
type='checkbox'
|
||||
onChange={(e) => props.onChange(e.target.checked)}
|
||||
/>
|
||||
|
||||
<span>{props.switchName}</span>
|
||||
</label>
|
||||
</div>
|
||||
<div className={classes.switchDescription}>
|
||||
<span>{props.switchDescription}</span>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
157
Components/UI/Switch/Switch.module.css
Normal file
157
Components/UI/Switch/Switch.module.css
Normal file
|
@ -0,0 +1,157 @@
|
|||
.switchWrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.switch {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.switchDescription {
|
||||
display: flex;
|
||||
margin: 8px 0px 0px 0px;
|
||||
color: #6c737f;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.pureMaterialSwitch {
|
||||
z-index: 0;
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
color: rgba(var(--pure-material-onsurface-rgb, 0, 0, 0), 0.87);
|
||||
font-family: var(
|
||||
--pure-material-font,
|
||||
'Roboto',
|
||||
'Segoe UI',
|
||||
BlinkMacSystemFont,
|
||||
system-ui,
|
||||
-apple-system
|
||||
);
|
||||
font-size: 16px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
/* Input */
|
||||
.pureMaterialSwitch > input {
|
||||
appearance: none;
|
||||
-moz-appearance: none;
|
||||
-webkit-appearance: none;
|
||||
z-index: -1;
|
||||
position: absolute;
|
||||
right: 6px;
|
||||
top: -8px;
|
||||
display: block;
|
||||
margin: 0;
|
||||
border-radius: 50%;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
background-color: rgba(var(--pure-material-onsurface-rgb, 0, 0, 0), 0.38);
|
||||
outline: none;
|
||||
opacity: 0;
|
||||
transform: scale(1);
|
||||
pointer-events: none;
|
||||
transition:
|
||||
opacity 0.3s 0.1s,
|
||||
transform 0.2s 0.1s;
|
||||
}
|
||||
|
||||
/* Span */
|
||||
.pureMaterialSwitch > span {
|
||||
display: inline-block;
|
||||
width: 100%;
|
||||
cursor: pointer;
|
||||
font-size: 1rem;
|
||||
font-weight: 500;
|
||||
color: #494b7a;
|
||||
}
|
||||
|
||||
/* Track */
|
||||
.pureMaterialSwitch > span::before {
|
||||
content: '';
|
||||
float: right;
|
||||
display: inline-block;
|
||||
margin: 5px 0 5px 30px;
|
||||
border-radius: 7px;
|
||||
width: 36px;
|
||||
height: 14px;
|
||||
background-color: rgba(var(--pure-material-onsurface-rgb, 0, 0, 0), 0.38);
|
||||
vertical-align: top;
|
||||
transition:
|
||||
background-color 0.2s,
|
||||
opacity 0.2s;
|
||||
}
|
||||
|
||||
/* Thumb */
|
||||
.pureMaterialSwitch > span::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 2px;
|
||||
right: 16px;
|
||||
border-radius: 50%;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
background-color: rgb(var(--pure-material-onprimary-rgb, 255, 255, 255));
|
||||
box-shadow:
|
||||
0 3px 1px -2px rgba(0, 0, 0, 0.2),
|
||||
0 2px 2px 0 rgba(0, 0, 0, 0.14),
|
||||
0 1px 5px 0 rgba(0, 0, 0, 0.12);
|
||||
transition:
|
||||
background-color 0.2s,
|
||||
transform 0.2s;
|
||||
}
|
||||
|
||||
/* Checked */
|
||||
.pureMaterialSwitch > input:checked {
|
||||
right: -10px;
|
||||
background-color: rgb(var(--pure-material-primary-rgb, 109, 74, 255));
|
||||
}
|
||||
|
||||
.pureMaterialSwitch > input:checked + span::before {
|
||||
background-color: rgba(var(--pure-material-primary-rgb, 109, 74, 255), 0.6);
|
||||
}
|
||||
|
||||
.pureMaterialSwitch > input:checked + span::after {
|
||||
background-color: rgb(var(--pure-material-primary-rgb, 109, 74, 255));
|
||||
transform: translateX(16px);
|
||||
}
|
||||
|
||||
/* Active */
|
||||
.pureMaterialSwitch > input:active {
|
||||
opacity: 1;
|
||||
transform: scale(0);
|
||||
transition:
|
||||
transform 0s,
|
||||
opacity 0s;
|
||||
}
|
||||
|
||||
.pureMaterialSwitch > input:active + span::before {
|
||||
background-color: rgba(var(--pure-material-primary-rgb, 109, 74, 255), 0.6);
|
||||
}
|
||||
|
||||
.pureMaterialSwitch > input:checked:active + span::before {
|
||||
background-color: rgba(var(--pure-material-onsurface-rgb, 0, 0, 0), 0.38);
|
||||
}
|
||||
|
||||
/* Disabled */
|
||||
.pureMaterialSwitch > input:disabled + span {
|
||||
cursor: wait;
|
||||
}
|
||||
|
||||
/* .pureMaterialSwitch > input:disabled {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.pureMaterialSwitch > input:disabled + span {
|
||||
color: rgb(var(--pure-material-onsurface-rgb, 0, 0, 0));
|
||||
opacity: 0.38;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.pureMaterialSwitch > input:disabled + span::before {
|
||||
background-color: rgba(var(--pure-material-onsurface-rgb, 0, 0, 0), 0.38);
|
||||
}
|
||||
|
||||
.pureMaterialSwitch > input:checked:disabled + span::before {
|
||||
background-color: rgba(var(--pure-material-primary-rgb, 109, 74, 255), 0.6);
|
||||
} */
|
|
@ -1,7 +1,7 @@
|
|||
//Lib
|
||||
import React from 'react';
|
||||
import classes from '../WizardStep1/WizardStep1.module.css';
|
||||
import { IconDeviceDesktopAnalytics, IconTerminal2 } from '@tabler/icons';
|
||||
import { IconDeviceDesktopAnalytics, IconTerminal2 } from '@tabler/icons-react';
|
||||
|
||||
function WizardStep1() {
|
||||
return (
|
||||
|
@ -18,7 +18,7 @@ function WizardStep1() {
|
|||
<a
|
||||
href='https://borgbackup.readthedocs.io/en/stable/installation.html'
|
||||
target='_blank'
|
||||
rel='noopener noreferrer'
|
||||
rel='noreferrer'
|
||||
>
|
||||
found here
|
||||
</a>
|
||||
|
@ -27,7 +27,7 @@ function WizardStep1() {
|
|||
<a
|
||||
href='https://torsion.org/borgmatic/'
|
||||
target='_blank'
|
||||
rel='noopener noreferrer'
|
||||
rel='noreferrer'
|
||||
>
|
||||
Borgmatic
|
||||
</a>{' '}
|
||||
|
@ -35,7 +35,7 @@ function WizardStep1() {
|
|||
<a
|
||||
href='https://packages.debian.org/buster/borgmatic'
|
||||
target='_blank'
|
||||
rel='noopener noreferrer'
|
||||
rel='noreferrer'
|
||||
>
|
||||
Debian package
|
||||
</a>
|
||||
|
@ -55,7 +55,7 @@ function WizardStep1() {
|
|||
<a
|
||||
href='https://vorta.borgbase.com/'
|
||||
target='_blank'
|
||||
rel='noopener noreferrer'
|
||||
rel='noreferrer'
|
||||
>
|
||||
just here
|
||||
</a>
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
.container {
|
||||
margin: 40px 20px 20px 5px;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.12), 0 1px 2px rgba(0, 0, 0, 0.24);
|
||||
box-shadow:
|
||||
0 1px 3px rgba(0, 0, 0, 0.12),
|
||||
0 1px 2px rgba(0, 0, 0, 0.24);
|
||||
border-radius: 5px;
|
||||
text-align: left;
|
||||
padding: 30px 70px;
|
||||
|
@ -54,8 +56,15 @@ h1 .icon {
|
|||
.code {
|
||||
background-color: #111827;
|
||||
color: #f8f8f2;
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas,
|
||||
liberation mono, courier new, monospace;
|
||||
font-family:
|
||||
ui-monospace,
|
||||
SFMono-Regular,
|
||||
Menlo,
|
||||
Monaco,
|
||||
Consolas,
|
||||
liberation mono,
|
||||
courier new,
|
||||
monospace;
|
||||
padding: 5px 15px;
|
||||
border-radius: 5px;
|
||||
display: inline-block;
|
||||
|
@ -91,8 +100,15 @@ h1 .icon {
|
|||
|
||||
.verifyOrange li .sshPublicKey {
|
||||
background-color: #282a36;
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas,
|
||||
liberation mono, courier new, monospace;
|
||||
font-family:
|
||||
ui-monospace,
|
||||
SFMono-Regular,
|
||||
Menlo,
|
||||
Monaco,
|
||||
Consolas,
|
||||
liberation mono,
|
||||
courier new,
|
||||
monospace;
|
||||
border-radius: 5px;
|
||||
padding: 5px;
|
||||
}
|
||||
|
|
|
@ -1,10 +1,20 @@
|
|||
//Lib
|
||||
import React from 'react';
|
||||
import classes from '../WizardStep1/WizardStep1.module.css';
|
||||
import { IconTool, IconAlertCircle } from '@tabler/icons';
|
||||
import { IconTool, IconAlertCircle } from '@tabler/icons-react';
|
||||
import CopyButton from '../../UI/CopyButton/CopyButton';
|
||||
import lanCommandOption from '../../../helpers/functions/lanCommandOption';
|
||||
|
||||
function WizardStep2(props) {
|
||||
////Vars
|
||||
const wizardEnv = props.wizardEnv;
|
||||
const UNIX_USER = wizardEnv.UNIX_USER;
|
||||
//Needed to generate command for borg over LAN instead of WAN if env vars are set and option enabled.
|
||||
const { FQDN, SSH_SERVER_PORT } = lanCommandOption(
|
||||
wizardEnv,
|
||||
props.selectedOption.lanCommand
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={classes.container}>
|
||||
<h1>
|
||||
|
@ -23,13 +33,11 @@ function WizardStep2(props) {
|
|||
>
|
||||
<div className={classes.code}>
|
||||
borg init -e repokey-blake2 ssh://
|
||||
{props.selectedOption.unixUser}@
|
||||
{process.env.NEXT_PUBLIC_HOSTNAME}:
|
||||
{process.env.NEXT_PUBLIC_SSH_SERVER_PORT}/./
|
||||
{props.selectedOption.repository}
|
||||
{UNIX_USER}@{FQDN}:{SSH_SERVER_PORT}/./
|
||||
{props.selectedOption.repositoryName}
|
||||
</div>
|
||||
<CopyButton
|
||||
dataToCopy={`borg init -e repokey-blake2 ssh://${props.selectedOption.unixUser}@${process.env.NEXT_PUBLIC_HOSTNAME}:${process.env.NEXT_PUBLIC_SSH_SERVER_PORT}/./${props.selectedOption.repository}`}
|
||||
dataToCopy={`borg init -e repokey-blake2 ssh://${UNIX_USER}@${FQDN}:${SSH_SERVER_PORT}/./${props.selectedOption.repositoryName}`}
|
||||
/>
|
||||
</div>
|
||||
<div className={classes.note}>
|
||||
|
@ -37,7 +45,7 @@ function WizardStep2(props) {
|
|||
For more information,{' '}
|
||||
<a
|
||||
href='https://borgbackup.readthedocs.io/en/stable/usage/init.html?highlight=init#more-encryption-modes'
|
||||
rel='noopener noreferrer'
|
||||
rel='noreferrer'
|
||||
target='_blank'
|
||||
>
|
||||
click here
|
||||
|
@ -49,7 +57,7 @@ function WizardStep2(props) {
|
|||
<div className={classes.separator}></div>
|
||||
<h2>Borgmatic</h2>
|
||||
<div className={classes.description}>
|
||||
If you are using Borgmatic and have <b>already edited</b> the
|
||||
If you are using Borgmatic and have <b>already edited</b> the configuration file
|
||||
(find a sample on the step 4) :
|
||||
<br />
|
||||
<div
|
||||
|
@ -60,9 +68,9 @@ function WizardStep2(props) {
|
|||
}}
|
||||
>
|
||||
<div className={classes.code}>
|
||||
borg init -e repokey-blake2
|
||||
borgmatic init -e repokey-blake2
|
||||
</div>
|
||||
<CopyButton dataToCopy='borg init -e repokey-blake2' />
|
||||
<CopyButton dataToCopy='borgmatic init -e repokey-blake2' />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
@ -80,20 +88,18 @@ function WizardStep2(props) {
|
|||
>
|
||||
<div className={classes.code}>
|
||||
ssh://
|
||||
{props.selectedOption.unixUser}@
|
||||
{process.env.NEXT_PUBLIC_HOSTNAME}:
|
||||
{process.env.NEXT_PUBLIC_SSH_SERVER_PORT}/./
|
||||
{props.selectedOption.repository}
|
||||
{UNIX_USER}@{FQDN}:{SSH_SERVER_PORT}/./
|
||||
{props.selectedOption.repositoryName}
|
||||
</div>
|
||||
<CopyButton
|
||||
dataToCopy={`ssh://${props.selectedOption.unixUser}@${process.env.NEXT_PUBLIC_HOSTNAME}:${process.env.NEXT_PUBLIC_SSH_SERVER_PORT}/./${props.selectedOption.repository}`}
|
||||
dataToCopy={`ssh://${UNIX_USER}@${FQDN}:${SSH_SERVER_PORT}/./${props.selectedOption.repositoryName}`}
|
||||
/>
|
||||
</div>
|
||||
For more information about the Vorta graphical client, please
|
||||
refer to{' '}
|
||||
<a
|
||||
href='https://vorta.borgbase.com/usage/remote/'
|
||||
rel='noopener noreferrer'
|
||||
rel='noreferrer'
|
||||
target='_blank'
|
||||
>
|
||||
this documentation
|
||||
|
@ -108,24 +114,21 @@ function WizardStep2(props) {
|
|||
<b>Check the fingerprint of server</b>
|
||||
</div>
|
||||
To check that you are talking to the right server, please make
|
||||
sure to validate the following sshPublicKeys when you first
|
||||
connect :
|
||||
sure to validate one of the following key's fingerprint when you
|
||||
first connect :
|
||||
<li>
|
||||
<span className={classes.sshPublicKey}>
|
||||
ECDSA :{' '}
|
||||
{process.env.NEXT_PUBLIC_SSH_SERVER_FINGERPRINT_ECDSA}
|
||||
ECDSA : {wizardEnv.SSH_SERVER_FINGERPRINT_ECDSA}
|
||||
</span>
|
||||
</li>
|
||||
<li>
|
||||
<span className={classes.sshPublicKey}>
|
||||
ED25519 :{' '}
|
||||
{process.env.NEXT_PUBLIC_SSH_SERVER_FINGERPRINT_ED25519}
|
||||
ED25519 : {wizardEnv.SSH_SERVER_FINGERPRINT_ED25519}
|
||||
</span>
|
||||
</li>
|
||||
<li>
|
||||
<span className={classes.sshPublicKey}>
|
||||
RSA :{' '}
|
||||
{process.env.NEXT_PUBLIC_SSH_SERVER_FINGERPRINT_RSA}
|
||||
RSA : {wizardEnv.SSH_SERVER_FINGERPRINT_RSA}
|
||||
</span>
|
||||
</li>
|
||||
</div>
|
||||
|
|
|
@ -1,10 +1,20 @@
|
|||
//Lib
|
||||
import React from 'react';
|
||||
import classes from '../WizardStep1/WizardStep1.module.css';
|
||||
import { IconChecks, IconPlayerPlay } from '@tabler/icons';
|
||||
import { IconChecks, IconPlayerPlay } from '@tabler/icons-react';
|
||||
import CopyButton from '../../UI/CopyButton/CopyButton';
|
||||
import lanCommandOption from '../../../helpers/functions/lanCommandOption';
|
||||
|
||||
function WizardStep3(props) {
|
||||
////Vars
|
||||
const wizardEnv = props.wizardEnv;
|
||||
const UNIX_USER = wizardEnv.UNIX_USER;
|
||||
//Needed to generate command for borg over LAN instead of WAN if env vars are set and option enabled.
|
||||
const { FQDN, SSH_SERVER_PORT } = lanCommandOption(
|
||||
wizardEnv,
|
||||
props.selectedOption.lanCommand
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={classes.container}>
|
||||
<h1>
|
||||
|
@ -22,14 +32,12 @@ function WizardStep3(props) {
|
|||
>
|
||||
<div className={classes.code}>
|
||||
borg create ssh://
|
||||
{props.selectedOption.unixUser}@
|
||||
{process.env.NEXT_PUBLIC_HOSTNAME}:
|
||||
{process.env.NEXT_PUBLIC_SSH_SERVER_PORT}/./
|
||||
{props.selectedOption.repository}
|
||||
{UNIX_USER}@{FQDN}:{SSH_SERVER_PORT}/./
|
||||
{props.selectedOption.repositoryName}
|
||||
::archive1 /your/pathToBackup
|
||||
</div>
|
||||
<CopyButton
|
||||
dataToCopy={`borg create ssh://${props.selectedOption.unixUser}@${process.env.NEXT_PUBLIC_HOSTNAME}:${process.env.NEXT_PUBLIC_SSH_SERVER_PORT}/./${props.selectedOption.repository}::archive1 /your/pathToBackup`}
|
||||
dataToCopy={`borg create ssh://${UNIX_USER}@${FQDN}:${SSH_SERVER_PORT}/./${props.selectedOption.repositoryName}::archive1 /your/pathToBackup`}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -70,13 +78,11 @@ function WizardStep3(props) {
|
|||
>
|
||||
<div className={classes.code}>
|
||||
borg check -v --progress ssh://
|
||||
{props.selectedOption.unixUser}@
|
||||
{process.env.NEXT_PUBLIC_HOSTNAME}:
|
||||
{process.env.NEXT_PUBLIC_SSH_SERVER_PORT}/./
|
||||
{props.selectedOption.repository}
|
||||
{UNIX_USER}@{FQDN}:{SSH_SERVER_PORT}/./
|
||||
{props.selectedOption.repositoryName}
|
||||
</div>
|
||||
<CopyButton
|
||||
dataToCopy={`borg check -v --progress ssh://${props.selectedOption.unixUser}@${process.env.NEXT_PUBLIC_HOSTNAME}:${process.env.NEXT_PUBLIC_SSH_SERVER_PORT}/./${props.selectedOption.repository}`}
|
||||
dataToCopy={`borg check -v --progress ssh://${UNIX_USER}@${FQDN}:${SSH_SERVER_PORT}/./${props.selectedOption.repositoryName}`}
|
||||
/>
|
||||
</div>
|
||||
<li>List the remote archives with :</li>
|
||||
|
@ -89,13 +95,11 @@ function WizardStep3(props) {
|
|||
>
|
||||
<div className={classes.code}>
|
||||
borg list ssh://
|
||||
{props.selectedOption.unixUser}@
|
||||
{process.env.NEXT_PUBLIC_HOSTNAME}:
|
||||
{process.env.NEXT_PUBLIC_SSH_SERVER_PORT}/./
|
||||
{props.selectedOption.repository}
|
||||
{UNIX_USER}@{FQDN}:{SSH_SERVER_PORT}/./
|
||||
{props.selectedOption.repositoryName}
|
||||
</div>
|
||||
<CopyButton
|
||||
dataToCopy={`borg list ssh://${props.selectedOption.unixUser}@${process.env.NEXT_PUBLIC_HOSTNAME}:${process.env.NEXT_PUBLIC_SSH_SERVER_PORT}/./${props.selectedOption.repository}`}
|
||||
dataToCopy={`borg list ssh://${UNIX_USER}@${FQDN}:${SSH_SERVER_PORT}/./${props.selectedOption.repositoryName}`}
|
||||
/>
|
||||
</div>
|
||||
<li>Download a remote archive with the following command :</li>
|
||||
|
@ -108,14 +112,12 @@ function WizardStep3(props) {
|
|||
>
|
||||
<div className={classes.code}>
|
||||
borg export-tar --tar-filter="gzip -9" ssh://
|
||||
{props.selectedOption.unixUser}@
|
||||
{process.env.NEXT_PUBLIC_HOSTNAME}:
|
||||
{process.env.NEXT_PUBLIC_SSH_SERVER_PORT}/./
|
||||
{props.selectedOption.repository}
|
||||
{UNIX_USER}@{FQDN}:{SSH_SERVER_PORT}/./
|
||||
{props.selectedOption.repositoryName}
|
||||
::archive1 archive1.tar.gz
|
||||
</div>
|
||||
<CopyButton
|
||||
dataToCopy={`borg export-tar --tar-filter="gzip -9" ssh://${props.selectedOption.unixUser}@${process.env.NEXT_PUBLIC_HOSTNAME}:${process.env.NEXT_PUBLIC_SSH_SERVER_PORT}/./${props.selectedOption.repository}::archive1 archive1.tar.gz`}
|
||||
dataToCopy={`borg export-tar --tar-filter="gzip -9" ssh://${UNIX_USER}@${FQDN}:${SSH_SERVER_PORT}/./${props.selectedOption.repositoryName}::archive1 archive1.tar.gz`}
|
||||
/>
|
||||
</div>
|
||||
<li>
|
||||
|
@ -131,14 +133,12 @@ function WizardStep3(props) {
|
|||
>
|
||||
<div className={classes.code}>
|
||||
borg mount ssh://
|
||||
{props.selectedOption.unixUser}@
|
||||
{process.env.NEXT_PUBLIC_HOSTNAME}:
|
||||
{process.env.NEXT_PUBLIC_SSH_SERVER_PORT}/./
|
||||
{props.selectedOption.repository}
|
||||
{UNIX_USER}@{FQDN}:{SSH_SERVER_PORT}/./
|
||||
{props.selectedOption.repositoryName}
|
||||
::archive1 /tmp/yourMountPoint
|
||||
</div>
|
||||
<CopyButton
|
||||
dataToCopy={`borg mount ssh://${props.selectedOption.unixUser}@${process.env.NEXT_PUBLIC_HOSTNAME}:${process.env.NEXT_PUBLIC_SSH_SERVER_PORT}/./${props.selectedOption.repository}::archive1 /tmp/yourMountPoint`}
|
||||
dataToCopy={`borg mount ssh://${UNIX_USER}@${FQDN}:${SSH_SERVER_PORT}/./${props.selectedOption.repositoryName}::archive1 /tmp/yourMountPoint`}
|
||||
/>
|
||||
</div>
|
||||
<br />
|
||||
|
@ -146,7 +146,7 @@ function WizardStep3(props) {
|
|||
archives, please refer to{' '}
|
||||
<a
|
||||
href='https://borgbackup.readthedocs.io/en/stable/usage/check.html'
|
||||
rel='noopener noreferrer'
|
||||
rel='noreferrer'
|
||||
target='_blank'
|
||||
>
|
||||
this documentation
|
||||
|
@ -158,7 +158,7 @@ function WizardStep3(props) {
|
|||
If you are using Borgmatic, please refer to{' '}
|
||||
<a
|
||||
href='https://torsion.org/borgmatic/docs/how-to/deal-with-very-large-backups/#consistency-check-configuration'
|
||||
rel='noopener noreferrer'
|
||||
rel='noreferrer'
|
||||
target='_blank'
|
||||
>
|
||||
this documentation
|
||||
|
@ -170,7 +170,7 @@ function WizardStep3(props) {
|
|||
If you are using the Vorta graphical client, please refer to{' '}
|
||||
<a
|
||||
href='https://vorta.borgbase.com/usage/'
|
||||
rel='noopener noreferrer'
|
||||
rel='noreferrer'
|
||||
target='_blank'
|
||||
>
|
||||
this documentation
|
||||
|
|
|
@ -1,10 +1,20 @@
|
|||
//Lib
|
||||
import React from 'react';
|
||||
import classes from '../WizardStep1/WizardStep1.module.css';
|
||||
import { IconWand } from '@tabler/icons';
|
||||
import { IconWand } from '@tabler/icons-react';
|
||||
import CopyButton from '../../UI/CopyButton/CopyButton';
|
||||
import lanCommandOption from '../../../helpers/functions/lanCommandOption';
|
||||
|
||||
function WizardStep4(props) {
|
||||
////Vars
|
||||
const wizardEnv = props.wizardEnv;
|
||||
const UNIX_USER = wizardEnv.UNIX_USER;
|
||||
//Needed to generate command for borg over LAN instead of WAN if env vars are set and option enabled.
|
||||
const { FQDN, SSH_SERVER_PORT } = lanCommandOption(
|
||||
wizardEnv,
|
||||
props.selectedOption.lanCommand
|
||||
);
|
||||
|
||||
const configBorgmatic = `location:
|
||||
# List of source directories to backup.
|
||||
source_directories:
|
||||
|
@ -13,10 +23,10 @@ function WizardStep4(props) {
|
|||
|
||||
repositories:
|
||||
# Paths of local or remote repositories to backup to.
|
||||
- ssh://${props.selectedOption.unixUser}@${process.env.NEXT_PUBLIC_HOSTNAME}:${process.env.NEXT_PUBLIC_SSH_SERVER_PORT}/./${props.selectedOption.repository}
|
||||
- ssh://${UNIX_USER}@${FQDN}:${SSH_SERVER_PORT}/./${props.selectedOption.repositoryName}
|
||||
|
||||
storage:
|
||||
archive_name_format: '{NEXT_PUBLIC_HOSTNAME}-documents-{now}'
|
||||
archive_name_format: '{FQDN}-documents-{now}'
|
||||
encryption_passphrase: "YOUR PASSPHRASE"
|
||||
|
||||
retention:
|
||||
|
@ -30,7 +40,7 @@ consistency:
|
|||
checks:
|
||||
- name: repository
|
||||
- name: archives
|
||||
- frequency: 2 weeks
|
||||
frequency: 2 weeks
|
||||
|
||||
#hooks:
|
||||
# Custom preparation scripts to run.
|
||||
|
@ -55,7 +65,7 @@ consistency:
|
|||
documentation
|
||||
<a
|
||||
href='https://borgbackup.readthedocs.io/en/stable/quickstart.html#automating-backups'
|
||||
rel='noopener noreferrer'
|
||||
rel='noreferrer'
|
||||
target='_blank'
|
||||
>
|
||||
right here
|
||||
|
@ -70,7 +80,7 @@ consistency:
|
|||
to
|
||||
<a
|
||||
href='https://vorta.borgbase.com/usage/#scheduling-automatic-backups'
|
||||
rel='noopener noreferrer'
|
||||
rel='noreferrer'
|
||||
target='_blank'
|
||||
>
|
||||
this documentation
|
||||
|
@ -83,7 +93,7 @@ consistency:
|
|||
If you are using Borgmatic, you can check
|
||||
<a
|
||||
href='https://torsion.org/borgmatic/docs/how-to/set-up-backups/#autopilot'
|
||||
rel='noopener noreferrer'
|
||||
rel='noreferrer'
|
||||
target='_blank'
|
||||
>
|
||||
this documentation
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
//Lib
|
||||
import React from 'react';
|
||||
import classes from './WizardStepBar.module.css';
|
||||
import { IconChevronLeft, IconChevronRight } from '@tabler/icons';
|
||||
import { IconChevronLeft, IconChevronRight } from '@tabler/icons-react';
|
||||
|
||||
function WizardStepBar(props) {
|
||||
////Functions
|
||||
|
|
|
@ -78,7 +78,7 @@ export default function StorageUsedChartBar() {
|
|||
datasets: [
|
||||
{
|
||||
label: 'Storage used (%)',
|
||||
//storageUsed is in octet, storageSize is in Go. Round to 1 decimal for %.
|
||||
//storageUsed is in octet, storageSize is in GB. Round to 1 decimal for %.
|
||||
data: data.map((repo) =>
|
||||
(
|
||||
((repo.storageUsed / 1000000) * 100) /
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
//Lib
|
||||
import classes from './RepoList.module.css';
|
||||
import { useState, useEffect, useRef } from 'react';
|
||||
import { IconPlus, IconChevronDown, IconChevronUp } from '@tabler/icons';
|
||||
import { useState, useEffect } from 'react';
|
||||
import { IconPlus } from '@tabler/icons-react';
|
||||
import { useRouter } from 'next/router';
|
||||
import Link from 'next/link';
|
||||
import useSWR, { useSWRConfig } from 'swr';
|
||||
|
@ -43,12 +43,27 @@ export default function RepoList() {
|
|||
if (router.pathname.startsWith('/manage-repo/edit')) {
|
||||
setDisplayRepoEdit(!displayRepoEdit);
|
||||
}
|
||||
//Fetch wizardEnv to hydrate Repo components
|
||||
const fetchWizardEnv = async () => {
|
||||
try {
|
||||
const response = await fetch('/api/account/getWizardEnv', {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-type': 'application/json',
|
||||
},
|
||||
});
|
||||
setWizardEnv((await response.json()).wizardEnv);
|
||||
} catch (error) {
|
||||
console.log('Fetching datas error');
|
||||
}
|
||||
};
|
||||
fetchWizardEnv();
|
||||
}, []);
|
||||
|
||||
////States
|
||||
const [displayRepoAdd, setDisplayRepoAdd] = useState(false);
|
||||
const [displayRepoEdit, setDisplayRepoEdit] = useState(false);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [wizardEnv, setWizardEnv] = useState({});
|
||||
|
||||
////Functions
|
||||
|
||||
|
@ -67,36 +82,8 @@ export default function RepoList() {
|
|||
return <ToastContainer />;
|
||||
}
|
||||
|
||||
//BUTTON : Display or not repo details for ONE repo
|
||||
const displayDetailsForOneHandler = async (id, boolean) => {
|
||||
setIsLoading(true);
|
||||
await fetch('/api/repo/id/' + id + '/displayDetails', {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ displayDetails: boolean }),
|
||||
})
|
||||
.then((response) => {
|
||||
if (response.ok) {
|
||||
mutate('/api/repo');
|
||||
setIsLoading(false);
|
||||
} else {
|
||||
setIsLoading(false);
|
||||
toast.error('API error', toastOptions);
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.log(error);
|
||||
setIsLoading(false);
|
||||
toast.error('API error', toastOptions);
|
||||
});
|
||||
};
|
||||
|
||||
//BUTTON : Display RepoManage component box for ADD
|
||||
const manageRepoAddHandler = () => {
|
||||
setDisplayRepoAdd(!displayRepoAdd);
|
||||
//Redirect url to HOME when cross is clicked.
|
||||
router.replace('/manage-repo/add');
|
||||
};
|
||||
|
||||
|
@ -129,48 +116,17 @@ export default function RepoList() {
|
|||
alias={repo.alias}
|
||||
status={repo.status}
|
||||
lastSave={repo.lastSave}
|
||||
repository={repo.repository}
|
||||
alert={repo.alert}
|
||||
repositoryName={repo.repositoryName}
|
||||
storageSize={repo.storageSize}
|
||||
storageUsed={repo.storageUsed}
|
||||
sshPublicKey={repo.sshPublicKey}
|
||||
displayDetails={repo.displayDetails}
|
||||
unixUser={repo.unixUser}
|
||||
comment={repo.comment}
|
||||
lanCommand={repo.lanCommand}
|
||||
appendOnlyMode={repo.appendOnlyMode}
|
||||
repoManageEditHandler={() => repoManageEditHandler(repo.id)}
|
||||
wizardEnv={wizardEnv}
|
||||
></Repo>
|
||||
{repo.displayDetails ? (
|
||||
<div className={classes.chevron}>
|
||||
{isLoading ? (
|
||||
<IconChevronUp color='#494b7a' size={28} />
|
||||
) : (
|
||||
<IconChevronUp
|
||||
color='#494b7a'
|
||||
size={28}
|
||||
onClick={() => {
|
||||
displayDetailsForOneHandler(repo.id, false);
|
||||
// tell all SWRs with this key to revalidate
|
||||
mutate('/api/repo');
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div className={classes.chevron}>
|
||||
{isLoading ? (
|
||||
<IconChevronDown color='#494b7a' size={28} />
|
||||
) : (
|
||||
<IconChevronDown
|
||||
color='#494b7a'
|
||||
size={28}
|
||||
onClick={() => {
|
||||
displayDetailsForOneHandler(repo.id, true);
|
||||
// tell all SWRs with this key to revalidate
|
||||
mutate('/api/repo');
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
|
|
@ -112,17 +112,6 @@
|
|||
transform: scale(0.96);
|
||||
}
|
||||
|
||||
.chevron {
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
.chevron :focus,
|
||||
.chevron :hover {
|
||||
cursor: pointer;
|
||||
filter: invert(27%) sepia(82%) saturate(2209%) hue-rotate(240deg)
|
||||
brightness(99%) contrast(105%);
|
||||
}
|
||||
|
||||
@media all and (max-width: 1000px) {
|
||||
.newRepoButton {
|
||||
display: none;
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
//Lib
|
||||
import classes from './RepoManage.module.css';
|
||||
import { IconAlertCircle, IconX } from '@tabler/icons';
|
||||
import { IconAlertCircle, IconX } from '@tabler/icons-react';
|
||||
import { useState } from 'react';
|
||||
import { useRouter } from 'next/router';
|
||||
import { toast } from 'react-toastify';
|
||||
|
@ -8,6 +8,8 @@ import 'react-toastify/dist/ReactToastify.css';
|
|||
import { useForm, Controller } from 'react-hook-form';
|
||||
import { SpinnerDotted } from 'spinners-react';
|
||||
import Select from 'react-select';
|
||||
import Link from 'next/link';
|
||||
import { IconExternalLink } from '@tabler/icons-react';
|
||||
|
||||
export default function RepoManage(props) {
|
||||
////Var
|
||||
|
@ -21,6 +23,7 @@ export default function RepoManage(props) {
|
|||
} = useForm({ mode: 'onChange' });
|
||||
//List of possible times for alerts
|
||||
const alertOptions = [
|
||||
{ value: 0, label: 'Disabled' },
|
||||
{ value: 3600, label: '1 hour' },
|
||||
{ value: 21600, label: '6 hours' },
|
||||
{ value: 43200, label: '12 hours' },
|
||||
|
@ -106,10 +109,58 @@ export default function RepoManage(props) {
|
|||
});
|
||||
};
|
||||
|
||||
//Verify that the SSH key is unique
|
||||
const isSSHKeyUnique = async (sshPublicKey) => {
|
||||
let isUnique = true;
|
||||
|
||||
// Extract the first two columns of the SSH key in the form
|
||||
const publicKeyPrefix = sshPublicKey.split(' ').slice(0, 2).join(' ');
|
||||
|
||||
await fetch('/api/repo', { method: 'GET' })
|
||||
.then((response) => response.json())
|
||||
.then((data) => {
|
||||
for (let element in data.repoList) {
|
||||
// Extract the first two columns of the SSH key in the repoList
|
||||
const repoPublicKeyPrefix = data.repoList[
|
||||
element
|
||||
].sshPublicKey
|
||||
.split(' ')
|
||||
.slice(0, 2)
|
||||
.join(' ');
|
||||
|
||||
if (
|
||||
repoPublicKeyPrefix === publicKeyPrefix && // Compare the first two columns of the SSH key
|
||||
(!targetRepo ||
|
||||
data.repoList[element].id != targetRepo.id)
|
||||
) {
|
||||
toast.error(
|
||||
'The SSH key is already used in repository #' +
|
||||
data.repoList[element].id +
|
||||
'. Please use another key or delete the key from the other repository.',
|
||||
toastOptions
|
||||
);
|
||||
isUnique = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.log(error);
|
||||
toast.error('An error has occurred', toastOptions);
|
||||
isUnique = false;
|
||||
});
|
||||
return isUnique;
|
||||
};
|
||||
|
||||
//Form submit Handler for ADD or EDIT a repo
|
||||
const formSubmitHandler = async (dataForm) => {
|
||||
//Loading button on submit to avoid multiple send.
|
||||
setIsLoading(true);
|
||||
//Verify that the SSH key is unique
|
||||
if (!(await isSSHKeyUnique(dataForm.sshkey))) {
|
||||
setIsLoading(false);
|
||||
return;
|
||||
}
|
||||
//ADD a repo
|
||||
if (props.mode == 'add') {
|
||||
const newRepo = {
|
||||
|
@ -118,6 +169,8 @@ export default function RepoManage(props) {
|
|||
sshPublicKey: dataForm.sshkey,
|
||||
comment: dataForm.comment,
|
||||
alert: dataForm.alert.value,
|
||||
lanCommand: dataForm.lanCommand,
|
||||
appendOnlyMode: dataForm.appendOnlyMode,
|
||||
};
|
||||
//POST API to send new repo
|
||||
await fetch('/api/repo/add', {
|
||||
|
@ -127,7 +180,7 @@ export default function RepoManage(props) {
|
|||
},
|
||||
body: JSON.stringify(newRepo),
|
||||
})
|
||||
.then((response) => {
|
||||
.then(async (response) => {
|
||||
if (response.ok) {
|
||||
toast.success(
|
||||
'New repository added ! 🥳',
|
||||
|
@ -135,9 +188,13 @@ export default function RepoManage(props) {
|
|||
);
|
||||
router.replace('/');
|
||||
} else {
|
||||
toast.error('An error has occurred', toastOptions);
|
||||
const errorMessage = await response.json();
|
||||
toast.error(
|
||||
`An error has occurred : ${errorMessage.message}`,
|
||||
toastOptions
|
||||
);
|
||||
router.replace('/');
|
||||
console.log('Fail to post');
|
||||
console.log(`Fail to ${props.mode}`);
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
|
@ -153,6 +210,8 @@ export default function RepoManage(props) {
|
|||
sshPublicKey: dataForm.sshkey,
|
||||
comment: dataForm.comment,
|
||||
alert: dataForm.alert.value,
|
||||
lanCommand: dataForm.lanCommand,
|
||||
appendOnlyMode: dataForm.appendOnlyMode,
|
||||
};
|
||||
await fetch('/api/repo/id/' + router.query.slug + '/edit', {
|
||||
method: 'PUT',
|
||||
|
@ -161,7 +220,7 @@ export default function RepoManage(props) {
|
|||
},
|
||||
body: JSON.stringify(dataEdited),
|
||||
})
|
||||
.then((response) => {
|
||||
.then(async (response) => {
|
||||
if (response.ok) {
|
||||
toast.success(
|
||||
'The repository #' +
|
||||
|
@ -171,9 +230,13 @@ export default function RepoManage(props) {
|
|||
);
|
||||
router.replace('/');
|
||||
} else {
|
||||
toast.error('An error has occurred', toastOptions);
|
||||
const errorMessage = await response.json();
|
||||
toast.error(
|
||||
`An error has occurred : ${errorMessage.message}`,
|
||||
toastOptions
|
||||
);
|
||||
router.replace('/');
|
||||
console.log('Fail to PUT');
|
||||
console.log(`Fail to ${props.mode}`);
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
|
@ -323,9 +386,10 @@ export default function RepoManage(props) {
|
|||
</span>
|
||||
)}
|
||||
{/* SIZE */}
|
||||
<label htmlFor='size'>Storage Size (Go)</label>
|
||||
<label htmlFor='size'>Storage Size (GB)</label>
|
||||
<input
|
||||
type='number'
|
||||
min='1'
|
||||
defaultValue={
|
||||
props.mode == 'edit'
|
||||
? targetRepo.storageSize
|
||||
|
@ -333,11 +397,6 @@ export default function RepoManage(props) {
|
|||
}
|
||||
{...register('size', {
|
||||
required: 'A size is required.',
|
||||
maxLength: {
|
||||
value: 3,
|
||||
message:
|
||||
'999(Go) is the maximum value.',
|
||||
},
|
||||
})}
|
||||
/>
|
||||
{errors.size && (
|
||||
|
@ -368,6 +427,66 @@ export default function RepoManage(props) {
|
|||
{errors.comment.message}
|
||||
</span>
|
||||
)}
|
||||
{/* LAN COMMAND GENERATION */}
|
||||
<div className={classes.optionCommandWrapper}>
|
||||
<input
|
||||
type='checkbox'
|
||||
name='lanCommand'
|
||||
defaultChecked={
|
||||
props.mode == 'edit'
|
||||
? targetRepo.lanCommand
|
||||
: false
|
||||
}
|
||||
{...register('lanCommand')}
|
||||
/>
|
||||
<label htmlFor='lanCommand'>
|
||||
Generates commands for use over LAN.
|
||||
</label>
|
||||
<Link
|
||||
style={{
|
||||
alignSelf: 'baseline',
|
||||
marginLeft: '5px',
|
||||
}}
|
||||
href='https://borgwarehouse.com/docs/user-manual/repositories/#generates-commands-for-use-over-lan'
|
||||
rel='noreferrer'
|
||||
target='_blank'
|
||||
>
|
||||
<IconExternalLink
|
||||
size={16}
|
||||
color='#6c737f'
|
||||
/>
|
||||
</Link>
|
||||
</div>
|
||||
{/* APPEND-ONLY MODE */}
|
||||
<div className={classes.optionCommandWrapper}>
|
||||
<input
|
||||
type='checkbox'
|
||||
name='appendOnlyMode'
|
||||
defaultChecked={
|
||||
props.mode == 'edit'
|
||||
? targetRepo.appendOnlyMode
|
||||
: false
|
||||
}
|
||||
{...register('appendOnlyMode')}
|
||||
/>
|
||||
<label htmlFor='appendOnlyMode'>
|
||||
Enable append-only mode.
|
||||
</label>
|
||||
<Link
|
||||
style={{
|
||||
alignSelf: 'baseline',
|
||||
marginLeft: '5px',
|
||||
}}
|
||||
href='https://borgwarehouse.com/docs/user-manual/repositories/#append-only-mode'
|
||||
rel='noreferrer'
|
||||
target='_blank'
|
||||
>
|
||||
<IconExternalLink
|
||||
size={16}
|
||||
color='#6c737f'
|
||||
/>
|
||||
</Link>
|
||||
</div>
|
||||
{/* ALERT */}
|
||||
<label
|
||||
style={{ margin: '25px auto 10px auto' }}
|
||||
|
@ -385,7 +504,7 @@ export default function RepoManage(props) {
|
|||
x.value ===
|
||||
targetRepo.alert
|
||||
)
|
||||
: alertOptions[3]
|
||||
: alertOptions[4]
|
||||
}
|
||||
control={control}
|
||||
render={({
|
||||
|
|
|
@ -126,6 +126,28 @@
|
|||
margin-top: 3px;
|
||||
}
|
||||
|
||||
.optionCommandWrapper {
|
||||
display: flex;
|
||||
margin-top: 20px;
|
||||
color: #494b7a;
|
||||
}
|
||||
|
||||
.optionCommandWrapper label {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.optionCommandWrapper input[type='checkbox'] {
|
||||
width: auto;
|
||||
margin-right: 8px;
|
||||
cursor: pointer;
|
||||
accent-color: #6d4aff;
|
||||
}
|
||||
.optionCommandWrapper input[type='checkbox']:focus {
|
||||
outline: 0;
|
||||
box-shadow: none;
|
||||
accent-color: #6d4aff;
|
||||
}
|
||||
|
||||
/* DELETE DIALOG */
|
||||
|
||||
.deleteDialogWrapper {
|
||||
|
|
|
@ -20,10 +20,10 @@ function SetupWizard(props) {
|
|||
const [list, setList] = useState([]);
|
||||
const [listIsLoading, setListIsLoading] = useState(true);
|
||||
const [step, setStep] = useState();
|
||||
const [wizardEnv, setWizardEnv] = useState({});
|
||||
const [selectedOption, setSelectedOption] = useState({
|
||||
id: '#id',
|
||||
repository: 'repo',
|
||||
unixUser: 'user',
|
||||
});
|
||||
|
||||
////LifeCycle
|
||||
|
@ -45,6 +45,21 @@ function SetupWizard(props) {
|
|||
}
|
||||
};
|
||||
repoList();
|
||||
//Fetch wizardEnv to hydrate Wizard' steps
|
||||
const fetchWizardEnv = async () => {
|
||||
try {
|
||||
const response = await fetch('/api/account/getWizardEnv', {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-type': 'application/json',
|
||||
},
|
||||
});
|
||||
setWizardEnv((await response.json()).wizardEnv);
|
||||
} catch (error) {
|
||||
console.log('Fetching datas error');
|
||||
}
|
||||
};
|
||||
fetchWizardEnv();
|
||||
}, []);
|
||||
//Component did update
|
||||
useEffect(() => {
|
||||
|
@ -59,8 +74,8 @@ function SetupWizard(props) {
|
|||
label: `${repo.alias} - #${repo.id}`,
|
||||
value: `${repo.alias} - #${repo.id}`,
|
||||
id: repo.id,
|
||||
repository: repo.repository,
|
||||
unixUser: repo.unixUser,
|
||||
repositoryName: repo.repositoryName,
|
||||
lanCommand: repo.lanCommand,
|
||||
}));
|
||||
|
||||
//Step button (free selection of user)
|
||||
|
@ -85,11 +100,26 @@ function SetupWizard(props) {
|
|||
if (step == 1) {
|
||||
return <WizardStep1 />;
|
||||
} else if (step == 2) {
|
||||
return <WizardStep2 selectedOption={selectedOption} />;
|
||||
return (
|
||||
<WizardStep2
|
||||
selectedOption={selectedOption}
|
||||
wizardEnv={wizardEnv}
|
||||
/>
|
||||
);
|
||||
} else if (step == 3) {
|
||||
return <WizardStep3 selectedOption={selectedOption} />;
|
||||
return (
|
||||
<WizardStep3
|
||||
selectedOption={selectedOption}
|
||||
wizardEnv={wizardEnv}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
return <WizardStep4 selectedOption={selectedOption} />;
|
||||
return (
|
||||
<WizardStep4
|
||||
selectedOption={selectedOption}
|
||||
wizardEnv={wizardEnv}
|
||||
/>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -0,0 +1,220 @@
|
|||
//Lib
|
||||
import { useEffect } from 'react';
|
||||
import { toast } from 'react-toastify';
|
||||
import 'react-toastify/dist/ReactToastify.css';
|
||||
import classes from '../UserSettings.module.css';
|
||||
import { useState } from 'react';
|
||||
import { SpinnerCircularFixed } from 'spinners-react';
|
||||
import { IconExternalLink } from '@tabler/icons-react';
|
||||
import Link from 'next/link';
|
||||
|
||||
//Components
|
||||
import Error from '../../../Components/UI/Error/Error';
|
||||
import Switch from '../../../Components/UI/Switch/Switch';
|
||||
import AppriseURLs from './AppriseURLs/AppriseURLs';
|
||||
import AppriseMode from './AppriseMode/AppriseMode';
|
||||
|
||||
export default function AppriseAlertSettings() {
|
||||
//Var
|
||||
const toastOptions = {
|
||||
position: 'top-right',
|
||||
autoClose: 5000,
|
||||
hideProgressBar: false,
|
||||
closeOnClick: true,
|
||||
pauseOnHover: true,
|
||||
draggable: true,
|
||||
progress: undefined,
|
||||
//Callback > re-enabled button after notification.
|
||||
onClose: () => setDisabled(false),
|
||||
};
|
||||
|
||||
////State
|
||||
const [checkIsLoading, setCheckIsLoading] = useState(true);
|
||||
const [error, setError] = useState();
|
||||
const [disabled, setDisabled] = useState(false);
|
||||
const [checked, setChecked] = useState();
|
||||
const [testIsLoading, setTestIsLoading] = useState(false);
|
||||
const [info, setInfo] = useState(false);
|
||||
|
||||
////LifeCycle
|
||||
//Component did mount
|
||||
useEffect(() => {
|
||||
//Initial fetch to get the status of Apprise Alert
|
||||
const getAppriseAlert = async () => {
|
||||
try {
|
||||
const response = await fetch('/api/account/getAppriseAlert', {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-type': 'application/json',
|
||||
},
|
||||
});
|
||||
setChecked((await response.json()).appriseAlert);
|
||||
setCheckIsLoading(false);
|
||||
} catch (error) {
|
||||
setError(
|
||||
'Fetching apprise alert setting failed. Contact your administrator.'
|
||||
);
|
||||
console.log('Fetching apprise alert setting failed.');
|
||||
setCheckIsLoading(false);
|
||||
}
|
||||
};
|
||||
getAppriseAlert();
|
||||
}, []);
|
||||
|
||||
////Functions
|
||||
//Switch to enable/disable Apprise notifications
|
||||
const onChangeSwitchHandler = async (data) => {
|
||||
//Remove old error
|
||||
setError();
|
||||
//Disabled button
|
||||
setDisabled(true);
|
||||
await fetch('/api/account/updateAppriseAlert', {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(data),
|
||||
})
|
||||
.then((response) => {
|
||||
console.log(response);
|
||||
if (response.ok) {
|
||||
if (data.appriseAlert) {
|
||||
setChecked(!checked);
|
||||
toast.success(
|
||||
'Apprise notifications enabled.',
|
||||
toastOptions
|
||||
);
|
||||
} else {
|
||||
setChecked(!checked);
|
||||
toast.success(
|
||||
'Apprise notifications disabled.',
|
||||
toastOptions
|
||||
);
|
||||
}
|
||||
} else {
|
||||
setError('Update apprise alert setting failed.');
|
||||
setTimeout(() => {
|
||||
setError();
|
||||
setDisabled(false);
|
||||
}, 4000);
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.log(error);
|
||||
setError('Update Apprise failed. Contact your administrator.');
|
||||
setTimeout(() => {
|
||||
setError();
|
||||
setDisabled(false);
|
||||
}, 4000);
|
||||
});
|
||||
};
|
||||
|
||||
//Send Apprise test notification to services
|
||||
const onSendTestAppriseHandler = async () => {
|
||||
//Loading
|
||||
setTestIsLoading(true);
|
||||
//Remove old error
|
||||
setError();
|
||||
try {
|
||||
const response = await fetch('/api/account/sendTestApprise', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ sendTestApprise: true }),
|
||||
});
|
||||
const result = await response.json();
|
||||
if (!response.ok) {
|
||||
setTestIsLoading(false);
|
||||
setError(result.message);
|
||||
} else {
|
||||
setTestIsLoading(false);
|
||||
setInfo(true);
|
||||
setTimeout(() => {
|
||||
setInfo(false);
|
||||
}, 4000);
|
||||
}
|
||||
} catch (error) {
|
||||
setTestIsLoading(false);
|
||||
console.log(error);
|
||||
setError('Send notification failed. Contact your administrator.');
|
||||
setTimeout(() => {
|
||||
setError();
|
||||
}, 4000);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* APPRISE ALERT */}
|
||||
<div className={classes.containerSetting}>
|
||||
<div className={classes.settingCategory}>
|
||||
<h2 style={{ alignSelf: 'baseline' }}>Apprise alert</h2>
|
||||
<Link
|
||||
style={{ alignSelf: 'baseline', marginLeft: '5px' }}
|
||||
href='https://borgwarehouse.com/docs/user-manual/account/#apprise'
|
||||
rel='noreferrer'
|
||||
target='_blank'
|
||||
>
|
||||
<IconExternalLink size={16} color='#6c737f' />
|
||||
</Link>
|
||||
</div>
|
||||
<div className={classes.setting}>
|
||||
<div className={classes.bwFormWrapper}>
|
||||
{/* NOTIFY SWITCH */}
|
||||
{checkIsLoading ? (
|
||||
<SpinnerCircularFixed
|
||||
size={30}
|
||||
thickness={150}
|
||||
speed={150}
|
||||
color='#704dff'
|
||||
secondaryColor='#c3b6fa'
|
||||
/>
|
||||
) : (
|
||||
<Switch
|
||||
checked={checked}
|
||||
disabled={disabled}
|
||||
switchName='Notify my Apprise services'
|
||||
switchDescription='You will receive an alert on all your services every 24H if you have a down status.'
|
||||
onChange={(e) =>
|
||||
onChangeSwitchHandler({ appriseAlert: e })
|
||||
}
|
||||
/>
|
||||
)}
|
||||
{/* APPRISE SERVICES URLS */}
|
||||
<AppriseURLs />
|
||||
{/* APPRISE MODE SELECTION */}
|
||||
<AppriseMode />
|
||||
{/* APPRISE TEST BUTTON */}
|
||||
{testIsLoading ? (
|
||||
<SpinnerCircularFixed
|
||||
style={{ marginTop: '20px' }}
|
||||
size={30}
|
||||
thickness={150}
|
||||
speed={150}
|
||||
color='#704dff'
|
||||
secondaryColor='#c3b6fa'
|
||||
/>
|
||||
) : (
|
||||
<button
|
||||
style={{ marginTop: '20px' }}
|
||||
className='defaultButton'
|
||||
onClick={() => onSendTestAppriseHandler()}
|
||||
>
|
||||
Send a test notification
|
||||
</button>
|
||||
)}
|
||||
{info && (
|
||||
<span
|
||||
style={{ marginLeft: '10px', color: '#119300' }}
|
||||
>
|
||||
Notification successfully sent.
|
||||
</span>
|
||||
)}
|
||||
{error && <Error message={error} />}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,173 @@
|
|||
//Lib
|
||||
import { useEffect } from 'react';
|
||||
import classes from '../../UserSettings.module.css';
|
||||
import { useState } from 'react';
|
||||
import { SpinnerCircularFixed } from 'spinners-react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
|
||||
//Components
|
||||
import Error from '../../../../Components/UI/Error/Error';
|
||||
|
||||
export default function AppriseMode() {
|
||||
//Var
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
formState: { errors },
|
||||
} = useForm({ mode: 'onBlur' });
|
||||
|
||||
////State
|
||||
const [formIsLoading, setFormIsLoading] = useState(false);
|
||||
const [modeFormIsSaved, setModeFormIsSaved] = useState(false);
|
||||
const [error, setError] = useState(false);
|
||||
const [displayStatelessURL, setDisplayStatelessURL] = useState(false);
|
||||
const [appriseMode, setAppriseMode] = useState('stateless');
|
||||
const [appriseStatelessURL, setAppriseStatelessURL] = useState();
|
||||
|
||||
////LifeCycle
|
||||
//Component did mount
|
||||
useEffect(() => {
|
||||
//Initial fetch to get Apprise Mode enabled
|
||||
const getAppriseMode = async () => {
|
||||
try {
|
||||
const response = await fetch('/api/account/getAppriseMode', {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-type': 'application/json',
|
||||
},
|
||||
});
|
||||
const { appriseStatelessURL, appriseMode } =
|
||||
await response.json();
|
||||
setAppriseMode(appriseMode);
|
||||
if (appriseMode == 'stateless') {
|
||||
setAppriseStatelessURL(appriseStatelessURL);
|
||||
setDisplayStatelessURL(true);
|
||||
}
|
||||
} catch (error) {
|
||||
console.log('Fetching Apprise Mode failed.');
|
||||
}
|
||||
};
|
||||
getAppriseMode();
|
||||
}, []);
|
||||
|
||||
////Functions
|
||||
//Form submit handler to modify Apprise Mode
|
||||
const modeFormSubmitHandler = async (data) => {
|
||||
//Remove old error
|
||||
setError();
|
||||
//Loading button on submit to avoid multiple send.
|
||||
setFormIsLoading(true);
|
||||
//POST API to update Apprise Mode
|
||||
try {
|
||||
const response = await fetch('/api/account/updateAppriseMode', {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
const result = await response.json();
|
||||
|
||||
if (!response.ok) {
|
||||
setFormIsLoading(false);
|
||||
setError(result.message);
|
||||
setTimeout(() => setError(), 4000);
|
||||
} else {
|
||||
setFormIsLoading(false);
|
||||
setModeFormIsSaved(true);
|
||||
setTimeout(() => setModeFormIsSaved(false), 3000);
|
||||
}
|
||||
} catch (error) {
|
||||
setFormIsLoading(false);
|
||||
setError('Change mode failed. Contact your administrator.');
|
||||
setTimeout(() => {
|
||||
setError();
|
||||
}, 4000);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* APPRISE MODE SELECTION */}
|
||||
<div className={classes.headerFormAppriseUrls}>
|
||||
<div style={{ margin: '0px 10px 0px 0px' }}>Apprise mode</div>
|
||||
<div style={{ display: 'flex' }}>
|
||||
{formIsLoading && (
|
||||
<SpinnerCircularFixed
|
||||
size={18}
|
||||
thickness={150}
|
||||
speed={150}
|
||||
color='#704dff'
|
||||
secondaryColor='#c3b6fa'
|
||||
/>
|
||||
)}
|
||||
{modeFormIsSaved && (
|
||||
<div className={classes.formIsSavedMessage}>
|
||||
✅ Apprise mode has been saved.
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{error && <Error message={error} />}
|
||||
<form
|
||||
className={classes.bwForm}
|
||||
onBlur={handleSubmit(modeFormSubmitHandler)}
|
||||
>
|
||||
<div className='radio-group'>
|
||||
<label style={{ marginRight: '50px' }}>
|
||||
<div style={{ display: 'flex' }}>
|
||||
<input
|
||||
{...register('appriseMode')}
|
||||
type='radio'
|
||||
value='package'
|
||||
onClick={() => {
|
||||
setDisplayStatelessURL(false);
|
||||
setAppriseMode('package');
|
||||
}}
|
||||
checked={
|
||||
appriseMode == 'package' ? true : false
|
||||
}
|
||||
/>
|
||||
<span>Local package</span>
|
||||
</div>
|
||||
</label>
|
||||
<label>
|
||||
<div style={{ display: 'flex' }}>
|
||||
<input
|
||||
{...register('appriseMode')}
|
||||
value='stateless'
|
||||
type='radio'
|
||||
onClick={() => {
|
||||
setDisplayStatelessURL(true);
|
||||
setAppriseMode('stateless');
|
||||
}}
|
||||
checked={
|
||||
appriseMode == 'stateless' ? true : false
|
||||
}
|
||||
/>
|
||||
<span>Stateless API server</span>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
{displayStatelessURL && (
|
||||
<input
|
||||
type='text'
|
||||
placeholder='http://localhost:8000'
|
||||
defaultValue={appriseStatelessURL}
|
||||
{...register('appriseStatelessURL', {
|
||||
pattern: {
|
||||
value: /^(http|https):\/\/.+/g,
|
||||
message: 'Invalid URL format.',
|
||||
},
|
||||
})}
|
||||
/>
|
||||
)}
|
||||
{errors.appriseStatelessURL && (
|
||||
<small className={classes.errorMessage}>
|
||||
{errors.appriseStatelessURL.message}
|
||||
</small>
|
||||
)}
|
||||
</form>
|
||||
</>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,163 @@
|
|||
//Lib
|
||||
import { useEffect } from 'react';
|
||||
import classes from '../../UserSettings.module.css';
|
||||
import { useState } from 'react';
|
||||
import { SpinnerCircularFixed } from 'spinners-react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
|
||||
//Components
|
||||
import Error from '../../../../Components/UI/Error/Error';
|
||||
|
||||
export default function AppriseURLs() {
|
||||
//Var
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
formState: { errors },
|
||||
} = useForm({ mode: 'onBlur' });
|
||||
|
||||
////State
|
||||
const [formIsLoading, setFormIsLoading] = useState(false);
|
||||
const [urlsFormIsSaved, setUrlsFormIsSaved] = useState(false);
|
||||
const [appriseServicesList, setAppriseServicesList] = useState();
|
||||
const [error, setError] = useState();
|
||||
|
||||
////LifeCycle
|
||||
//Component did mount
|
||||
useEffect(() => {
|
||||
//Initial fetch to build the list of Apprise Services enabled
|
||||
const getAppriseServices = async () => {
|
||||
try {
|
||||
const response = await fetch(
|
||||
'/api/account/getAppriseServices',
|
||||
{
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-type': 'application/json',
|
||||
},
|
||||
}
|
||||
);
|
||||
let servicesArray = (await response.json()).appriseServices;
|
||||
const AppriseServicesListToText = () => {
|
||||
let list = '';
|
||||
for (let service of servicesArray) {
|
||||
list += service + '\n';
|
||||
}
|
||||
return list;
|
||||
};
|
||||
setAppriseServicesList(AppriseServicesListToText());
|
||||
} catch (error) {
|
||||
console.log('Fetching Apprise services list failed.');
|
||||
}
|
||||
};
|
||||
getAppriseServices();
|
||||
}, []);
|
||||
|
||||
////Functions
|
||||
//Form submit handler to modify Apprise services
|
||||
const urlsFormSubmitHandler = async (data) => {
|
||||
//Remove old error
|
||||
setError();
|
||||
//Loading button on submit to avoid multiple send.
|
||||
setFormIsLoading(true);
|
||||
//POST API to update Apprise Services
|
||||
try {
|
||||
const response = await fetch('/api/account/updateAppriseServices', {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
const result = await response.json();
|
||||
|
||||
if (!response.ok) {
|
||||
setFormIsLoading(false);
|
||||
setError(result.message);
|
||||
setTimeout(() => setError(), 4000);
|
||||
} else {
|
||||
setFormIsLoading(false);
|
||||
setUrlsFormIsSaved(true);
|
||||
setTimeout(() => setUrlsFormIsSaved(false), 3000);
|
||||
}
|
||||
} catch (error) {
|
||||
setFormIsLoading(false);
|
||||
setError(
|
||||
'Failed to update your services. Contact your administrator.'
|
||||
);
|
||||
setTimeout(() => {
|
||||
setError();
|
||||
}, 4000);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* APPRISE SERVICES URLS */}
|
||||
<div className={classes.headerFormAppriseUrls}>
|
||||
<div style={{ marginRight: '10px' }}>Apprise URLs</div>
|
||||
{error && <Error message={error} />}
|
||||
<div style={{ display: 'flex' }}>
|
||||
{formIsLoading && (
|
||||
<SpinnerCircularFixed
|
||||
size={18}
|
||||
thickness={150}
|
||||
speed={150}
|
||||
color='#704dff'
|
||||
secondaryColor='#c3b6fa'
|
||||
/>
|
||||
)}
|
||||
{urlsFormIsSaved && (
|
||||
<div className={classes.formIsSavedMessage}>
|
||||
✅ Apprise configuration has been saved.
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<form
|
||||
onBlur={handleSubmit(urlsFormSubmitHandler)}
|
||||
className={classes.bwForm + ' ' + classes.currentSetting}
|
||||
>
|
||||
<textarea
|
||||
style={{ height: '100px' }}
|
||||
type='text'
|
||||
placeholder={
|
||||
'matrixs://{user}:{password}@{matrixhost}\ndiscord://{WebhookID}/{WebhookToken}\nmmosts://user@hostname/authkey'
|
||||
}
|
||||
defaultValue={appriseServicesList}
|
||||
{...register('appriseURLs', {
|
||||
pattern: {
|
||||
value: /^.+:\/\/.+$/gm,
|
||||
message: 'Invalid URLs format.',
|
||||
},
|
||||
})}
|
||||
/>
|
||||
{errors.appriseURLs && (
|
||||
<small className={classes.errorMessage}>
|
||||
{errors.appriseURLs.message}
|
||||
</small>
|
||||
)}
|
||||
</form>
|
||||
<div
|
||||
style={{
|
||||
color: '#6c737f',
|
||||
fontSize: '0.875rem',
|
||||
marginBottom: '20px',
|
||||
}}
|
||||
>
|
||||
Use{' '}
|
||||
<a
|
||||
style={{
|
||||
color: '#6d4aff',
|
||||
textDecoration: 'none',
|
||||
}}
|
||||
href='https://github.com/caronc/apprise#supported-notifications'
|
||||
rel='noreferrer'
|
||||
>
|
||||
Apprise URLs
|
||||
</a>{' '}
|
||||
to send a notification to any service. Only one URL per line.
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
208
Containers/UserSettings/EmailAlertSettings/EmailAlertSettings.js
Normal file
208
Containers/UserSettings/EmailAlertSettings/EmailAlertSettings.js
Normal file
|
@ -0,0 +1,208 @@
|
|||
//Lib
|
||||
import { useEffect } from 'react';
|
||||
import { toast } from 'react-toastify';
|
||||
import 'react-toastify/dist/ReactToastify.css';
|
||||
import classes from '../UserSettings.module.css';
|
||||
import { useState } from 'react';
|
||||
import { SpinnerCircularFixed } from 'spinners-react';
|
||||
import { IconExternalLink } from '@tabler/icons-react';
|
||||
import Link from 'next/link';
|
||||
|
||||
//Components
|
||||
import Error from '../../../Components/UI/Error/Error';
|
||||
import Switch from '../../../Components/UI/Switch/Switch';
|
||||
|
||||
export default function EmailAlertSettings() {
|
||||
//Var
|
||||
const toastOptions = {
|
||||
position: 'top-right',
|
||||
autoClose: 5000,
|
||||
hideProgressBar: false,
|
||||
closeOnClick: true,
|
||||
pauseOnHover: true,
|
||||
draggable: true,
|
||||
progress: undefined,
|
||||
//Callback > re-enabled button after notification.
|
||||
onClose: () => setDisabled(false),
|
||||
};
|
||||
|
||||
////State
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [testIsLoading, setTestIsLoading] = useState(false);
|
||||
const [error, setError] = useState();
|
||||
const [disabled, setDisabled] = useState(false);
|
||||
const [checked, setChecked] = useState();
|
||||
const [info, setInfo] = useState(false);
|
||||
|
||||
////LifeCycle
|
||||
//Component did mount
|
||||
useEffect(() => {
|
||||
const dataFetch = async () => {
|
||||
try {
|
||||
const response = await fetch('/api/account/getEmailAlert', {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-type': 'application/json',
|
||||
},
|
||||
});
|
||||
setChecked((await response.json()).emailAlert);
|
||||
setIsLoading(false);
|
||||
} catch (error) {
|
||||
setError(
|
||||
'Fetching email alert setting failed. Contact your administrator.'
|
||||
);
|
||||
console.log('Fetching email alert setting failed.');
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
dataFetch();
|
||||
}, []);
|
||||
|
||||
////Functions
|
||||
//Switch to enable/disable Email notifications
|
||||
const onChangeSwitchHandler = async (data) => {
|
||||
//Remove old error
|
||||
setError();
|
||||
//Disabled button
|
||||
setDisabled(true);
|
||||
await fetch('/api/account/updateEmailAlert', {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(data),
|
||||
})
|
||||
.then((response) => {
|
||||
console.log(response);
|
||||
if (response.ok) {
|
||||
if (data.emailAlert) {
|
||||
setChecked(!checked);
|
||||
toast.success(
|
||||
'Email notification enabled !',
|
||||
toastOptions
|
||||
);
|
||||
} else {
|
||||
setChecked(!checked);
|
||||
toast.success(
|
||||
'Email notification disabled !',
|
||||
toastOptions
|
||||
);
|
||||
}
|
||||
} else {
|
||||
setError('Update email alert setting failed.');
|
||||
setTimeout(() => {
|
||||
setError();
|
||||
setDisabled(false);
|
||||
}, 4000);
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.log(error);
|
||||
setError('Update failed. Contact your administrator.');
|
||||
setTimeout(() => {
|
||||
setError();
|
||||
setDisabled(false);
|
||||
}, 4000);
|
||||
});
|
||||
};
|
||||
|
||||
//Send a test notification by email
|
||||
const onSendTestMailHandler = async () => {
|
||||
//Loading
|
||||
setTestIsLoading(true);
|
||||
//Remove old error
|
||||
setError();
|
||||
await fetch('/api/account/sendTestEmail', {
|
||||
method: 'POST',
|
||||
})
|
||||
.then((response) => {
|
||||
if (!response.ok) {
|
||||
setTestIsLoading(false);
|
||||
setError('Failed to send the notification.');
|
||||
setTimeout(() => {
|
||||
setError();
|
||||
}, 4000);
|
||||
} else {
|
||||
setTestIsLoading(false);
|
||||
setInfo(true);
|
||||
setTimeout(() => {
|
||||
setInfo(false);
|
||||
}, 4000);
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
setTestIsLoading(false);
|
||||
console.log(error);
|
||||
setError('Send email failed. Contact your administrator.');
|
||||
setTimeout(() => {
|
||||
setError();
|
||||
}, 4000);
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* EMAIL ALERT */}
|
||||
<div className={classes.containerSetting}>
|
||||
<div className={classes.settingCategory}>
|
||||
<h2 style={{ alignSelf: 'baseline' }}>Email alert</h2>
|
||||
<Link
|
||||
style={{ alignSelf: 'baseline', marginLeft: '5px' }}
|
||||
href='https://borgwarehouse.com/docs/user-manual/account/#alerting'
|
||||
rel='noreferrer'
|
||||
target='_blank'
|
||||
>
|
||||
<IconExternalLink size={16} color='#6c737f' />
|
||||
</Link>
|
||||
</div>
|
||||
<div className={classes.setting}>
|
||||
<div className={classes.bwFormWrapper}>
|
||||
{isLoading ? (
|
||||
<SpinnerCircularFixed
|
||||
size={30}
|
||||
thickness={150}
|
||||
speed={150}
|
||||
color='#704dff'
|
||||
secondaryColor='#c3b6fa'
|
||||
/>
|
||||
) : (
|
||||
<Switch
|
||||
checked={checked}
|
||||
disabled={disabled}
|
||||
switchName='Alert me by email'
|
||||
switchDescription='You will receive an alert every 24H if you have a down status.'
|
||||
onChange={(e) =>
|
||||
onChangeSwitchHandler({ emailAlert: e })
|
||||
}
|
||||
/>
|
||||
)}
|
||||
{testIsLoading ? (
|
||||
<SpinnerCircularFixed
|
||||
size={30}
|
||||
thickness={150}
|
||||
speed={150}
|
||||
color='#704dff'
|
||||
secondaryColor='#c3b6fa'
|
||||
/>
|
||||
) : (
|
||||
<button
|
||||
className='defaultButton'
|
||||
onClick={onSendTestMailHandler}
|
||||
>
|
||||
Send a test mail
|
||||
</button>
|
||||
)}
|
||||
{info && (
|
||||
<span
|
||||
style={{ marginLeft: '10px', color: '#119300' }}
|
||||
>
|
||||
Mail successfully sent.
|
||||
</span>
|
||||
)}
|
||||
{error && <Error message={error} />}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
138
Containers/UserSettings/EmailSettings/EmailSettings.js
Normal file
138
Containers/UserSettings/EmailSettings/EmailSettings.js
Normal file
|
@ -0,0 +1,138 @@
|
|||
//Lib
|
||||
import { toast } from 'react-toastify';
|
||||
import 'react-toastify/dist/ReactToastify.css';
|
||||
import classes from '../UserSettings.module.css';
|
||||
import { useState } from 'react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { SpinnerDotted } from 'spinners-react';
|
||||
|
||||
//Components
|
||||
import Error from '../../../Components/UI/Error/Error';
|
||||
import Info from '../../../Components/UI/Info/Info';
|
||||
|
||||
export default function EmailSettings(props) {
|
||||
//Var
|
||||
const toastOptions = {
|
||||
position: 'top-right',
|
||||
autoClose: 8000,
|
||||
hideProgressBar: false,
|
||||
closeOnClick: true,
|
||||
pauseOnHover: true,
|
||||
draggable: true,
|
||||
progress: undefined,
|
||||
};
|
||||
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
reset,
|
||||
formState: { errors, isSubmitting, isValid },
|
||||
} = useForm({ mode: 'onChange' });
|
||||
|
||||
////State
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [error, setError] = useState();
|
||||
const [info, setInfo] = useState(false);
|
||||
|
||||
////Functions
|
||||
//Form submit Handler for ADD a repo
|
||||
const formSubmitHandler = async (data) => {
|
||||
//Remove old error
|
||||
setError();
|
||||
//Loading button on submit to avoid multiple send.
|
||||
setIsLoading(true);
|
||||
//POST API to send the new mail address
|
||||
try {
|
||||
const response = await fetch('/api/account/updateEmail', {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
const result = await response.json();
|
||||
|
||||
if (!response.ok) {
|
||||
setIsLoading(false);
|
||||
reset();
|
||||
setError(result.message);
|
||||
setTimeout(() => setError(), 4000);
|
||||
} else {
|
||||
reset();
|
||||
setIsLoading(false);
|
||||
setInfo(true);
|
||||
toast.success('Email edited !', toastOptions);
|
||||
}
|
||||
} catch (error) {
|
||||
reset();
|
||||
setIsLoading(false);
|
||||
setError("Can't update your email. Contact your administrator.");
|
||||
setTimeout(() => setError(), 4000);
|
||||
}
|
||||
};
|
||||
return (
|
||||
<>
|
||||
{/* EMAIL */}
|
||||
<div className={classes.containerSetting}>
|
||||
<div className={classes.settingCategory}>
|
||||
<h2>Email</h2>
|
||||
</div>
|
||||
<div className={classes.setting}>
|
||||
<div className={classes.bwFormWrapper}>
|
||||
{info ? ( //For local JWTs (cookie) without an OAuth provider, Next-Auth does not allow
|
||||
//at the time this code is written to refresh client-side session information
|
||||
//without triggering a logout.
|
||||
//I chose to inform the user to reconnect rather than force logout.
|
||||
<Info message='Please, logout to update your session.' />
|
||||
) : (
|
||||
<form
|
||||
onSubmit={handleSubmit(formSubmitHandler)}
|
||||
className={
|
||||
classes.bwForm +
|
||||
' ' +
|
||||
classes.currentSetting
|
||||
}
|
||||
>
|
||||
<p>
|
||||
{error && <Error message={error} />}
|
||||
<input
|
||||
type='email'
|
||||
placeholder={props.email}
|
||||
{...register('email', {
|
||||
required: true,
|
||||
pattern: {
|
||||
value: /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/,
|
||||
message:
|
||||
'Your email is not valid.',
|
||||
},
|
||||
})}
|
||||
/>
|
||||
{errors.email && (
|
||||
<small className={classes.errorMessage}>
|
||||
{errors.email.message}
|
||||
</small>
|
||||
)}
|
||||
</p>
|
||||
<button
|
||||
className={classes.AccountSettingsButton}
|
||||
disabled={!isValid || isSubmitting}
|
||||
>
|
||||
{isLoading ? (
|
||||
<SpinnerDotted
|
||||
size={20}
|
||||
thickness={150}
|
||||
speed={100}
|
||||
color='#fff'
|
||||
/>
|
||||
) : (
|
||||
'Update your email'
|
||||
)}
|
||||
</button>
|
||||
</form>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
136
Containers/UserSettings/PasswordSettings/PasswordSettings.js
Normal file
136
Containers/UserSettings/PasswordSettings/PasswordSettings.js
Normal file
|
@ -0,0 +1,136 @@
|
|||
//Lib
|
||||
import { toast } from 'react-toastify';
|
||||
import 'react-toastify/dist/ReactToastify.css';
|
||||
import classes from '../UserSettings.module.css';
|
||||
import { useState } from 'react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { SpinnerDotted } from 'spinners-react';
|
||||
|
||||
//Components
|
||||
import Error from '../../../Components/UI/Error/Error';
|
||||
|
||||
export default function PasswordSettings(props) {
|
||||
//Var
|
||||
const toastOptions = {
|
||||
position: 'top-right',
|
||||
autoClose: 5000,
|
||||
hideProgressBar: false,
|
||||
closeOnClick: true,
|
||||
pauseOnHover: true,
|
||||
draggable: true,
|
||||
progress: undefined,
|
||||
};
|
||||
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
reset,
|
||||
formState: { errors, isSubmitting, isValid },
|
||||
} = useForm({ mode: 'onChange' });
|
||||
|
||||
////State
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [error, setError] = useState();
|
||||
|
||||
////Functions
|
||||
//Form submit Handler for ADD a repo
|
||||
const formSubmitHandler = async (data) => {
|
||||
console.log(data);
|
||||
|
||||
//Remove old error
|
||||
setError();
|
||||
//Loading button on submit to avoid multiple send.
|
||||
setIsLoading(true);
|
||||
//POST API to send the new and old password
|
||||
try {
|
||||
const response = await fetch('/api/account/updatePassword', {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
const result = await response.json();
|
||||
|
||||
if (!response.ok) {
|
||||
setIsLoading(false);
|
||||
reset();
|
||||
setError(result.message);
|
||||
setTimeout(() => setError(), 4000);
|
||||
} else {
|
||||
reset();
|
||||
setIsLoading(false);
|
||||
toast.success('🔑 Password edited !', toastOptions);
|
||||
}
|
||||
} catch (error) {
|
||||
reset();
|
||||
setIsLoading(false);
|
||||
setError("Can't update your password. Contact your administrator.");
|
||||
setTimeout(() => setError(), 4000);
|
||||
}
|
||||
};
|
||||
return (
|
||||
<>
|
||||
{/* PASSWORD */}
|
||||
<div className={classes.containerSetting}>
|
||||
<div className={classes.settingCategory}>
|
||||
<h2>Password</h2>
|
||||
</div>
|
||||
<div className={classes.setting}>
|
||||
<div className={classes.bwFormWrapper}>
|
||||
<form
|
||||
onSubmit={handleSubmit(formSubmitHandler)}
|
||||
className={classes.bwForm}
|
||||
>
|
||||
{error && <Error message={error} />}
|
||||
<p>
|
||||
<input
|
||||
type='password'
|
||||
placeholder='Current password'
|
||||
{...register('oldPassword', {
|
||||
required: true,
|
||||
})}
|
||||
/>
|
||||
{errors.oldPassword &&
|
||||
errors.oldPassword.type === 'required' && (
|
||||
<small className={classes.errorMessage}>
|
||||
This field is required.
|
||||
</small>
|
||||
)}
|
||||
</p>
|
||||
<p>
|
||||
<input
|
||||
type='password'
|
||||
placeholder='New password'
|
||||
{...register('newPassword', {
|
||||
required: true,
|
||||
})}
|
||||
/>
|
||||
{errors.newPassword && (
|
||||
<small className={classes.errorMessage}>
|
||||
This field is required.
|
||||
</small>
|
||||
)}
|
||||
</p>
|
||||
<button
|
||||
className={classes.AccountSettingsButton}
|
||||
disabled={!isValid || isSubmitting}
|
||||
>
|
||||
{isLoading ? (
|
||||
<SpinnerDotted
|
||||
size={20}
|
||||
thickness={150}
|
||||
speed={100}
|
||||
color='#fff'
|
||||
/>
|
||||
) : (
|
||||
'Update your password'
|
||||
)}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
67
Containers/UserSettings/UserSettings.js
Normal file
67
Containers/UserSettings/UserSettings.js
Normal file
|
@ -0,0 +1,67 @@
|
|||
//Lib
|
||||
import 'react-toastify/dist/ReactToastify.css';
|
||||
import classes from './UserSettings.module.css';
|
||||
import { useState } from 'react';
|
||||
|
||||
//Components
|
||||
import EmailSettings from './EmailSettings/EmailSettings';
|
||||
import PasswordSettings from './PasswordSettings/PasswordSettings';
|
||||
import UsernameSettings from './UsernameSettings/UsernameSettings';
|
||||
import EmailAlertSettings from './EmailAlertSettings/EmailAlertSettings';
|
||||
import AppriseAlertSettings from './AppriseAlertSettings/AppriseAlertSettings';
|
||||
|
||||
export default function UserSettings(props) {
|
||||
//States
|
||||
const [tab, setTab] = useState('General');
|
||||
|
||||
return (
|
||||
<div className={classes.containerSettings}>
|
||||
<div>
|
||||
<h1
|
||||
style={{
|
||||
color: '#494b7a',
|
||||
textAlign: 'left',
|
||||
marginLeft: '30px',
|
||||
}}
|
||||
>
|
||||
Account{' '}
|
||||
</h1>
|
||||
</div>
|
||||
<div className={classes.tabList}>
|
||||
<button
|
||||
className={
|
||||
tab == 'General'
|
||||
? classes.tabListButtonActive
|
||||
: classes.tabListButton
|
||||
}
|
||||
onClick={() => setTab('General')}
|
||||
>
|
||||
General
|
||||
</button>
|
||||
<button
|
||||
className={
|
||||
tab == 'Notifications'
|
||||
? classes.tabListButtonActive
|
||||
: classes.tabListButton
|
||||
}
|
||||
onClick={() => setTab('Notifications')}
|
||||
>
|
||||
Notifications
|
||||
</button>
|
||||
</div>
|
||||
{tab == 'General' && (
|
||||
<>
|
||||
<PasswordSettings username={props.data.user.name} />
|
||||
<EmailSettings email={props.data.user.email} />
|
||||
<UsernameSettings username={props.data.user.name} />{' '}
|
||||
</>
|
||||
)}
|
||||
{tab == 'Notifications' && (
|
||||
<>
|
||||
<EmailAlertSettings />
|
||||
<AppriseAlertSettings />
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
251
Containers/UserSettings/UserSettings.module.css
Normal file
251
Containers/UserSettings/UserSettings.module.css
Normal file
|
@ -0,0 +1,251 @@
|
|||
.containerSettings {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
max-width: 1000px;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.containerSetting {
|
||||
display: flex;
|
||||
flex-flow: row wrap;
|
||||
width: 100%;
|
||||
margin: 40px 20px 0px 5px;
|
||||
text-align: left;
|
||||
padding: 28px 24px;
|
||||
animation: entrance ease-in 0.3s 1 normal none;
|
||||
border-bottom: 1px solid #e5e7eb;
|
||||
}
|
||||
|
||||
@keyframes entrance {
|
||||
0% {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
50% {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.settingCategory {
|
||||
max-width: 33.3333%;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
}
|
||||
.settingCategory h2 {
|
||||
color: #494b7a;
|
||||
margin: 0;
|
||||
font-size: 1.3em;
|
||||
}
|
||||
|
||||
.setting {
|
||||
max-width: 66.6666%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* Forms */
|
||||
|
||||
.bwForm {
|
||||
width: 80%;
|
||||
border-radius: 5px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.bwFormWrapper {
|
||||
text-align: left;
|
||||
margin: auto;
|
||||
width: 100%;
|
||||
height: auto;
|
||||
color: #494b7a;
|
||||
font-family: var(
|
||||
--pure-material-font,
|
||||
'Roboto',
|
||||
'Segoe UI',
|
||||
BlinkMacSystemFont,
|
||||
system-ui,
|
||||
-apple-system
|
||||
);
|
||||
}
|
||||
|
||||
.bwFormWrapper p {
|
||||
margin-block-start: 0em;
|
||||
}
|
||||
|
||||
.bwForm label {
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
text-align: center;
|
||||
/* margin-top: 20px; */
|
||||
color: #494b7a;
|
||||
}
|
||||
|
||||
.bwForm input,
|
||||
.bwForm textarea,
|
||||
.bwForm select {
|
||||
border: 1px solid #6d4aff21;
|
||||
font-size: 16px;
|
||||
height: auto;
|
||||
margin: 0;
|
||||
margin-bottom: 0px;
|
||||
outline: 0;
|
||||
padding: 10px;
|
||||
width: 100%;
|
||||
background-color: #f5f5f5;
|
||||
border-radius: 5px;
|
||||
/* color: #1b1340; */
|
||||
color: #494b7a;
|
||||
box-shadow: 0 1px 0 rgba(0, 0, 0, 0.03) inset;
|
||||
font-family: (
|
||||
--pure-material-font,
|
||||
'Roboto',
|
||||
'Segoe UI',
|
||||
BlinkMacSystemFont,
|
||||
system-ui
|
||||
);
|
||||
}
|
||||
|
||||
.bwForm textarea {
|
||||
resize: vertical;
|
||||
overflow: auto;
|
||||
white-space: pre;
|
||||
}
|
||||
|
||||
.bwForm textarea:focus,
|
||||
.bwForm input:focus,
|
||||
.bwForm select:focus {
|
||||
outline: 1px solid #6d4aff;
|
||||
box-shadow: 0 0 10px 3px rgba(110, 74, 255, 0.605);
|
||||
}
|
||||
|
||||
.bwForm .invalid {
|
||||
background: #f3c7c7;
|
||||
border: 1px solid #e45454;
|
||||
outline: 1px solid #ff4a4a;
|
||||
}
|
||||
|
||||
.bwForm .invalid:focus {
|
||||
background: #f3c7c7;
|
||||
border: 1px solid #e45454;
|
||||
outline: 1px solid #ff4a4a;
|
||||
box-shadow: 0 0 10px 3px rgba(255, 74, 74, 0.605);
|
||||
}
|
||||
|
||||
.bwForm button {
|
||||
display: block;
|
||||
}
|
||||
.bwForm button:hover {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.errorMessage {
|
||||
color: red;
|
||||
display: block;
|
||||
margin-top: 3px;
|
||||
}
|
||||
|
||||
.currentSetting input::placeholder {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.headerFormAppriseUrls {
|
||||
font-weight: 500;
|
||||
color: #494b7a;
|
||||
margin: 40px 0px 10px 0px;
|
||||
display: flex;
|
||||
padding-right: 5px;
|
||||
}
|
||||
|
||||
.formIsSavedMessage {
|
||||
color: rgb(0, 164, 0);
|
||||
animation: entrance 0.5s cubic-bezier(0.25, 0.46, 0.45, 0.94) both;
|
||||
font-weight: 300;
|
||||
}
|
||||
|
||||
.tabList {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.tabListButton {
|
||||
color: #494b7a;
|
||||
padding: 12px 0px;
|
||||
min-height: 48px;
|
||||
overflow: hidden;
|
||||
text-align: center;
|
||||
flex-direction: column;
|
||||
font-size: 1em;
|
||||
font-weight: 500;
|
||||
line-height: 1.71;
|
||||
text-transform: none;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
vertical-align: middle;
|
||||
text-decoration: none;
|
||||
border: 0;
|
||||
background-color: transparent;
|
||||
margin-left: 30px;
|
||||
border-bottom: 2px solid transparent;
|
||||
}
|
||||
|
||||
.tabListButton:hover {
|
||||
color: #6d4aff;
|
||||
border-bottom: 2px solid #6d4aff;
|
||||
}
|
||||
|
||||
.tabListButtonActive {
|
||||
color: #6d4aff;
|
||||
border: 0;
|
||||
border-bottom: 2px solid #6d4aff;
|
||||
padding: 12px 0px;
|
||||
min-height: 48px;
|
||||
overflow: hidden;
|
||||
text-align: center;
|
||||
flex-direction: column;
|
||||
font-size: 1em;
|
||||
font-weight: 500;
|
||||
line-height: 1.71;
|
||||
text-transform: none;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
vertical-align: middle;
|
||||
text-decoration: none;
|
||||
background-color: transparent;
|
||||
margin-left: 30px;
|
||||
}
|
||||
|
||||
.AccountSettingsButton {
|
||||
border: 0;
|
||||
padding: 10px 15px;
|
||||
background-color: #6d4aff;
|
||||
color: white;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
font-size: 1em;
|
||||
}
|
||||
|
||||
.AccountSettingsButton:hover {
|
||||
border: 0;
|
||||
padding: 10px 15px;
|
||||
background-color: #4f31ce;
|
||||
color: white;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
font-size: 1em;
|
||||
}
|
||||
|
||||
.AccountSettingsButton:active {
|
||||
border: 0;
|
||||
padding: 10px 15px;
|
||||
background-color: #4f31ce;
|
||||
color: white;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
font-size: 1em;
|
||||
transform: scale(0.95);
|
||||
}
|
147
Containers/UserSettings/UsernameSettings/UsernameSettings.js
Normal file
147
Containers/UserSettings/UsernameSettings/UsernameSettings.js
Normal file
|
@ -0,0 +1,147 @@
|
|||
//Lib
|
||||
import { toast } from 'react-toastify';
|
||||
import 'react-toastify/dist/ReactToastify.css';
|
||||
import classes from '../UserSettings.module.css';
|
||||
import { useState } from 'react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { SpinnerDotted } from 'spinners-react';
|
||||
|
||||
//Components
|
||||
import Error from '../../../Components/UI/Error/Error';
|
||||
import Info from '../../../Components/UI/Info/Info';
|
||||
|
||||
export default function UsernameSettings(props) {
|
||||
//Var
|
||||
const toastOptions = {
|
||||
position: 'top-right',
|
||||
autoClose: 8000,
|
||||
hideProgressBar: false,
|
||||
closeOnClick: true,
|
||||
pauseOnHover: true,
|
||||
draggable: true,
|
||||
progress: undefined,
|
||||
};
|
||||
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
reset,
|
||||
formState: { errors, isSubmitting, isValid },
|
||||
} = useForm({ mode: 'onChange' });
|
||||
|
||||
////State
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [error, setError] = useState();
|
||||
const [info, setInfo] = useState(false);
|
||||
|
||||
////Functions
|
||||
//Form submit Handler for ADD a repo
|
||||
const formSubmitHandler = async (data) => {
|
||||
//Remove old error
|
||||
setError();
|
||||
//Loading button on submit to avoid multiple send.
|
||||
setIsLoading(true);
|
||||
//POST API to update the username
|
||||
try {
|
||||
const response = await fetch('/api/account/updateUsername', {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
const result = await response.json();
|
||||
|
||||
if (!response.ok) {
|
||||
setIsLoading(false);
|
||||
reset();
|
||||
setError(result.message);
|
||||
setTimeout(() => setError(), 4000);
|
||||
} else {
|
||||
reset();
|
||||
setIsLoading(false);
|
||||
setInfo(true);
|
||||
toast.success('Username edited !', toastOptions);
|
||||
}
|
||||
} catch (error) {
|
||||
reset();
|
||||
setIsLoading(false);
|
||||
setError("Can't update your username. Contact your administrator.");
|
||||
setTimeout(() => setError(), 4000);
|
||||
}
|
||||
};
|
||||
return (
|
||||
<>
|
||||
{/* Username */}
|
||||
<div className={classes.containerSetting}>
|
||||
<div className={classes.settingCategory}>
|
||||
<h2>Username</h2>
|
||||
</div>
|
||||
<div className={classes.setting}>
|
||||
<div className={classes.bwFormWrapper}>
|
||||
{info ? (
|
||||
//For local JWTs (cookie) without an OAuth provider, Next-Auth does not allow
|
||||
//at the time this code is written to refresh client-side session information
|
||||
//without triggering a logout.
|
||||
//I chose to inform the user to reconnect rather than force logout.
|
||||
<Info message='Please, logout to update your session.' />
|
||||
) : (
|
||||
<form
|
||||
onSubmit={handleSubmit(formSubmitHandler)}
|
||||
className={
|
||||
classes.bwForm +
|
||||
' ' +
|
||||
classes.currentSetting
|
||||
}
|
||||
>
|
||||
<p>
|
||||
{error && <Error message={error} />}
|
||||
<input
|
||||
type='text'
|
||||
placeholder={props.username}
|
||||
{...register('username', {
|
||||
required: 'A username is required.',
|
||||
pattern: {
|
||||
value: /^[a-z]{5,15}$/,
|
||||
message:
|
||||
'Only a-z characters are allowed.',
|
||||
},
|
||||
maxLength: {
|
||||
value: 10,
|
||||
message: '15 characters max.',
|
||||
},
|
||||
minLength: {
|
||||
value: 5,
|
||||
message: '5 characters min.',
|
||||
},
|
||||
})}
|
||||
/>
|
||||
{errors.username && (
|
||||
<small className={classes.errorMessage}>
|
||||
{errors.username.message}
|
||||
</small>
|
||||
)}
|
||||
</p>
|
||||
<button
|
||||
className={classes.AccountSettingsButton}
|
||||
disabled={!isValid || isSubmitting}
|
||||
>
|
||||
{isLoading ? (
|
||||
<SpinnerDotted
|
||||
size={20}
|
||||
thickness={150}
|
||||
speed={100}
|
||||
color='#fff'
|
||||
/>
|
||||
) : (
|
||||
'Update your username'
|
||||
)}
|
||||
</button>
|
||||
</form>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
59
Dockerfile
Normal file
59
Dockerfile
Normal file
|
@ -0,0 +1,59 @@
|
|||
ARG UID=1001
|
||||
ARG GID=1001
|
||||
|
||||
FROM node:20-bookworm-slim as base
|
||||
|
||||
# build stage
|
||||
FROM base AS deps
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY package.json package-lock.json ./
|
||||
|
||||
RUN npm ci --only=production
|
||||
|
||||
FROM base AS builder
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY --from=deps /app/node_modules ./node_modules
|
||||
|
||||
COPY . .
|
||||
|
||||
RUN sed -i "s/images:/output: 'standalone',images:/" next.config.js
|
||||
|
||||
RUN npm run build
|
||||
|
||||
# run stage
|
||||
FROM base AS runner
|
||||
|
||||
ARG UID
|
||||
ARG GID
|
||||
|
||||
ENV NODE_ENV production
|
||||
|
||||
RUN echo 'deb http://deb.debian.org/debian bookworm-backports main' >> /etc/apt/sources.list
|
||||
RUN apt-get update && apt-get install -y \
|
||||
supervisor curl jq jc borgbackup/bookworm-backports openssh-server rsyslog && \
|
||||
apt-get clean && rm -rf /var/lib/apt/lists/*
|
||||
|
||||
RUN groupadd -g ${GID} borgwarehouse && useradd -m -u ${UID} -g ${GID} borgwarehouse
|
||||
|
||||
RUN cp /etc/ssh/moduli /home/borgwarehouse/
|
||||
|
||||
WORKDIR /home/borgwarehouse/app
|
||||
|
||||
COPY --from=builder --chown=borgwarehouse:borgwarehouse /app/docker/docker-bw-init.sh /app/LICENSE ./
|
||||
COPY --from=builder --chown=borgwarehouse:borgwarehouse /app/helpers/shells ./helpers/shells
|
||||
COPY --from=builder --chown=borgwarehouse:borgwarehouse /app/.next/standalone ./
|
||||
COPY --from=builder --chown=borgwarehouse:borgwarehouse /app/public ./public
|
||||
COPY --from=builder --chown=borgwarehouse:borgwarehouse /app/.next/static ./.next/static
|
||||
COPY --from=builder --chown=borgwarehouse:borgwarehouse /app/docker/supervisord.conf ./
|
||||
COPY --from=builder --chown=borgwarehouse:borgwarehouse /app/docker/rsyslog.conf /etc/rsyslog.conf
|
||||
COPY --from=builder --chown=borgwarehouse:borgwarehouse /app/docker/sshd_config ./
|
||||
|
||||
USER borgwarehouse
|
||||
|
||||
EXPOSE 3000 22
|
||||
|
||||
ENTRYPOINT ["./docker-bw-init.sh"]
|
64
README.md
64
README.md
|
@ -1,7 +1,12 @@
|
|||
<div align="center">
|
||||
|
||||
[![Next][Next.js]][Next-url]
|
||||
[![React][React.js]][React-url]
|
||||
[![Next][Next.js]][Next-url]
|
||||
[![React][React.js]][React-url]
|
||||
|
||||
</div>
|
||||
<div align="center">
|
||||
|
||||
[![Docker](https://img.shields.io/badge/Docker-borgwarehouse-blue?style=for-the-badge&logo=docker)](https://hub.docker.com/r/borgwarehouse/borgwarehouse)
|
||||
|
||||
</div>
|
||||
|
||||
|
@ -19,7 +24,16 @@
|
|||
</a>
|
||||
</div>
|
||||
|
||||
## What is BorgWarehouse ?
|
||||
## ⭐ Support the Project
|
||||
<div align="center">
|
||||
<a href="https://github.com/sponsors/Ravinou"><img alt="GitHub Sponsors" src="https://img.shields.io/github/sponsors/Ravinou?style=for-the-badge&logo=github&label=Github%20Sponsors&link=https%3A%2F%2Fgithub.com%2Fsponsors%2FRavinou"></a>
|
||||
<a href="https://liberapay.com/R4VEN/"><img alt="Liberapay patrons" src="https://img.shields.io/liberapay/patrons/R4VEN?style=for-the-badge&logo=liberapay&label=Liberapay%20Sponsors&link=https%3A%2F%2Fliberapay.com%2FR4VEN"></a>
|
||||
</div>
|
||||
|
||||
|
||||
If you find BorgWarehouse helpful or interesting, please consider **giving it a star on GitHub** and **[sponsoring](https://github.com/sponsors/Ravinou)**. Your support is greatly appreciated!
|
||||
|
||||
## ✨ What is BorgWarehouse ?
|
||||
|
||||
**BorgWarehouse is a graphical interface to manage a central [BorgBackup](https://borgbackup.readthedocs.io/en/stable/#what-is-borgbackup) repository server.**
|
||||
|
||||
|
@ -37,43 +51,33 @@ With BorgWarehouse, you have an interface that allows you to do all this simply
|
|||
|
||||
The whole system part is automatically managed by BorgWarehouse and **you don't have to touch your terminal anymore** while enjoying a visual feedback on the status of your repositories.
|
||||
|
||||
## Get started
|
||||
## 📖 Get started
|
||||
|
||||
You can find the documentation here : <a href="https://borgwarehouse.com/docs/prologue/introduction/">https://borgwarehouse.com/</a>
|
||||
You can find the documentation here : [borgwarehouse.com](https://borgwarehouse.com/docs/prologue/introduction/)
|
||||
|
||||
## :key: Environment Variables
|
||||
## 🔑 Environment Variables
|
||||
|
||||
To run this project, you will need to add the following environment variables to your `.env.local` file.
|
||||
To run this project, you will need to add some environment variables.
|
||||
|
||||
Variables to create (all required) :
|
||||
You will find a complete documentation for this [here](https://borgwarehouse.com/docs/admin-manual/env-vars/).
|
||||
|
||||
- `NEXTAUTH_URL` : The url of your application as **https://borgwarehouse.com**.
|
||||
- `NEXTAUTH_SECRET` : A secret random key.
|
||||
- `CRONJOB_KEY` : A secret API key for cronjob.
|
||||
- `NEXT_PUBLIC_HOSTNAME` : FQDN as **borgwarehouse.com**
|
||||
- `NEXT_PUBLIC_SSH_SERVER_PORT` : SSH port of your server as **22**.
|
||||
- `NEXT_PUBLIC_SSH_SERVER_FINGERPRINT_RSA` : Your server SSH fingerprint for RSA.
|
||||
- `NEXT_PUBLIC_SSH_SERVER_FINGERPRINT_ED25519` : Your server SSH fingerprint for ED25519.
|
||||
- `NEXT_PUBLIC_SSH_SERVER_FINGERPRINT_ECDSA` : Your server SSH fingerprint for ECDSA.
|
||||
## ⏬ How to update ?
|
||||
|
||||
Example for a valid `.env.local` file :
|
||||
|
||||
```bash
|
||||
NEXTAUTH_URL=https://yourbwdomain.com
|
||||
NEXTAUTH_SECRET=YOURFIRSTSECRET
|
||||
CRONJOB_KEY=YOURSECONDSECRET
|
||||
NEXT_PUBLIC_HOSTNAME=yourbwdomain.com
|
||||
NEXT_PUBLIC_SSH_SERVER_PORT=22
|
||||
NEXT_PUBLIC_SSH_SERVER_FINGERPRINT_RSA=SHA256:36mfYNRrm1aconVt6cBpi8LhAoPP4kB8QsVW4n8eGHQ
|
||||
NEXT_PUBLIC_SSH_SERVER_FINGERPRINT_ED25519=SHA256:tYQuzrZZMqaw0Bzvn/sMoDs1CVEitZ9IrRyUg02yTPA
|
||||
NEXT_PUBLIC_SSH_SERVER_FINGERPRINT_ECDSA=SHA256:nTpxui1oEmH9konPau17qBVIzBQVOsD1BIbBFU5IL04
|
||||
```
|
||||
|
||||
You can find more details about generating your secrets or retrieving your SSH fingerprint. You can find more details about generating your secrets or retrieving your SSH fingerprint <a href="https://borgwarehouse.com/docs/admin-manual/debian-installation/#configure-application-environment-variables">in the documentation</a>.
|
||||
Check the online documentation [just here](https://borgwarehouse.com/docs/admin-manual/how-to-update/) !
|
||||
|
||||
<!-- MARKDOWN LINKS & IMAGES -->
|
||||
<!-- https://www.markdownguide.org/basic-syntax/#reference-style-links -->
|
||||
|
||||
## ❤️ Special thanks to sponsors ❤️
|
||||
|
||||
### 🥇 Current sponsors 🥇
|
||||
<a href="https://github.com/royalmoose"><img src="https://avatars.githubusercontent.com/royalmoose" style="width:50px; border-radius:50%;"/></a>
|
||||
<a href="https://github.com/Magneticdud"><img src="https://avatars.githubusercontent.com/Magneticdud" style="width:50px; border-radius:50%;"/></a>
|
||||
<a href="https://github.com/dhenry123"><img src="https://avatars.githubusercontent.com/dhenry123" style="width:50px; border-radius:50%;"/></a>
|
||||
|
||||
#### Past sponsors
|
||||
<a href="https://github.com/shad-lp"><img src="https://avatars.githubusercontent.com/shad-lp" style="width:25px; border-radius:50%;"/></a>
|
||||
|
||||
[next.js]: https://img.shields.io/badge/next.js-000000?style=for-the-badge&logo=nextdotjs&logoColor=white
|
||||
[next-url]: https://nextjs.org/
|
||||
[react.js]: https://img.shields.io/badge/React-20232A?style=for-the-badge&logo=react&logoColor=61DAFB
|
||||
|
|
30
docker-compose.yml
Normal file
30
docker-compose.yml
Normal file
|
@ -0,0 +1,30 @@
|
|||
version: '3'
|
||||
services:
|
||||
borgwarehouse:
|
||||
container_name: borgwarehouse
|
||||
# If you want to build the image yourself, uncomment the following lines and comment the image line
|
||||
#build:
|
||||
# context: .
|
||||
# dockerfile: Dockerfile
|
||||
# args:
|
||||
# - UID=${UID}
|
||||
# - GID=${GID}
|
||||
image: borgwarehouse/borgwarehouse
|
||||
user: '${UID:?UID variable missing}:${GID:?GID variable missing}'
|
||||
ports:
|
||||
- '${WEB_SERVER_PORT:?WEB_SERVER_PORT variable missing}:3000'
|
||||
- '${SSH_SERVER_PORT:?SSH_SERVER_PORT variable missing}:22'
|
||||
env_file:
|
||||
- .env
|
||||
volumes:
|
||||
- ${CONFIG_PATH:?CONFIG_PATH variable missing}:/home/borgwarehouse/app/config
|
||||
- ${SSH_PATH:?SSH_PATH variable missing}:/home/borgwarehouse/.ssh
|
||||
- ${SSH_HOST:?SSH_HOST variable missing}:/etc/ssh
|
||||
- ${BORG_REPOSITORY_PATH:?BORG_REPOSITORY_PATH variable missing}:/home/borgwarehouse/repos
|
||||
- ${TMP_PATH:?TMP_PATH variable missing}:/home/borgwarehouse/tmp
|
||||
- ${LOGS_PATH:?LOGS_PATH variable missing}:/home/borgwarehouse/logs
|
||||
# Apprise is used to send notifications, it's optional. http://apprise:8000 is the URL to use in BorgWarehouse.
|
||||
apprise:
|
||||
container_name: apprise
|
||||
image: caronc/apprise
|
||||
user: 'www-data:www-data'
|
86
docker/docker-bw-init.sh
Executable file
86
docker/docker-bw-init.sh
Executable file
|
@ -0,0 +1,86 @@
|
|||
#!/bin/bash
|
||||
|
||||
set -e
|
||||
|
||||
SSH_DIR="/home/borgwarehouse/.ssh"
|
||||
AUTHORIZED_KEYS_FILE="$SSH_DIR/authorized_keys"
|
||||
REPOS_DIR="/home/borgwarehouse/repos"
|
||||
|
||||
print_green() {
|
||||
echo -e "\e[92m$1\e[0m";
|
||||
}
|
||||
print_red() {
|
||||
echo -e "\e[91m$1\e[0m";
|
||||
}
|
||||
|
||||
init_ssh_server() {
|
||||
if [ -z "$(ls -A /etc/ssh)" ]; then
|
||||
print_green "/etc/ssh is empty, generating SSH host keys..."
|
||||
ssh-keygen -A
|
||||
cp /home/borgwarehouse/moduli /etc/ssh/
|
||||
fi
|
||||
if [ ! -f "/etc/ssh/sshd_config" ]; then
|
||||
print_green "sshd_config not found in your volume, copying the default one..."
|
||||
cp /home/borgwarehouse/app/sshd_config /etc/ssh/
|
||||
fi
|
||||
}
|
||||
|
||||
check_ssh_directory() {
|
||||
if [ ! -d "$SSH_DIR" ]; then
|
||||
print_red "The .ssh directory does not exist, you need to mount it as docker volume."
|
||||
exit 1
|
||||
else
|
||||
chmod 700 "$SSH_DIR"
|
||||
fi
|
||||
}
|
||||
|
||||
create_authorized_keys_file() {
|
||||
if [ ! -f "$AUTHORIZED_KEYS_FILE" ]; then
|
||||
print_green "The authorized_keys file does not exist, creating..."
|
||||
touch "$AUTHORIZED_KEYS_FILE"
|
||||
fi
|
||||
chmod 600 "$AUTHORIZED_KEYS_FILE"
|
||||
}
|
||||
|
||||
check_repos_directory() {
|
||||
if [ ! -d "$REPOS_DIR" ]; then
|
||||
print_red "The repos directory does not exist, you need to mount it as docker volume."
|
||||
exit 2
|
||||
else
|
||||
chmod 700 "$REPOS_DIR"
|
||||
fi
|
||||
}
|
||||
|
||||
get_SSH_fingerprints() {
|
||||
print_green "Getting SSH fingerprints..."
|
||||
RSA_FINGERPRINT=$(ssh-keygen -lf /etc/ssh/ssh_host_rsa_key | awk '{print $2}')
|
||||
ED25519_FINGERPRINT=$(ssh-keygen -lf /etc/ssh/ssh_host_ed25519_key | awk '{print $2}')
|
||||
ECDSA_FINGERPRINT=$(ssh-keygen -lf /etc/ssh/ssh_host_ecdsa_key | awk '{print $2}')
|
||||
export SSH_SERVER_FINGERPRINT_RSA="$RSA_FINGERPRINT"
|
||||
export SSH_SERVER_FINGERPRINT_ED25519="$ED25519_FINGERPRINT"
|
||||
export SSH_SERVER_FINGERPRINT_ECDSA="$ECDSA_FINGERPRINT"
|
||||
}
|
||||
|
||||
check_env() {
|
||||
if [ -z "$CRONJOB_KEY" ]; then
|
||||
CRONJOB_KEY=$(openssl rand -base64 32)
|
||||
print_green "CRONJOB_KEY not found or empty. Generating a random key..."
|
||||
export CRONJOB_KEY
|
||||
fi
|
||||
|
||||
if [ -z "$NEXTAUTH_SECRET" ]; then
|
||||
NEXTAUTH_SECRET=$(openssl rand -base64 32)
|
||||
print_green "NEXTAUTH_SECRET not found or empty. Generating a random key..."
|
||||
export NEXTAUTH_SECRET
|
||||
fi
|
||||
}
|
||||
|
||||
check_env
|
||||
init_ssh_server
|
||||
check_ssh_directory
|
||||
create_authorized_keys_file
|
||||
check_repos_directory
|
||||
get_SSH_fingerprints
|
||||
|
||||
print_green "Successful initialization. BorgWarehouse is ready !"
|
||||
exec supervisord -c /home/borgwarehouse/app/supervisord.conf
|
40
docker/rsyslog.conf
Normal file
40
docker/rsyslog.conf
Normal file
|
@ -0,0 +1,40 @@
|
|||
# rsyslog configuration file
|
||||
|
||||
$WorkDirectory /home/borgwarehouse/tmp
|
||||
$FileOwner borgwarehouse
|
||||
$FileGroup borgwarehouse
|
||||
$FileCreateMode 0640
|
||||
$DirCreateMode 0755
|
||||
$Umask 0022
|
||||
|
||||
$RepeatedMsgReduction on
|
||||
|
||||
module(load="imfile" PollingInterval="10")
|
||||
|
||||
input(type="imfile"
|
||||
File="/home/borgwarehouse/tmp/borgwarehouse.log"
|
||||
Tag="BorgWarehouse"
|
||||
Severity="info"
|
||||
Facility="local7"
|
||||
ruleset="bwLogs")
|
||||
|
||||
input(type="imfile"
|
||||
File="/home/borgwarehouse/tmp/sshd.log"
|
||||
Tag="sshd"
|
||||
Severity="info"
|
||||
Facility="local7"
|
||||
ruleset="sshdLogs")
|
||||
|
||||
$template myFormat,"%timegenerated:::date-rfc3339% %syslogtag% %msg%\n"
|
||||
|
||||
ruleset(name="bwLogs") {
|
||||
action(type="omfile"
|
||||
File="/home/borgwarehouse/logs/borgwarehouse.log"
|
||||
Template="myFormat")
|
||||
}
|
||||
|
||||
ruleset(name="sshdLogs") {
|
||||
action(type="omfile"
|
||||
File="/home/borgwarehouse/logs/sshd.log"
|
||||
Template="myFormat")
|
||||
}
|
32
docker/sshd_config
Normal file
32
docker/sshd_config
Normal file
|
@ -0,0 +1,32 @@
|
|||
Port 22
|
||||
PidFile /home/borgwarehouse/tmp/sshd.pid
|
||||
AllowUsers borgwarehouse
|
||||
LogLevel INFO
|
||||
SyslogFacility AUTH
|
||||
|
||||
# Security
|
||||
Protocol 2
|
||||
PermitRootLogin no
|
||||
PasswordAuthentication no
|
||||
ChallengeResponseAuthentication no
|
||||
AuthenticationMethods publickey
|
||||
MaxAuthTries 2
|
||||
MaxStartups 2:30:10
|
||||
LoginGraceTime 30
|
||||
UsePAM no
|
||||
|
||||
# Useless options for BorgWarehouse
|
||||
PrintMotd no
|
||||
UseDNS no
|
||||
AllowTcpForwarding no
|
||||
X11Forwarding no
|
||||
PermitTTY no
|
||||
|
||||
# Ciphers
|
||||
Ciphers aes256-ctr,aes192-ctr,aes128-ctr
|
||||
MACs hmac-sha2-512-etm@openssh.com,hmac-sha2-256-etm@openssh.com
|
||||
|
||||
# With low bandwidth or huge backup, uncomment the following lines to avoid SSH timeout (Broken pipe).
|
||||
#ClientAliveInterval 600
|
||||
#ClientAliveCountMax 0
|
||||
|
24
docker/supervisord.conf
Normal file
24
docker/supervisord.conf
Normal file
|
@ -0,0 +1,24 @@
|
|||
[supervisord]
|
||||
nodaemon=true
|
||||
logfile=/home/borgwarehouse/logs/supervisord.log
|
||||
loglevel=error
|
||||
pidfile=/home/borgwarehouse/tmp/supervisord.pid
|
||||
logfile_maxbytes=10MB
|
||||
logfile_backups=5
|
||||
|
||||
[program:sshd]
|
||||
command=/usr/sbin/sshd -D -e -f /etc/ssh/sshd_config
|
||||
stdout_logfile=/home/borgwarehouse/tmp/sshd.log
|
||||
stdout_logfile_maxbytes=10MB
|
||||
stdout_logfile_backups=5
|
||||
redirect_stderr=true
|
||||
|
||||
[program:borgwarehouse]
|
||||
command=/usr/local/bin/node server.js
|
||||
stdout_logfile=/home/borgwarehouse/tmp/borgwarehouse.log
|
||||
stdout_logfile_maxbytes=10MB
|
||||
stdout_logfile_backups=5
|
||||
redirect_stderr=true
|
||||
|
||||
[program:rsyslogd]
|
||||
command=rsyslogd -n -i /home/borgwarehouse/tmp/rsyslogd.pid -f /etc/rsyslog.conf
|
13
helpers/functions/lanCommandOption.js
Normal file
13
helpers/functions/lanCommandOption.js
Normal file
|
@ -0,0 +1,13 @@
|
|||
export default function lanCommandOption(wizardEnv, lanCommand) {
|
||||
let FQDN;
|
||||
let SSH_SERVER_PORT;
|
||||
if (lanCommand && wizardEnv.FQDN_LAN && wizardEnv.SSH_SERVER_PORT_LAN) {
|
||||
FQDN = wizardEnv.FQDN_LAN;
|
||||
SSH_SERVER_PORT = wizardEnv.SSH_SERVER_PORT_LAN;
|
||||
} else {
|
||||
FQDN = wizardEnv.FQDN;
|
||||
SSH_SERVER_PORT = wizardEnv.SSH_SERVER_PORT;
|
||||
}
|
||||
|
||||
return { FQDN, SSH_SERVER_PORT };
|
||||
}
|
18
helpers/functions/nodemailerSMTP.js
Normal file
18
helpers/functions/nodemailerSMTP.js
Normal file
|
@ -0,0 +1,18 @@
|
|||
//Lib
|
||||
import nodemailer from 'nodemailer';
|
||||
|
||||
export default function nodemailerSMTP() {
|
||||
const transporter = nodemailer.createTransport({
|
||||
port: process.env.MAIL_SMTP_PORT,
|
||||
host: process.env.MAIL_SMTP_HOST,
|
||||
auth: {
|
||||
user: process.env.MAIL_SMTP_LOGIN,
|
||||
pass: process.env.MAIL_SMTP_PWD,
|
||||
},
|
||||
tls: {
|
||||
// do not fail on invalid certs >> allow self-signed or invalid TLS certificate
|
||||
rejectUnauthorized: process.env.MAIL_REJECT_SELFSIGNED_TLS,
|
||||
},
|
||||
});
|
||||
return transporter;
|
||||
}
|
45
helpers/functions/repoHistory.js
Normal file
45
helpers/functions/repoHistory.js
Normal file
|
@ -0,0 +1,45 @@
|
|||
import { promises as fs } from 'fs';
|
||||
import path from 'path';
|
||||
|
||||
export default async function repoHistory(data) {
|
||||
try {
|
||||
const repoHistoryDir = path.join(process.cwd(), '/config/versions');
|
||||
const maxBackupCount = parseInt(process.env.MAX_REPO_BACKUP_COUNT) || 8;
|
||||
const timestamp = new Date().toISOString();
|
||||
const backupDate = timestamp.split('T')[0];
|
||||
|
||||
//Create the directory if it does not exist
|
||||
await fs.mkdir(repoHistoryDir, { recursive: true });
|
||||
|
||||
const existingBackups = await fs.readdir(repoHistoryDir);
|
||||
|
||||
if (existingBackups.length >= maxBackupCount) {
|
||||
existingBackups.sort();
|
||||
const backupsToDelete = existingBackups.slice(
|
||||
0,
|
||||
existingBackups.length - maxBackupCount + 1
|
||||
);
|
||||
for (const backupToDelete of backupsToDelete) {
|
||||
const backupFilePathToDelete = path.join(
|
||||
repoHistoryDir,
|
||||
backupToDelete
|
||||
);
|
||||
await fs.unlink(backupFilePathToDelete);
|
||||
}
|
||||
}
|
||||
|
||||
const backupFileName = `${backupDate}.log`;
|
||||
const backupFilePath = path.join(repoHistoryDir, backupFileName);
|
||||
const jsonData = JSON.stringify(data, null, 2);
|
||||
|
||||
const logData = `\n>>>> History of file repo.json at "${timestamp}" <<<<\n${jsonData}\n`;
|
||||
|
||||
// Écrire ou réécrire le fichier avec le contenu mis à jour
|
||||
await fs.appendFile(backupFilePath, logData);
|
||||
} catch (error) {
|
||||
console.error(
|
||||
'An error occurred while saving the repo history :',
|
||||
error.message
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,88 +1,84 @@
|
|||
#!/bin/bash
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# Shell created by Raven for BorgWarehouse.
|
||||
# This shell takes 3 arguments : [reponame] X [SSH pub key] X [quota]
|
||||
# This shell takes 2 arguments : [SSH pub key] X [quota] x [append only mode (boolean)]
|
||||
# Main steps are :
|
||||
# - check if args are present
|
||||
# - check the ssh pub key format
|
||||
# - check if the ssh pub key is already present in authorized_keys
|
||||
# - check if borgbackup package is install
|
||||
# - generate a random username, check if it exists in /etc/passwd
|
||||
# - add the user (with random name), group, shell and home
|
||||
# - create a pool which is the folder where all the repositories for a user are located (only one by user for borgwarehouse usage)
|
||||
# - create the authorized_keys
|
||||
# - generate a random repositoryName
|
||||
# - add the SSH public key in the authorized_keys with borg restriction for repository and storage quota.
|
||||
# This simple method prevents the user from connecting to the server with a shell in SSH.
|
||||
# He can only use the borg command. Moreover, he will not be able to leave his repository or create a new one.
|
||||
# It is similar to a jail and that is the goal.
|
||||
|
||||
# Limitation : all SSH pubkey are unique : https://github.com/borgbackup/borg/issues/7757
|
||||
|
||||
# Exit when any command fails
|
||||
set -e
|
||||
|
||||
# Load .env if exists
|
||||
if [[ -f .env ]]; then
|
||||
source .env
|
||||
fi
|
||||
|
||||
# Default value if .env not exists
|
||||
: "${home:=/home/borgwarehouse}"
|
||||
|
||||
# Some variables
|
||||
pool="${home}/repos"
|
||||
authorized_keys="${home}/.ssh/authorized_keys"
|
||||
|
||||
# Check args
|
||||
if [ "$1" == "" ] || [ "$2" == "" ] || [ "$3" == "" ];then
|
||||
echo "This shell takes 3 argument : Reponame, SSH Public Key, Quota in Go [e.g. : 10] "
|
||||
if [ "$1" == "" ] || [ "$2" == "" ] || [ "$3" != "true" ] && [ "$3" != "false" ];then
|
||||
echo -n "This shell takes 3 arguments : SSH Public Key, Quota in Go [e.g. : 10], Append only mode [true|false]"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check if the SSH public key is a valid format
|
||||
# This pattern validates SSH public keys for : rsa, ed25519, ed25519-sk
|
||||
pattern='(ssh-ed25519 AAAAC3NzaC1lZDI1NTE5|sk-ssh-ed25519@openssh.com AAAAGnNrLXNzaC1lZDI1NTE5QG9wZW5zc2guY29t|ssh-rsa AAAAB3NzaC1yc2)[0-9A-Za-z+/]+[=]{0,3}(\s.*)?'
|
||||
if [[ ! "$2" =~ $pattern ]]
|
||||
if [[ ! "$1" =~ $pattern ]]
|
||||
then
|
||||
echo "Invalid public SSH KEY format. Provide a key in OpenSSH format (rsa, ed25519, ed25519-sk)"
|
||||
echo -n "Invalid public SSH KEY format. Provide a key in OpenSSH format (rsa, ed25519, ed25519-sk)"
|
||||
exit 2
|
||||
fi
|
||||
|
||||
## Check if authorized_keys exists
|
||||
if [ ! -f "${authorized_keys}" ];then
|
||||
echo -n "${authorized_keys} must be present"
|
||||
exit 5
|
||||
fi
|
||||
|
||||
# Check if SSH pub key is already present in authorized_keys
|
||||
if grep -q "$1" "$authorized_keys"; then
|
||||
echo -n "SSH pub key already present in authorized_keys"
|
||||
exit 3
|
||||
fi
|
||||
|
||||
# Check if borgbackup is installed
|
||||
if ! [ -x "$(command -v borg)" ]; then
|
||||
echo "You must install borgbackup package."
|
||||
exit 3
|
||||
echo -n "You must install borgbackup package."
|
||||
exit 4
|
||||
fi
|
||||
|
||||
# Generation of a random for username
|
||||
randUsername () {
|
||||
# Generation of a random for repositoryName
|
||||
randRepositoryName () {
|
||||
openssl rand -hex 4
|
||||
}
|
||||
user=$(randUsername)
|
||||
repositoryName=$(randRepositoryName)
|
||||
|
||||
# Check if the random is already a username.
|
||||
while grep -q $user /etc/passwd
|
||||
do
|
||||
user=$(randUsername)
|
||||
done
|
||||
|
||||
# Some variables
|
||||
group="${user}"
|
||||
home="/var/borgwarehouse/${user}"
|
||||
pool="${home}/repos"
|
||||
authorized_keys="${home}/.ssh/authorized_keys"
|
||||
|
||||
## add user and create homedirectory ${user} - [shell=/bin/bash home=${home} group=${group}]
|
||||
sudo useradd -d ${home} -s "/bin/bash" -m --badname ${user}
|
||||
|
||||
## Create directory ${home}/.ssh
|
||||
sudo mkdir -p ${home}/.ssh
|
||||
|
||||
## Create autorized_keys file
|
||||
sudo touch ${home}/.ssh/authorized_keys
|
||||
|
||||
## Create the repo
|
||||
sudo mkdir -p "${pool}/$1"
|
||||
|
||||
## Change permissions
|
||||
sudo chmod -R 750 ${home}
|
||||
sudo chmod 600 ${authorized_keys}
|
||||
sudo chown -R ${user}:borgwarehouse ${home}
|
||||
|
||||
## Check if authorized_keys exists
|
||||
if [ ! -f "${authorized_keys}" ];then
|
||||
echo "${authorized_keys} must be present"
|
||||
exit 4
|
||||
# Append only mode
|
||||
if [ "$3" == "true" ]; then
|
||||
appendOnlyMode=" --append-only"
|
||||
else
|
||||
appendOnlyMode=""
|
||||
fi
|
||||
|
||||
## Add ssh public key in authorized_keys with borg restriction for only 1 repository (:$1) and storage quota
|
||||
restricted_authkeys="command=\"cd ${pool};borg serve --restrict-to-repository ${pool}/$1 --storage-quota $3G\",restrict $2"
|
||||
echo "$restricted_authkeys" | sudo tee ${authorized_keys} >/dev/null
|
||||
## Add ssh public key in authorized_keys with borg restriction for only 1 repository and storage quota
|
||||
restricted_authkeys="command=\"cd ${pool};borg serve${appendOnlyMode} --restrict-to-path ${pool}/${repositoryName} --storage-quota $2G\",restrict $1"
|
||||
echo "$restricted_authkeys" | tee -a "${authorized_keys}" >/dev/null
|
||||
|
||||
## Return the unix user
|
||||
echo ${user}
|
||||
## Return the repositoryName
|
||||
echo "${repositoryName}"
|
|
@ -1,32 +1,47 @@
|
|||
#!/bin/bash
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# Shell created by Raven for BorgWarehouse.
|
||||
# This shell takes 1 arg : [user] with 8 char. length only.
|
||||
# This shell **delete the user** in arg and **all his data**.
|
||||
# This shell takes 1 arg : [repositoryName] with 8 char. length only.
|
||||
# This shell **delete the repository** in arg and **all his data** and the line associated in the authorized_keys file.
|
||||
|
||||
# Exit when any command fails
|
||||
set -e
|
||||
|
||||
# Load .env if exists
|
||||
if [[ -f .env ]]; then
|
||||
source .env
|
||||
fi
|
||||
|
||||
# Default value if .env not exists
|
||||
: "${home:=/home/borgwarehouse}"
|
||||
|
||||
# Some variables
|
||||
pool="${home}/repos"
|
||||
authorized_keys="${home}/.ssh/authorized_keys"
|
||||
|
||||
# Check arg
|
||||
if [[ $# -ne 1 || $1 = "" ]]; then
|
||||
echo "You must provide a username in argument."
|
||||
echo -n "You must provide a repositoryName in argument."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check if username length is 8 char. With createRepo.sh our randoms have a length of 8 characters.
|
||||
# Check if the repositoryName length is 8 char. With createRepo.sh our randoms have a length of 8 characters.
|
||||
# If we receive another length there is necessarily a problem.
|
||||
username=$1
|
||||
if [ ${#username} != 8 ]
|
||||
then
|
||||
echo "Error with the length of the username."
|
||||
repositoryName=$1
|
||||
if [ ${#repositoryName} != 8 ]; then
|
||||
echo -n "Error with the length of the repositoryName."
|
||||
exit 2
|
||||
fi
|
||||
|
||||
# Delete the user if it exists
|
||||
if id "$1" &>/dev/null; then
|
||||
sudo userdel -rf $1
|
||||
echo "The user $1 and all his data have been deleted"
|
||||
# Delete the repository and the line associated in the authorized_keys file
|
||||
if [ -d "${pool}/${repositoryName}" ]; then
|
||||
# Delete the repository
|
||||
rm -rf """${pool}""/""${repositoryName:?}"""
|
||||
# Delete the line in the authorized_keys file
|
||||
sed -i "/${repositoryName}/d" "${authorized_keys}"
|
||||
echo -n "The folder ""${pool}"/"${repositoryName}"" and all its data have been deleted. The line associated in the authorized_keys file has been deleted."
|
||||
else
|
||||
echo "The user $1 does not exist"
|
||||
exit 3
|
||||
# Delete the line in the authorized_keys file
|
||||
sed -i "/${repositoryName}/d" "${authorized_keys}"
|
||||
echo -n "The folder ""${pool}"/"${repositoryName}"" did not exist (repository never initialized or used). The line associated in the authorized_keys file has been deleted."
|
||||
fi
|
|
@ -1,20 +1,16 @@
|
|||
#!/bin/bash
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# Shell created by Raven for BorgWarehouse.
|
||||
# Get the timestamp of the last modification of the file integrity.* for of all repositories in a JSON output.
|
||||
# stdout will be an array like :
|
||||
# [
|
||||
# {
|
||||
# "user": "09d8240f",
|
||||
# "lastSave": 1668513608
|
||||
# "repositoryName": "a7035047",
|
||||
# "lastSave": 1691341603
|
||||
# },
|
||||
# {
|
||||
# "user": "635a6f8b",
|
||||
# "lastSave": 1667910810
|
||||
# },
|
||||
# {
|
||||
# "user": "83bd4ef1",
|
||||
# "lastSave": 1667985985
|
||||
# "repositoryName": "a7035048",
|
||||
# "lastSave": 1691342688
|
||||
# }
|
||||
# ]
|
||||
|
||||
|
@ -22,4 +18,18 @@
|
|||
# Exit when any command fails
|
||||
set -e
|
||||
|
||||
stat -c {\"user\":\"%U\",\"lastSave\":%Y\} /var/borgwarehouse/*/repos/*/integrity* | jq -s
|
||||
# Load .env if exists
|
||||
if [[ -f .env ]]; then
|
||||
source .env
|
||||
fi
|
||||
|
||||
# Default value if .env not exists
|
||||
: "${home:=/home/borgwarehouse}"
|
||||
|
||||
if [ -n "$(find -L "${home}"/repos -mindepth 1 -maxdepth 1 -type d)" ]; then
|
||||
stat --format='{"repositoryName":"%n","lastSave":%Y}' \
|
||||
"${home}"/repos/*/integrity* |
|
||||
jq --slurp '[.[] | .repositoryName = (.repositoryName | split("/")[-2])]'
|
||||
else
|
||||
echo "[]"
|
||||
fi
|
|
@ -1,4 +1,4 @@
|
|||
#!/bin/bash
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# Shell created by Raven for BorgWarehouse.
|
||||
# Get the size of all repositories in a JSON output.
|
||||
|
@ -14,6 +14,14 @@
|
|||
# Exit when any command fails
|
||||
set -e
|
||||
|
||||
# Load .env if exists
|
||||
if [[ -f .env ]]; then
|
||||
source .env
|
||||
fi
|
||||
|
||||
# Default value if .env not exists
|
||||
: "${home:=/home/borgwarehouse}"
|
||||
|
||||
# Use jc to output a JSON format with du command
|
||||
cd /var/borgwarehouse
|
||||
sudo jc du -s *
|
||||
cd "${home}"/repos
|
||||
du -s -- * | jc --du
|
||||
|
|
136
helpers/shells/recreateRepoConfigFile-v1.sh
Normal file
136
helpers/shells/recreateRepoConfigFile-v1.sh
Normal file
|
@ -0,0 +1,136 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
################################################################################
|
||||
# What is this script ?
|
||||
# If you lose the repo.json file, this script will help you rebuild a new one.
|
||||
# To do this, I've written this shell that reads the BorgWarehouse repository
|
||||
# tree and generates a corresponding object.
|
||||
# This script is only intended to be used in emergencies (data corruption,
|
||||
# update problems...) and as a last resort to rebuild your repo.json file.
|
||||
|
||||
# Of course, certain parameters cannot be recovered, such as comments,
|
||||
# repository size or aliases.
|
||||
# You'll have to re-configure this from the web interface, but most of the work
|
||||
# is done.
|
||||
|
||||
# This script should be used with the root user, as it is necessary to read
|
||||
# authorized_keys files.
|
||||
|
||||
# This script simply displays a valid JSON object on your screen. Copy its
|
||||
# entire content into the config/repo.json file.
|
||||
# There's no need to restart BorgWarehouse, as this can be done on the fly.
|
||||
|
||||
# With the option `-a` or `--auto-size` the script calculates the current size
|
||||
# of the repo and calculates the next largest two potency.
|
||||
# By default the size is otherwise 2G.
|
||||
# Examples for the calculation:
|
||||
#
|
||||
# | Size | Calc. |
|
||||
# |-----:|------:|
|
||||
# | <=2G | 2G |
|
||||
# | 5G | 8G |
|
||||
# | 9G | 16G |
|
||||
# | 43G | 64G |
|
||||
################################################################################
|
||||
|
||||
bwDataDir="/var/borgwarehouse"
|
||||
directoriesList=$(ls -A $bwDataDir)
|
||||
_AUTOSIZE=0
|
||||
|
||||
POSITIONAL_ARGS=()
|
||||
|
||||
# shellcheck disable=SC2221,SC2222
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case $1 in
|
||||
-a|--auto-size)
|
||||
_AUTOSIZE=1; shift ;;
|
||||
-*|--*)
|
||||
echo "Unknown option $1"; exit 1 ;;
|
||||
*)
|
||||
POSITIONAL_ARGS+=("$1") ; shift ;;
|
||||
esac
|
||||
done
|
||||
|
||||
set -- "${POSITIONAL_ARGS[@]}" # restore positional parameters
|
||||
|
||||
function __repoSize() {
|
||||
if [ $_AUTOSIZE -eq 1 ]; then
|
||||
_repoSizeBytes=$(du --summarize --bytes "${1}" |
|
||||
grep --perl-regexp --only-matching '^\d+')
|
||||
if [ "$_repoSizeBytes" -le 2147483648 ]; then
|
||||
# Under 2G
|
||||
echo 2
|
||||
else
|
||||
# More than 2G, the next power of two is determined.
|
||||
_factor=2
|
||||
while true; do
|
||||
_repoSize=$((2**i))
|
||||
if [ 123 -lt $_repoSize ]; then
|
||||
echo $_repoSize
|
||||
break
|
||||
fi
|
||||
((i++))
|
||||
done
|
||||
fi
|
||||
else
|
||||
echo "2"
|
||||
fi
|
||||
}
|
||||
|
||||
finalObject="[]"
|
||||
i=0
|
||||
# Loop on each directory in bw-data
|
||||
for directory in $directoriesList ; do
|
||||
unixUser=$directory
|
||||
repository=$(ls "$bwDataDir/$directory/repos/")
|
||||
id=$i
|
||||
alias="Repo to rename $i"
|
||||
lastSave=0
|
||||
alert=90000
|
||||
storageSize=$(__repoSize "$bwDataDir/$directory/repos/$repository")
|
||||
storageUsed=0
|
||||
comment=""
|
||||
displayDetails=true
|
||||
status=false
|
||||
sshPublicKey=$(grep --only-matching --perl-regexp \
|
||||
'(?<=restrict ).*' \
|
||||
"$bwDataDir/$directory/.ssh/authorized_keys")
|
||||
|
||||
# Create a valid JSON object with jq for each repo
|
||||
objRepoJSON=$(jq -n --argjson id $id \
|
||||
--arg alias "$alias" \
|
||||
--arg repository "$repository" \
|
||||
--argjson status $status \
|
||||
--argjson lastSave $lastSave \
|
||||
--argjson alert $alert \
|
||||
--argjson storageSize "$storageSize" \
|
||||
--argjson storageUsed $storageUsed \
|
||||
--arg sshPublicKey "$sshPublicKey" \
|
||||
--arg comment "$comment" \
|
||||
--argjson displayDetails $displayDetails \
|
||||
--arg unixUser "$unixUser" \
|
||||
"{ \
|
||||
id: \$id, \
|
||||
alias: \$alias, \
|
||||
repository: \$repository, \
|
||||
status: \$status, \
|
||||
lastSave: \$lastSave, \
|
||||
alert: \$alert, \
|
||||
storageSize: \$storageSize, \
|
||||
storageUsed: \$storageUsed, \
|
||||
sshPublicKey: \$sshPublicKey, \
|
||||
comment: \$comment, \
|
||||
displayDetails: \$displayDetails, \
|
||||
unixUser: \$unixUser \
|
||||
}")
|
||||
|
||||
# Insert objRepoJSON in finalObject with jq
|
||||
finalObject=$(jq --argjson objRepoJSON \
|
||||
"$objRepoJSON" '. += [$objRepoJSON]' \
|
||||
<<< "$finalObject")
|
||||
|
||||
i=$((i+1))
|
||||
done
|
||||
|
||||
#Display finalObject on screen to copy/paste it in repo.json file
|
||||
echo "$finalObject"
|
|
@ -1,47 +1,79 @@
|
|||
#!/bin/bash
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# Shell created by Raven for BorgWarehouse.
|
||||
# This shell takes 2 args : [user] [new SSH pub key] [quota]
|
||||
# This shell updates the ssh key for a repository.
|
||||
# This shell takes 4 args: [repositoryName] [new SSH pub key] [quota] [append-only mode (boolean)]
|
||||
# This shell updates the SSH key and the quota for a repository.
|
||||
|
||||
# Exit when any command fails
|
||||
set -e
|
||||
|
||||
# Check args
|
||||
if [ "$1" == "" ] || [ "$2" == "" ] || [ "$3" == "" ];then
|
||||
echo "This shell takes 3 args : [user] [new SSH pub key] [quota]"
|
||||
exit 1
|
||||
# Load .env if exists
|
||||
if [[ -f .env ]]; then
|
||||
source .env
|
||||
fi
|
||||
|
||||
# Some variables
|
||||
home="/var/borgwarehouse/$1"
|
||||
# Default value if .env not exists
|
||||
: "${home:=/home/borgwarehouse}"
|
||||
|
||||
# Check args
|
||||
if [ "$1" == "" ] || [ "$2" == "" ] || [ "$3" == "" ] || [ "$4" != "true" ] && [ "$4" != "false" ]; then
|
||||
echo -n "This shell takes 4 args: [repositoryName] [new SSH pub key] [quota] [Append only mode [true|false]]"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check if the SSH public key is a valid format
|
||||
# This pattern validates SSH public keys for : rsa, ed25519, ed25519-sk
|
||||
pattern='(ssh-ed25519 AAAAC3NzaC1lZDI1NTE5|sk-ssh-ed25519@openssh.com AAAAGnNrLXNzaC1lZDI1NTE5QG9wZW5zc2guY29t|ssh-rsa AAAAB3NzaC1yc2)[0-9A-Za-z+/]+[=]{0,3}(\s.*)?'
|
||||
if [[ ! "$2" =~ $pattern ]]
|
||||
then
|
||||
echo "Invalid public SSH KEY format. Provide a key in OpenSSH format (rsa, ed25519, ed25519-sk)"
|
||||
echo -n "Invalid public SSH KEY format. Provide a key in OpenSSH format (rsa, ed25519, ed25519-sk)"
|
||||
exit 2
|
||||
fi
|
||||
|
||||
# Check if username length is 8 char. With createRepo.sh our randoms have a length of 8 characters.
|
||||
# If we receive another length there is necessarily a problem.
|
||||
username=$1
|
||||
if [ ${#username} != 8 ]
|
||||
then
|
||||
echo "Error with the length of the username."
|
||||
# Check if repositoryName length is 8 char. With createRepo.sh our randoms have a length of 8 characters.
|
||||
# If we receive another length, there is necessarily a problem.
|
||||
repositoryName=$1
|
||||
if [ ${#repositoryName} != 8 ]; then
|
||||
echo -n "Error with the length of the repositoryName."
|
||||
exit 3
|
||||
fi
|
||||
|
||||
# Check if the user exists
|
||||
if ! id "$1" &>/dev/null; then
|
||||
echo "The user $1 does not exist"
|
||||
# Check if a line in authorized_keys contains repository_name
|
||||
if ! grep -q "command=\".*${repositoryName}.*\",restrict" "$home/.ssh/authorized_keys"; then
|
||||
echo -n "No line containing $repositoryName found in authorized_keys"
|
||||
exit 4
|
||||
fi
|
||||
|
||||
# Modify authorized_keys for the user : only the ssh key is modify with this regex
|
||||
sudo sed -ri "s|(command=\".*\",restrict ).*|\1$2|g" "$home/.ssh/authorized_keys"
|
||||
# Check if the new SSH pub key is already present on a line OTHER than the one corresponding to repositoryName
|
||||
found=false
|
||||
regex="command=\".*${repositoryName}.*\",restrict"
|
||||
while IFS= read -r line; do
|
||||
if [[ $line =~ $pattern ]]; then
|
||||
# Get the SSH pub key of the line (ignore the comment)
|
||||
key1=$(echo "${BASH_REMATCH[0]}" | awk '{print $1 " " $2}')
|
||||
# Get the SSH pub key of the new SSH pub key (ignore the comment)
|
||||
key2=$(echo "$2" | awk '{print $1 " " $2}')
|
||||
|
||||
# Modify authorized_keys for the user : only the quota is modify with this regex
|
||||
sudo sed -ri "s|--storage-quota.*\"|--storage-quota $3G\"|g" "$home/.ssh/authorized_keys"
|
||||
if [ "$key1" == "$key2" ]; then
|
||||
# If the SSH pub key is already present on a line other than the one corresponding to repositoryName
|
||||
if [[ ! $line =~ $regex ]]; then
|
||||
found=true
|
||||
break
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
done < "$home/.ssh/authorized_keys"
|
||||
if [ "$found" = true ]; then
|
||||
echo -n "This SSH pub key is already present in authorized_keys on a different line."
|
||||
exit 5
|
||||
fi
|
||||
|
||||
# Append only mode
|
||||
if [ "$4" == "true" ]; then
|
||||
sed -ri "/command=\".*${repositoryName}.*\",restrict/ {/borg serve .*--append-only /! s|(borg serve )|\1--append-only |}" "$home/.ssh/authorized_keys"
|
||||
elif [ "$4" == "false" ]; then
|
||||
sed -ri "/command=\".*${repositoryName}.*\",restrict/ s|(--append-only )||g" "$home/.ssh/authorized_keys"
|
||||
fi
|
||||
|
||||
# Modify authorized_keys for the repositoryName: update the line with the quota and the SSH pub key
|
||||
sed -ri "s|(command=\".*${repositoryName}.*--storage-quota ).*G\",restrict .*|\\1$3G\",restrict $2|g" "$home/.ssh/authorized_keys"
|
||||
|
|
BIN
helpers/templates/attachments/alert-icon.png
Normal file
BIN
helpers/templates/attachments/alert-icon.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 2.8 KiB |
BIN
helpers/templates/attachments/valid-icon.png
Normal file
BIN
helpers/templates/attachments/valid-icon.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.1 KiB |
135
helpers/templates/emailAlertStatus.js
Normal file
135
helpers/templates/emailAlertStatus.js
Normal file
|
@ -0,0 +1,135 @@
|
|||
export default function emailTest(mailTo, username, aliasList) {
|
||||
const aliasTemplate = (x) => {
|
||||
let str = '';
|
||||
for (const alias of x) {
|
||||
str = str + '<li>' + alias + '</li>';
|
||||
}
|
||||
return str;
|
||||
};
|
||||
|
||||
const template = {
|
||||
from: 'BorgWarehouse' + '<' + process.env.MAIL_SMTP_FROM + '>',
|
||||
to: mailTo,
|
||||
subject: 'Down status alert !',
|
||||
text: 'Some repositories require your attention ! Please, check your BorgWarehouse interface.',
|
||||
html:
|
||||
`
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<style>
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
font-family: 'Inter', sans-serif;
|
||||
}
|
||||
.container {
|
||||
max-width: 475px;
|
||||
margin: 40px auto;
|
||||
border: 1px solid #eaeaea;
|
||||
border-radius: 5px;
|
||||
overflow: hidden;
|
||||
}
|
||||
.header {
|
||||
border-top: 4px solid;
|
||||
border-image-source: linear-gradient(90deg, #020024 0%, #6d4aff 50%, #020024 100%);
|
||||
border-image-slice: 1;
|
||||
}
|
||||
.content {
|
||||
padding: 20px;
|
||||
}
|
||||
.title {
|
||||
color: #6d4aff;
|
||||
font-weight: 700;
|
||||
font-size: 2em;
|
||||
text-align: center;
|
||||
margin-top: 20px;
|
||||
}
|
||||
.icon {
|
||||
display: block;
|
||||
margin: 30px auto 20px;
|
||||
text-align: center;
|
||||
}
|
||||
.message {
|
||||
font-weight: 500;
|
||||
color: #494b7a;
|
||||
font-size: 1.5em;
|
||||
text-align: center;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.description {
|
||||
color: #494b7a;
|
||||
font-size: 1.1em;
|
||||
margin-bottom: 50px;
|
||||
}
|
||||
.alert {
|
||||
margin: 2rem -0.5rem 0rem -0.5rem;
|
||||
color: #494b7a;
|
||||
background: #fff;
|
||||
border: 1px solid #6d4aff5c;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.12), 0 1px 2px rgba(0, 0, 0, 0.24);
|
||||
border-radius: 5px;
|
||||
font-size: 0.8em;
|
||||
font-family: Inter, sans-serif;
|
||||
padding: 1rem 1.5rem;
|
||||
display: flex;
|
||||
}
|
||||
.footer {
|
||||
font-size: 0.8em;
|
||||
color: #C8C8C8;
|
||||
text-align: center;
|
||||
}
|
||||
.footer a {
|
||||
color: #cfc4fb;
|
||||
text-decoration: none;
|
||||
}
|
||||
.alert a {
|
||||
text-decoration: none;
|
||||
color: #6d4aff;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="header"></div>
|
||||
<div class="content">
|
||||
<div class="title">BorgWarehouse</div>
|
||||
<div class="icon">
|
||||
<img src="cid:alert-icon" alt="alert icon" width="96" height="96">
|
||||
</div>
|
||||
<div class="message">
|
||||
<p>Some repositories require your attention, ` +
|
||||
username +
|
||||
`!</p>
|
||||
</div>
|
||||
<div class="description">
|
||||
<p>List of repositories with down status:</p>
|
||||
<ul>` +
|
||||
aliasTemplate(aliasList) +
|
||||
`</ul>
|
||||
</div>
|
||||
<div class="alert">
|
||||
<div style="flex-shrink: 1; margin-right: 0.75rem">🚩</div>
|
||||
<div style="width: 100%">
|
||||
Please remember that the status is based on <b>the last modification</b>. Backups are <b>encrypted from end to end between your client and the server</b> controlled by BorgWarehouse. Don't forget to <a href="https://borgwarehouse.com/docs/user-manual/setupwizard/#step-3--launch--verify" rel="noreferrer">check the integrity of your backups regularly</a>.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="footer">
|
||||
<p>About <a href="https://borgwarehouse.com/" target="_blank" rel="noreferrer">BorgWarehouse</a></p>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
`,
|
||||
attachments: [
|
||||
{
|
||||
path: 'helpers/templates/attachments/alert-icon.png',
|
||||
cid: 'alert-icon',
|
||||
},
|
||||
],
|
||||
};
|
||||
return template;
|
||||
}
|
103
helpers/templates/emailTest.js
Normal file
103
helpers/templates/emailTest.js
Normal file
|
@ -0,0 +1,103 @@
|
|||
export default function emailTest(mailTo, username) {
|
||||
const template = {
|
||||
from: 'BorgWarehouse' + '<' + process.env.MAIL_SMTP_FROM + '>',
|
||||
to: mailTo,
|
||||
subject: 'Testing email settings',
|
||||
text: 'If you received this email then the mail configuration seems to be correct.',
|
||||
html:
|
||||
`
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<style>
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
font-family: 'Inter', sans-serif;
|
||||
}
|
||||
.container {
|
||||
max-width: 475px;
|
||||
margin: 40px auto;
|
||||
border: 1px solid #eaeaea;
|
||||
border-radius: 5px;
|
||||
overflow: hidden;
|
||||
}
|
||||
.header {
|
||||
border-top: 4px solid;
|
||||
border-image-source: linear-gradient(90deg, #020024 0%, #6d4aff 50%, #020024 100%);
|
||||
border-image-slice: 1;
|
||||
}
|
||||
.content {
|
||||
padding: 20px;
|
||||
}
|
||||
.title {
|
||||
color: #6d4aff;
|
||||
font-weight: 700;
|
||||
font-size: 2em;
|
||||
text-align: center;
|
||||
margin-top: 20px;
|
||||
}
|
||||
.icon {
|
||||
display: block;
|
||||
margin: 30px auto 20px;
|
||||
text-align: center;
|
||||
}
|
||||
.message {
|
||||
font-weight: 500;
|
||||
color: #494b7a;
|
||||
font-size: 1.5em;
|
||||
text-align: center;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.description {
|
||||
color: #494b7a;
|
||||
font-size: 1.1em;
|
||||
text-align: center;
|
||||
margin-bottom: 50px;
|
||||
}
|
||||
.footer {
|
||||
font-size: 0.8em;
|
||||
color: #C8C8C8;
|
||||
text-align: center;
|
||||
}
|
||||
.footer a {
|
||||
color: #cfc4fb;
|
||||
text-decoration: none;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="header"></div>
|
||||
<div class="content">
|
||||
<div class="title">BorgWarehouse</div>
|
||||
<div class="icon">
|
||||
<img src="cid:valid-icon" alt="valid icon" width="96" height="96">
|
||||
</div>
|
||||
<div class="message">
|
||||
<p>Good job, ` +
|
||||
username +
|
||||
`!</p>
|
||||
</div>
|
||||
<div class="description">
|
||||
<p>If you received this mail then the configuration seems to be correct.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="footer">
|
||||
<p>About <a href="https://borgwarehouse.com/" target="_blank" rel="noreferrer">BorgWarehouse</a></p>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
`,
|
||||
attachments: [
|
||||
{
|
||||
path: 'helpers/templates/attachments/valid-icon.png',
|
||||
cid: 'valid-icon',
|
||||
},
|
||||
],
|
||||
};
|
||||
return template;
|
||||
}
|
6951
package-lock.json
generated
6951
package-lock.json
generated
File diff suppressed because it is too large
Load diff
30
package.json
30
package.json
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "borgwarehouse",
|
||||
"version": "1.0",
|
||||
"version": "2.3.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
|
@ -9,23 +9,23 @@
|
|||
"lint": "next lint"
|
||||
},
|
||||
"dependencies": {
|
||||
"@tabler/icons": "^1.96.0",
|
||||
"@tabler/icons-react": "^3.7.0",
|
||||
"bcryptjs": "^2.4.3",
|
||||
"chart.js": "^3.9.1",
|
||||
"next": "^13.0.5",
|
||||
"next-auth": "^4.17.0",
|
||||
"react": "^18.2.0",
|
||||
"react-chartjs-2": "^4.3.1",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-hook-form": "^7.36.1",
|
||||
"react-modal": "^3.15.1",
|
||||
"react-select": "^5.6.0",
|
||||
"react-toastify": "^9.0.8",
|
||||
"chart.js": "^4.4.3",
|
||||
"next": "^14.2.4",
|
||||
"next-auth": "^4.24.7",
|
||||
"nodemailer": "^6.9.14",
|
||||
"react": "^18.3.1",
|
||||
"react-chartjs-2": "^5.2.0",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-hook-form": "^7.52.0",
|
||||
"react-select": "^5.8.0",
|
||||
"react-toastify": "^10.0.5",
|
||||
"spinners-react": "^1.0.7",
|
||||
"swr": "^1.3.0"
|
||||
"swr": "^2.2.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"eslint": "8.23.1",
|
||||
"eslint-config-next": "^13.0.5"
|
||||
"eslint-config-next": "^14.2.4",
|
||||
"prettier": "^3.3.2"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -20,7 +20,7 @@ export default function MyApp({ Component, pageProps }) {
|
|||
<link rel='shortcut icon' href='/favicon.ico' />
|
||||
<title>BorgWarehouse</title>
|
||||
</Head>
|
||||
<ToastContainer />
|
||||
<ToastContainer stacked />
|
||||
<Component {...pageProps} />
|
||||
</Layout>
|
||||
</SessionProvider>
|
||||
|
|
|
@ -1,201 +1,35 @@
|
|||
//Lib
|
||||
import Head from 'next/head';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { useState } from 'react';
|
||||
import { SpinnerDotted } from 'spinners-react';
|
||||
import { toast } from 'react-toastify';
|
||||
import 'react-toastify/dist/ReactToastify.css';
|
||||
import { useSession } from 'next-auth/react';
|
||||
import { authOptions } from '../../pages/api/auth/[...nextauth]';
|
||||
import { unstable_getServerSession } from 'next-auth/next';
|
||||
import { getServerSession } from 'next-auth/next';
|
||||
|
||||
//Components
|
||||
import Error from '../../Components/UI/Error/Error';
|
||||
import UserSettings from '../../Containers/UserSettings/UserSettings';
|
||||
|
||||
export default function Account() {
|
||||
////Var
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
formState: { errors },
|
||||
reset,
|
||||
} = useForm();
|
||||
const { status, data } = useSession();
|
||||
|
||||
////State
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [error, setError] = useState();
|
||||
|
||||
////Functions
|
||||
//Form submit Handler for ADD a repo
|
||||
const formSubmitHandler = async (data) => {
|
||||
//Remove old error
|
||||
setError();
|
||||
//Loading button on submit to avoid multiple send.
|
||||
setIsLoading(true);
|
||||
//POST API to send the new and old password
|
||||
const response = await fetch('/api/account/updatePassword', {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
const result = await response.json();
|
||||
|
||||
if (!response.ok) {
|
||||
setIsLoading(false);
|
||||
reset();
|
||||
setError(result.message);
|
||||
setTimeout(() => setError(), 4000);
|
||||
} else {
|
||||
reset();
|
||||
setIsLoading(false);
|
||||
toast.success('🔑 Password edited !', {
|
||||
position: 'top-right',
|
||||
autoClose: 5000,
|
||||
hideProgressBar: false,
|
||||
closeOnClick: true,
|
||||
pauseOnHover: true,
|
||||
draggable: true,
|
||||
progress: undefined,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
//Function
|
||||
if (status == 'unauthenticated' || status == 'loading') {
|
||||
return <p>Loading...</p>;
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<Head>
|
||||
<title>Account - BorgWarehouse</title>
|
||||
</Head>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
justifyContent: 'center',
|
||||
}}
|
||||
>
|
||||
<div>
|
||||
<h1 style={{ color: '#494b7a', textAlign: 'center' }}>
|
||||
Welcome {status === 'authenticated' && data.user.name}{' '}
|
||||
👋
|
||||
</h1>
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
justifyContent: 'center',
|
||||
margin: '15px 0 0 0 ',
|
||||
width: 'auto',
|
||||
}}
|
||||
>
|
||||
<section
|
||||
style={{ display: 'flex', justifyContent: 'center' }}
|
||||
>
|
||||
<main
|
||||
style={{
|
||||
backgroundColor: '#212942',
|
||||
padding: '30px',
|
||||
borderRadius: '10px',
|
||||
boxShadow:
|
||||
'0 14px 28px rgba(0, 0, 0, .2), 0 10px 10px rgba(0, 0, 0, .2)',
|
||||
height: '100%',
|
||||
borderTop: '10px solid #704dff',
|
||||
animation: 'ease-in 0.3s 1 normal none',
|
||||
width: '360px',
|
||||
}}
|
||||
>
|
||||
<h1
|
||||
style={{
|
||||
color: '#a1a4ad',
|
||||
letterSpacing: '1.5px',
|
||||
textAlign: 'center',
|
||||
marginBottom: '2.5em',
|
||||
}}
|
||||
>
|
||||
Change your password
|
||||
</h1>
|
||||
{error && <Error message={error} />}
|
||||
<form onSubmit={handleSubmit(formSubmitHandler)}>
|
||||
<p>
|
||||
<input
|
||||
type='password'
|
||||
placeholder='Actual password'
|
||||
className='signInInput'
|
||||
{...register('oldPassword', {
|
||||
required: true,
|
||||
})}
|
||||
/>
|
||||
{errors.oldPassword &&
|
||||
errors.oldPassword.type ===
|
||||
'required' && (
|
||||
<small
|
||||
style={{
|
||||
color: 'red',
|
||||
display: 'block',
|
||||
marginTop: '3px',
|
||||
}}
|
||||
>
|
||||
This field is required.
|
||||
</small>
|
||||
)}
|
||||
</p>
|
||||
<p>
|
||||
<input
|
||||
type='password'
|
||||
placeholder='New password'
|
||||
className='signInInput'
|
||||
{...register('newPassword', {
|
||||
required: true,
|
||||
})}
|
||||
/>
|
||||
{errors.newPassword && (
|
||||
<small
|
||||
style={{
|
||||
color: 'red',
|
||||
display: 'block',
|
||||
marginTop: '3px',
|
||||
}}
|
||||
>
|
||||
This field is required.
|
||||
</small>
|
||||
)}
|
||||
</p>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
}}
|
||||
>
|
||||
<button
|
||||
className='signInButton'
|
||||
disabled={isLoading}
|
||||
>
|
||||
{isLoading ? (
|
||||
<SpinnerDotted
|
||||
size={20}
|
||||
thickness={150}
|
||||
speed={100}
|
||||
color='#fff'
|
||||
/>
|
||||
) : (
|
||||
'Update your password'
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</main>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<UserSettings status={status} data={data} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export async function getServerSideProps(context) {
|
||||
//Var
|
||||
const session = await unstable_getServerSession(
|
||||
const session = await getServerSession(
|
||||
context.req,
|
||||
context.res,
|
||||
authOptions
|
||||
|
|
63
pages/api/account/getAppriseAlert.js
Normal file
63
pages/api/account/getAppriseAlert.js
Normal file
|
@ -0,0 +1,63 @@
|
|||
//Lib
|
||||
import { promises as fs } from 'fs';
|
||||
import path from 'path';
|
||||
import { authOptions } from '../auth/[...nextauth]';
|
||||
import { getServerSession } from 'next-auth/next';
|
||||
|
||||
export default async function handler(req, res) {
|
||||
if (req.method == 'GET') {
|
||||
//Verify that the user is logged in.
|
||||
const session = await getServerSession(req, res, authOptions);
|
||||
if (!session) {
|
||||
res.status(401).json({ message: 'You must be logged in.' });
|
||||
return;
|
||||
}
|
||||
try {
|
||||
//Read the users file
|
||||
//Find the absolute path of the json directory
|
||||
const jsonDirectory = path.join(process.cwd(), '/config');
|
||||
let usersList = await fs.readFile(
|
||||
jsonDirectory + '/users.json',
|
||||
'utf8'
|
||||
);
|
||||
//Parse the usersList
|
||||
usersList = JSON.parse(usersList);
|
||||
|
||||
//Verify that the user of the session exists
|
||||
const userIndex = usersList
|
||||
.map((user) => user.username)
|
||||
.indexOf(session.user.name);
|
||||
if (userIndex === -1) {
|
||||
res.status(400).json({
|
||||
message:
|
||||
'User is incorrect. Please, logout to update your session.',
|
||||
});
|
||||
return;
|
||||
} else {
|
||||
//Send the appriseAlert bool
|
||||
res.status(200).json({
|
||||
appriseAlert: usersList[userIndex].appriseAlert,
|
||||
});
|
||||
return;
|
||||
}
|
||||
} catch (error) {
|
||||
//Log for backend
|
||||
console.log(error);
|
||||
//Log for frontend
|
||||
if (error.code == 'ENOENT') {
|
||||
res.status(500).json({
|
||||
status: 500,
|
||||
message: 'No such file or directory',
|
||||
});
|
||||
} else {
|
||||
res.status(500).json({
|
||||
status: 500,
|
||||
message: 'API error, contact the administrator',
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
res.status(405).json({ message: 'Bad request on API' });
|
||||
}
|
||||
}
|
65
pages/api/account/getAppriseMode.js
Normal file
65
pages/api/account/getAppriseMode.js
Normal file
|
@ -0,0 +1,65 @@
|
|||
//Lib
|
||||
import { promises as fs } from 'fs';
|
||||
import path from 'path';
|
||||
import { authOptions } from '../auth/[...nextauth]';
|
||||
import { getServerSession } from 'next-auth/next';
|
||||
|
||||
export default async function handler(req, res) {
|
||||
if (req.method == 'GET') {
|
||||
//Verify that the user is logged in.
|
||||
const session = await getServerSession(req, res, authOptions);
|
||||
if (!session) {
|
||||
res.status(401).json({ message: 'You must be logged in.' });
|
||||
return;
|
||||
}
|
||||
try {
|
||||
//Read the users file
|
||||
//Find the absolute path of the json directory
|
||||
const jsonDirectory = path.join(process.cwd(), '/config');
|
||||
let usersList = await fs.readFile(
|
||||
jsonDirectory + '/users.json',
|
||||
'utf8'
|
||||
);
|
||||
//Parse the usersList
|
||||
usersList = JSON.parse(usersList);
|
||||
|
||||
//Verify that the user of the session exists
|
||||
const userIndex = usersList
|
||||
.map((user) => user.username)
|
||||
.indexOf(session.user.name);
|
||||
if (userIndex === -1) {
|
||||
res.status(400).json({
|
||||
message:
|
||||
'User is incorrect. Please, logout to update your session.',
|
||||
});
|
||||
return;
|
||||
} else {
|
||||
//Send the appriseMode object
|
||||
res.status(200).json({
|
||||
appriseMode: usersList[userIndex].appriseMode,
|
||||
appriseStatelessURL:
|
||||
usersList[userIndex].appriseStatelessURL,
|
||||
});
|
||||
return;
|
||||
}
|
||||
} catch (error) {
|
||||
//Log for backend
|
||||
console.log(error);
|
||||
//Log for frontend
|
||||
if (error.code == 'ENOENT') {
|
||||
res.status(500).json({
|
||||
status: 500,
|
||||
message: 'No such file or directory',
|
||||
});
|
||||
} else {
|
||||
res.status(500).json({
|
||||
status: 500,
|
||||
message: 'API error, contact the administrator',
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
res.status(405).json({ message: 'Bad request on API' });
|
||||
}
|
||||
}
|
63
pages/api/account/getAppriseServices.js
Normal file
63
pages/api/account/getAppriseServices.js
Normal file
|
@ -0,0 +1,63 @@
|
|||
//Lib
|
||||
import { promises as fs } from 'fs';
|
||||
import path from 'path';
|
||||
import { authOptions } from '../auth/[...nextauth]';
|
||||
import { getServerSession } from 'next-auth/next';
|
||||
|
||||
export default async function handler(req, res) {
|
||||
if (req.method == 'GET') {
|
||||
//Verify that the user is logged in.
|
||||
const session = await getServerSession(req, res, authOptions);
|
||||
if (!session) {
|
||||
res.status(401).json({ message: 'You must be logged in.' });
|
||||
return;
|
||||
}
|
||||
try {
|
||||
//Read the users file
|
||||
//Find the absolute path of the json directory
|
||||
const jsonDirectory = path.join(process.cwd(), '/config');
|
||||
let usersList = await fs.readFile(
|
||||
jsonDirectory + '/users.json',
|
||||
'utf8'
|
||||
);
|
||||
//Parse the usersList
|
||||
usersList = JSON.parse(usersList);
|
||||
|
||||
//Verify that the user of the session exists
|
||||
const userIndex = usersList
|
||||
.map((user) => user.username)
|
||||
.indexOf(session.user.name);
|
||||
if (userIndex === -1) {
|
||||
res.status(400).json({
|
||||
message:
|
||||
'User is incorrect. Please, logout to update your session.',
|
||||
});
|
||||
return;
|
||||
} else {
|
||||
//Send the appriseServices array
|
||||
res.status(200).json({
|
||||
appriseServices: usersList[userIndex].appriseServices,
|
||||
});
|
||||
return;
|
||||
}
|
||||
} catch (error) {
|
||||
//Log for backend
|
||||
console.log(error);
|
||||
//Log for frontend
|
||||
if (error.code == 'ENOENT') {
|
||||
res.status(500).json({
|
||||
status: 500,
|
||||
message: 'No such file or directory',
|
||||
});
|
||||
} else {
|
||||
res.status(500).json({
|
||||
status: 500,
|
||||
message: 'API error, contact the administrator',
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
res.status(405).json({ message: 'Bad request on API' });
|
||||
}
|
||||
}
|
63
pages/api/account/getEmailAlert.js
Normal file
63
pages/api/account/getEmailAlert.js
Normal file
|
@ -0,0 +1,63 @@
|
|||
//Lib
|
||||
import { promises as fs } from 'fs';
|
||||
import path from 'path';
|
||||
import { authOptions } from '../auth/[...nextauth]';
|
||||
import { getServerSession } from 'next-auth/next';
|
||||
|
||||
export default async function handler(req, res) {
|
||||
if (req.method == 'GET') {
|
||||
//Verify that the user is logged in.
|
||||
const session = await getServerSession(req, res, authOptions);
|
||||
if (!session) {
|
||||
res.status(401).json({ message: 'You must be logged in.' });
|
||||
return;
|
||||
}
|
||||
try {
|
||||
//Read the users file
|
||||
//Find the absolute path of the json directory
|
||||
const jsonDirectory = path.join(process.cwd(), '/config');
|
||||
let usersList = await fs.readFile(
|
||||
jsonDirectory + '/users.json',
|
||||
'utf8'
|
||||
);
|
||||
//Parse the usersList
|
||||
usersList = JSON.parse(usersList);
|
||||
|
||||
//Verify that the user of the session exists
|
||||
const userIndex = usersList
|
||||
.map((user) => user.username)
|
||||
.indexOf(session.user.name);
|
||||
if (userIndex === -1) {
|
||||
res.status(400).json({
|
||||
message:
|
||||
'User is incorrect. Please, logout to update your session.',
|
||||
});
|
||||
return;
|
||||
} else {
|
||||
//Send the emailAlert bool
|
||||
res.status(200).json({
|
||||
emailAlert: usersList[userIndex].emailAlert,
|
||||
});
|
||||
return;
|
||||
}
|
||||
} catch (error) {
|
||||
//Log for backend
|
||||
console.log(error);
|
||||
//Log for frontend
|
||||
if (error.code == 'ENOENT') {
|
||||
res.status(500).json({
|
||||
status: 500,
|
||||
message: 'No such file or directory',
|
||||
});
|
||||
} else {
|
||||
res.status(500).json({
|
||||
status: 500,
|
||||
message: 'API error, contact the administrator',
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
res.status(405).json({ message: 'Bad request on API' });
|
||||
}
|
||||
}
|
49
pages/api/account/getWizardEnv.js
Normal file
49
pages/api/account/getWizardEnv.js
Normal file
|
@ -0,0 +1,49 @@
|
|||
//Lib
|
||||
import { authOptions } from '../auth/[...nextauth]';
|
||||
import { getServerSession } from 'next-auth/next';
|
||||
|
||||
export default async function handler(req, res) {
|
||||
if (req.method == 'GET') {
|
||||
//Verify that the user is logged in.
|
||||
const session = await getServerSession(req, res, authOptions);
|
||||
if (!session) {
|
||||
res.status(401).json({ message: 'You must be logged in.' });
|
||||
return;
|
||||
}
|
||||
try {
|
||||
function getEnvVariable(envName, defaultValue = '') {
|
||||
return process.env[envName] || defaultValue;
|
||||
}
|
||||
|
||||
const wizardEnv = {
|
||||
UNIX_USER: getEnvVariable('UNIX_USER', 'borgwarehouse'),
|
||||
FQDN: getEnvVariable('FQDN', 'localhost'),
|
||||
SSH_SERVER_PORT: getEnvVariable('SSH_SERVER_PORT', '22'),
|
||||
FQDN_LAN: getEnvVariable('FQDN_LAN'),
|
||||
SSH_SERVER_PORT_LAN: getEnvVariable('SSH_SERVER_PORT_LAN'),
|
||||
SSH_SERVER_FINGERPRINT_RSA: getEnvVariable(
|
||||
'SSH_SERVER_FINGERPRINT_RSA'
|
||||
),
|
||||
SSH_SERVER_FINGERPRINT_ED25519: getEnvVariable(
|
||||
'SSH_SERVER_FINGERPRINT_ED25519'
|
||||
),
|
||||
SSH_SERVER_FINGERPRINT_ECDSA: getEnvVariable(
|
||||
'SSH_SERVER_FINGERPRINT_ECDSA'
|
||||
),
|
||||
};
|
||||
res.status(200).json({ wizardEnv });
|
||||
return;
|
||||
} catch (error) {
|
||||
//Log for backend
|
||||
console.log(error);
|
||||
//Log for frontend
|
||||
res.status(500).json({
|
||||
status: 500,
|
||||
message: 'API error, contact the administrator',
|
||||
});
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
res.status(405).json({ message: 'Bad request on API' });
|
||||
}
|
||||
}
|
163
pages/api/account/sendTestApprise.js
Normal file
163
pages/api/account/sendTestApprise.js
Normal file
|
@ -0,0 +1,163 @@
|
|||
//Lib
|
||||
import { authOptions } from '../auth/[...nextauth]';
|
||||
import { getServerSession } from 'next-auth/next';
|
||||
import { promises as fs } from 'fs';
|
||||
import path from 'path';
|
||||
const { exec } = require('child_process');
|
||||
|
||||
export default async function handler(req, res) {
|
||||
if (req.method == 'POST') {
|
||||
//Verify that the user is logged in.
|
||||
const session = await getServerSession(req, res, authOptions);
|
||||
if (!session) {
|
||||
res.status(401).json({ message: 'You must be logged in.' });
|
||||
return;
|
||||
}
|
||||
|
||||
//The data we expect to receive
|
||||
let { sendTestApprise } = req.body;
|
||||
|
||||
//Read the users file
|
||||
//Find the absolute path of the json directory
|
||||
const jsonDirectory = path.join(process.cwd(), '/config');
|
||||
let usersList = await fs.readFile(
|
||||
jsonDirectory + '/users.json',
|
||||
'utf8'
|
||||
);
|
||||
//Parse the usersList
|
||||
usersList = JSON.parse(usersList);
|
||||
|
||||
//1 : Verify that the user of the session exists
|
||||
const userIndex = usersList
|
||||
.map((user) => user.username)
|
||||
.indexOf(session.user.name);
|
||||
if (userIndex === -1) {
|
||||
res.status(400).json({
|
||||
message:
|
||||
'User is incorrect. Please, logout to update your session.',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
//2 : control the data
|
||||
if (sendTestApprise !== true) {
|
||||
res.status(422).json({ message: 'Unexpected data' });
|
||||
return;
|
||||
}
|
||||
|
||||
//3 : if there is no service URLs, throw error
|
||||
if (
|
||||
!usersList[userIndex].appriseServices ||
|
||||
usersList[userIndex].appriseServices.length === 0
|
||||
) {
|
||||
res.status(422).json({
|
||||
message:
|
||||
'You must provide at least one Apprise URL to send a test.',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
////4 : Send the notification to services
|
||||
//Build the URLs service list as a single string
|
||||
let appriseServicesURLs = '';
|
||||
for (let service of usersList[userIndex].appriseServices) {
|
||||
appriseServicesURLs = appriseServicesURLs + service + ' ';
|
||||
}
|
||||
//Mode : package
|
||||
if (usersList[userIndex].appriseMode === 'package') {
|
||||
try {
|
||||
//Check if apprise is installed as local package.
|
||||
exec('apprise -V', (error, stderr, stdout) => {
|
||||
if (error) {
|
||||
console.log(
|
||||
`Error when checking if Apprise is a local package : ${error}`
|
||||
);
|
||||
res.status(500).json({
|
||||
message:
|
||||
'Apprise is not installed as local package on your server.',
|
||||
});
|
||||
return;
|
||||
} else {
|
||||
//Send notification via local package.
|
||||
exec(
|
||||
`apprise -v -b "This is a test notification from BorgWarehouse !" ${appriseServicesURLs}`,
|
||||
(error, stderr, stdout) => {
|
||||
if (stderr) {
|
||||
res.status(500).json({
|
||||
message:
|
||||
'There are some errors : ' + stderr,
|
||||
});
|
||||
return;
|
||||
} else {
|
||||
res.status(200).json({
|
||||
message:
|
||||
'Notifications successfully sent.',
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
});
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
res.status(500).json({
|
||||
message:
|
||||
'Error on sending notification. Contact your administrator.',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
//Mode : stateless
|
||||
} else if (usersList[userIndex].appriseMode === 'stateless') {
|
||||
//If stateless URL is empty
|
||||
if (usersList[userIndex].appriseStatelessURL === '') {
|
||||
res.status(500).json({
|
||||
message: 'Please, provide an Apprise stateless API URL.',
|
||||
});
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await fetch(
|
||||
usersList[userIndex].appriseStatelessURL + '/notify',
|
||||
{
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
urls: appriseServicesURLs,
|
||||
body: 'This is a test notification from BorgWarehouse !',
|
||||
}),
|
||||
}
|
||||
).then((response) => {
|
||||
if (response.ok) {
|
||||
res.status(200).json({
|
||||
message: 'Notifications successfully sent.',
|
||||
});
|
||||
return;
|
||||
} else {
|
||||
console.log(response);
|
||||
res.status(500).json({
|
||||
message:
|
||||
'There are some errors : ' +
|
||||
response.statusText,
|
||||
});
|
||||
return;
|
||||
}
|
||||
});
|
||||
} catch (err) {
|
||||
res.status(500).json({
|
||||
message: 'Error : ' + err.message,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
//Mode : unknown
|
||||
} else {
|
||||
res.status(422).json({
|
||||
message: 'No Apprise Mode selected or supported.',
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
48
pages/api/account/sendTestEmail.js
Normal file
48
pages/api/account/sendTestEmail.js
Normal file
|
@ -0,0 +1,48 @@
|
|||
//Lib
|
||||
import { authOptions } from '../auth/[...nextauth]';
|
||||
import { getServerSession } from 'next-auth/next';
|
||||
import nodemailerSMTP from '../../../helpers/functions/nodemailerSMTP';
|
||||
import emailTest from '../../../helpers/templates/emailTest';
|
||||
|
||||
export default async function handler(req, res) {
|
||||
if (req.method == 'POST') {
|
||||
//Verify that the user is logged in.
|
||||
const session = await getServerSession(req, res, authOptions);
|
||||
if (!session) {
|
||||
res.status(401).json({ message: 'You must be logged in.' });
|
||||
return;
|
||||
}
|
||||
|
||||
//Create the SMTP Transporter
|
||||
const transporter = nodemailerSMTP();
|
||||
|
||||
//Mail options
|
||||
const mailData = emailTest(session.user.email, session.user.name);
|
||||
|
||||
//Send mail
|
||||
try {
|
||||
transporter.sendMail(mailData, function (err, info) {
|
||||
if (err) {
|
||||
console.log(err);
|
||||
res.status(400).json({
|
||||
message:
|
||||
'An error occured while sending the email : ' + err,
|
||||
});
|
||||
return;
|
||||
} else {
|
||||
console.log(info);
|
||||
res.status(200).json({
|
||||
message: 'Mail successfully sent.',
|
||||
});
|
||||
return;
|
||||
}
|
||||
});
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
res.status(500).json({
|
||||
status: 500,
|
||||
message: 'API error, contact the administrator.',
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
86
pages/api/account/updateAppriseAlert.js
Normal file
86
pages/api/account/updateAppriseAlert.js
Normal file
|
@ -0,0 +1,86 @@
|
|||
//Lib
|
||||
import { promises as fs } from 'fs';
|
||||
import path from 'path';
|
||||
import { authOptions } from '../auth/[...nextauth]';
|
||||
import { getServerSession } from 'next-auth/next';
|
||||
|
||||
export default async function handler(req, res) {
|
||||
if (req.method == 'PUT') {
|
||||
//Verify that the user is logged in.
|
||||
const session = await getServerSession(req, res, authOptions);
|
||||
if (!session) {
|
||||
res.status(401).json({ message: 'You must be logged in.' });
|
||||
return;
|
||||
}
|
||||
|
||||
//The data we expect to receive
|
||||
let { appriseAlert } = req.body;
|
||||
|
||||
//Read the users file
|
||||
//Find the absolute path of the json directory
|
||||
const jsonDirectory = path.join(process.cwd(), '/config');
|
||||
let usersList = await fs.readFile(
|
||||
jsonDirectory + '/users.json',
|
||||
'utf8'
|
||||
);
|
||||
//Parse the usersList
|
||||
usersList = JSON.parse(usersList);
|
||||
|
||||
//1 : control the data
|
||||
if (typeof appriseAlert != 'boolean') {
|
||||
res.status(422).json({ message: 'Unexpected data' });
|
||||
return;
|
||||
}
|
||||
|
||||
//2 : Verify that the user of the session exists
|
||||
const userIndex = usersList
|
||||
.map((user) => user.username)
|
||||
.indexOf(session.user.name);
|
||||
if (userIndex === -1) {
|
||||
res.status(400).json({
|
||||
message:
|
||||
'User is incorrect. Please, logout to update your session.',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
//3 : Change the appriseAlert settings
|
||||
try {
|
||||
//Modify the appriseAlert bool for the user
|
||||
let newUsersList = usersList.map((user) =>
|
||||
user.username == session.user.name
|
||||
? { ...user, appriseAlert: appriseAlert }
|
||||
: user
|
||||
);
|
||||
//Stringify the new users list
|
||||
newUsersList = JSON.stringify(newUsersList);
|
||||
//Write the new JSON
|
||||
await fs.writeFile(
|
||||
jsonDirectory + '/users.json',
|
||||
newUsersList,
|
||||
(err) => {
|
||||
if (err) console.log(err);
|
||||
}
|
||||
);
|
||||
res.status(200).json({ message: 'Successful API send' });
|
||||
} catch (error) {
|
||||
//Log for backend
|
||||
console.log(error);
|
||||
//Log for frontend
|
||||
if (error.code == 'ENOENT') {
|
||||
res.status(500).json({
|
||||
status: 500,
|
||||
message: 'No such file or directory',
|
||||
});
|
||||
} else {
|
||||
res.status(500).json({
|
||||
status: 500,
|
||||
message: 'API error, contact the administrator',
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
res.status(405).json({ message: 'Bad request on API' });
|
||||
}
|
||||
}
|
90
pages/api/account/updateAppriseMode.js
Normal file
90
pages/api/account/updateAppriseMode.js
Normal file
|
@ -0,0 +1,90 @@
|
|||
//Lib
|
||||
import { promises as fs } from 'fs';
|
||||
import path from 'path';
|
||||
import { authOptions } from '../auth/[...nextauth]';
|
||||
import { getServerSession } from 'next-auth/next';
|
||||
|
||||
export default async function handler(req, res) {
|
||||
if (req.method == 'PUT') {
|
||||
//Verify that the user is logged in.
|
||||
const session = await getServerSession(req, res, authOptions);
|
||||
if (!session) {
|
||||
res.status(401).json({ message: 'You must be logged in.' });
|
||||
return;
|
||||
}
|
||||
|
||||
//The data we expect to receive
|
||||
let { appriseMode, appriseStatelessURL } = req.body;
|
||||
|
||||
//Read the users file
|
||||
//Find the absolute path of the json directory
|
||||
const jsonDirectory = path.join(process.cwd(), '/config');
|
||||
let usersList = await fs.readFile(
|
||||
jsonDirectory + '/users.json',
|
||||
'utf8'
|
||||
);
|
||||
//Parse the usersList
|
||||
usersList = JSON.parse(usersList);
|
||||
|
||||
//1 : control the data
|
||||
if (appriseMode != 'package' && appriseMode != 'stateless') {
|
||||
res.status(422).json({ message: 'Unexpected data' });
|
||||
return;
|
||||
}
|
||||
|
||||
//2 : Verify that the user of the session exists
|
||||
const userIndex = usersList
|
||||
.map((user) => user.username)
|
||||
.indexOf(session.user.name);
|
||||
if (userIndex === -1) {
|
||||
res.status(400).json({
|
||||
message:
|
||||
'User is incorrect. Please, logout to update your session.',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
//3 : Change the appriseMode
|
||||
try {
|
||||
//Modify the appriseMode for the user
|
||||
let newUsersList = usersList.map((user) =>
|
||||
user.username == session.user.name
|
||||
? {
|
||||
...user,
|
||||
appriseMode: appriseMode,
|
||||
appriseStatelessURL: appriseStatelessURL,
|
||||
}
|
||||
: user
|
||||
);
|
||||
//Stringify the new users list
|
||||
newUsersList = JSON.stringify(newUsersList);
|
||||
//Write the new JSON
|
||||
await fs.writeFile(
|
||||
jsonDirectory + '/users.json',
|
||||
newUsersList,
|
||||
(err) => {
|
||||
if (err) console.log(err);
|
||||
}
|
||||
);
|
||||
res.status(200).json({ message: 'Successful API send' });
|
||||
} catch (error) {
|
||||
//Log for backend
|
||||
console.log(error);
|
||||
//Log for frontend
|
||||
if (error.code == 'ENOENT') {
|
||||
res.status(500).json({
|
||||
status: 500,
|
||||
message: 'No such file or directory',
|
||||
});
|
||||
} else {
|
||||
res.status(500).json({
|
||||
status: 500,
|
||||
message: 'API error, contact the administrator',
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
res.status(405).json({ message: 'Bad request on API' });
|
||||
}
|
||||
}
|
89
pages/api/account/updateAppriseServices.js
Normal file
89
pages/api/account/updateAppriseServices.js
Normal file
|
@ -0,0 +1,89 @@
|
|||
//Lib
|
||||
import { promises as fs } from 'fs';
|
||||
import path from 'path';
|
||||
import { authOptions } from '../auth/[...nextauth]';
|
||||
import { getServerSession } from 'next-auth/next';
|
||||
|
||||
export default async function handler(req, res) {
|
||||
if (req.method == 'PUT') {
|
||||
//Verify that the user is logged in.
|
||||
const session = await getServerSession(req, res, authOptions);
|
||||
if (!session) {
|
||||
res.status(401).json({ message: 'You must be logged in.' });
|
||||
return;
|
||||
}
|
||||
|
||||
//The data we expect to receive
|
||||
let { appriseURLs } = req.body;
|
||||
|
||||
//Read the users file
|
||||
//Find the absolute path of the json directory
|
||||
const jsonDirectory = path.join(process.cwd(), '/config');
|
||||
let usersList = await fs.readFile(
|
||||
jsonDirectory + '/users.json',
|
||||
'utf8'
|
||||
);
|
||||
//Parse the usersList
|
||||
usersList = JSON.parse(usersList);
|
||||
|
||||
//1 : Verify that the user of the session exists
|
||||
const userIndex = usersList
|
||||
.map((user) => user.username)
|
||||
.indexOf(session.user.name);
|
||||
if (userIndex === -1) {
|
||||
res.status(400).json({
|
||||
message:
|
||||
'User is incorrect. Please, logout to update your session.',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
//2 : Update Apprise URLs list
|
||||
try {
|
||||
//Build the services URLs list from form
|
||||
const appriseURLsArray = appriseURLs
|
||||
.replace(/ /g, '')
|
||||
.split('\n')
|
||||
.filter((el) => el != '');
|
||||
|
||||
//Save the list for the user
|
||||
let newUsersList = usersList.map((user) =>
|
||||
user.username == session.user.name
|
||||
? {
|
||||
...user,
|
||||
appriseServices: appriseURLsArray,
|
||||
}
|
||||
: user
|
||||
);
|
||||
//Stringify the new users list
|
||||
newUsersList = JSON.stringify(newUsersList);
|
||||
//Write the new JSON
|
||||
await fs.writeFile(
|
||||
jsonDirectory + '/users.json',
|
||||
newUsersList,
|
||||
(err) => {
|
||||
if (err) console.log(err);
|
||||
}
|
||||
);
|
||||
res.status(200).json({ message: 'Successful API send' });
|
||||
} catch (error) {
|
||||
//Log for backend
|
||||
console.log(error);
|
||||
//Log for frontend
|
||||
if (error.code == 'ENOENT') {
|
||||
res.status(500).json({
|
||||
status: 500,
|
||||
message: 'No such file or directory',
|
||||
});
|
||||
} else {
|
||||
res.status(500).json({
|
||||
status: 500,
|
||||
message: 'API error, contact the administrator',
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
res.status(405).json({ message: 'Bad request on API' });
|
||||
}
|
||||
}
|
96
pages/api/account/updateEmail.js
Normal file
96
pages/api/account/updateEmail.js
Normal file
|
@ -0,0 +1,96 @@
|
|||
//Lib
|
||||
import { promises as fs } from 'fs';
|
||||
import path from 'path';
|
||||
import { authOptions } from '../auth/[...nextauth]';
|
||||
import { getServerSession } from 'next-auth/next';
|
||||
|
||||
export default async function handler(req, res) {
|
||||
if (req.method == 'PUT') {
|
||||
//Verify that the user is logged in.
|
||||
const session = await getServerSession(req, res, authOptions);
|
||||
if (!session) {
|
||||
res.status(401).json({ message: 'You must be logged in.' });
|
||||
return;
|
||||
}
|
||||
|
||||
//The data we expect to receive
|
||||
let { email } = req.body;
|
||||
|
||||
//Read the users file
|
||||
//Find the absolute path of the json directory
|
||||
const jsonDirectory = path.join(process.cwd(), '/config');
|
||||
let usersList = await fs.readFile(
|
||||
jsonDirectory + '/users.json',
|
||||
'utf8'
|
||||
);
|
||||
//Parse the usersList
|
||||
usersList = JSON.parse(usersList);
|
||||
|
||||
//1 : We check that we receive data.
|
||||
if (!email) {
|
||||
//If a variable is empty.
|
||||
res.status(400).json({ message: 'A field is missing.' });
|
||||
return;
|
||||
}
|
||||
|
||||
//2 : control the data
|
||||
const emailRegex = new RegExp(
|
||||
/^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/
|
||||
);
|
||||
if (!emailRegex.test(email)) {
|
||||
res.status(400).json({ message: 'Your email is not valid' });
|
||||
return;
|
||||
}
|
||||
|
||||
//3 : Verify that the user of the session exists
|
||||
const userIndex = usersList
|
||||
.map((user) => user.username)
|
||||
.indexOf(session.user.name);
|
||||
if (userIndex === -1) {
|
||||
res.status(400).json({
|
||||
message:
|
||||
'User is incorrect. Please, logout to update your session.',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
//4 : Change the email
|
||||
try {
|
||||
//Modify the email for the user
|
||||
let newUsersList = usersList.map((user) =>
|
||||
user.username == session.user.name
|
||||
? { ...user, email: email }
|
||||
: user
|
||||
);
|
||||
//Stringify the new users list
|
||||
newUsersList = JSON.stringify(newUsersList);
|
||||
//Write the new JSON
|
||||
await fs.writeFile(
|
||||
jsonDirectory + '/users.json',
|
||||
newUsersList,
|
||||
(err) => {
|
||||
if (err) console.log(err);
|
||||
}
|
||||
);
|
||||
res.status(200).json({ message: 'Successful API send' });
|
||||
} catch (error) {
|
||||
//Log for backend
|
||||
console.log(error);
|
||||
//Log for frontend
|
||||
if (error.code == 'ENOENT') {
|
||||
res.status(500).json({
|
||||
status: 500,
|
||||
message: 'No such file or directory',
|
||||
});
|
||||
} else {
|
||||
res.status(500).json({
|
||||
status: 500,
|
||||
message: 'API error, contact the administrator',
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
res.status(405).json({ message: 'Bad request on API' });
|
||||
}
|
||||
}
|
86
pages/api/account/updateEmailAlert.js
Normal file
86
pages/api/account/updateEmailAlert.js
Normal file
|
@ -0,0 +1,86 @@
|
|||
//Lib
|
||||
import { promises as fs } from 'fs';
|
||||
import path from 'path';
|
||||
import { authOptions } from '../auth/[...nextauth]';
|
||||
import { getServerSession } from 'next-auth/next';
|
||||
|
||||
export default async function handler(req, res) {
|
||||
if (req.method == 'PUT') {
|
||||
//Verify that the user is logged in.
|
||||
const session = await getServerSession(req, res, authOptions);
|
||||
if (!session) {
|
||||
res.status(401).json({ message: 'You must be logged in.' });
|
||||
return;
|
||||
}
|
||||
|
||||
//The data we expect to receive
|
||||
let { emailAlert } = req.body;
|
||||
|
||||
//Read the users file
|
||||
//Find the absolute path of the json directory
|
||||
const jsonDirectory = path.join(process.cwd(), '/config');
|
||||
let usersList = await fs.readFile(
|
||||
jsonDirectory + '/users.json',
|
||||
'utf8'
|
||||
);
|
||||
//Parse the usersList
|
||||
usersList = JSON.parse(usersList);
|
||||
|
||||
//1 : control the data
|
||||
if (typeof emailAlert != 'boolean') {
|
||||
res.status(422).json({ message: 'Unexpected data' });
|
||||
return;
|
||||
}
|
||||
|
||||
//2 : Verify that the user of the session exists
|
||||
const userIndex = usersList
|
||||
.map((user) => user.username)
|
||||
.indexOf(session.user.name);
|
||||
if (userIndex === -1) {
|
||||
res.status(400).json({
|
||||
message:
|
||||
'User is incorrect. Please, logout to update your session.',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
//3 : Change the emailAlert settings
|
||||
try {
|
||||
//Modify the email for the user
|
||||
let newUsersList = usersList.map((user) =>
|
||||
user.username == session.user.name
|
||||
? { ...user, emailAlert: emailAlert }
|
||||
: user
|
||||
);
|
||||
//Stringify the new users list
|
||||
newUsersList = JSON.stringify(newUsersList);
|
||||
//Write the new JSON
|
||||
await fs.writeFile(
|
||||
jsonDirectory + '/users.json',
|
||||
newUsersList,
|
||||
(err) => {
|
||||
if (err) console.log(err);
|
||||
}
|
||||
);
|
||||
res.status(200).json({ message: 'Successful API send' });
|
||||
} catch (error) {
|
||||
//Log for backend
|
||||
console.log(error);
|
||||
//Log for frontend
|
||||
if (error.code == 'ENOENT') {
|
||||
res.status(500).json({
|
||||
status: 500,
|
||||
message: 'No such file or directory',
|
||||
});
|
||||
} else {
|
||||
res.status(500).json({
|
||||
status: 500,
|
||||
message: 'API error, contact the administrator',
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
res.status(405).json({ message: 'Bad request on API' });
|
||||
}
|
||||
}
|
|
@ -2,13 +2,13 @@
|
|||
import { hashPassword, verifyPassword } from '../../../helpers/functions/auth';
|
||||
import { promises as fs } from 'fs';
|
||||
import path from 'path';
|
||||
import { authOptions } from '../../../pages/api/auth/[...nextauth]';
|
||||
import { unstable_getServerSession } from 'next-auth/next';
|
||||
import { authOptions } from '../auth/[...nextauth]';
|
||||
import { getServerSession } from 'next-auth/next';
|
||||
|
||||
export default async function handler(req, res) {
|
||||
if (req.method == 'PUT') {
|
||||
//Verify that the user is logged in.
|
||||
const session = await unstable_getServerSession(req, res, authOptions);
|
||||
const session = await getServerSession(req, res, authOptions);
|
||||
if (!session) {
|
||||
res.status(401).json({ message: 'You must be logged in.' });
|
||||
return;
|
||||
|
@ -64,9 +64,13 @@ export default async function handler(req, res) {
|
|||
//Stringify the new users list
|
||||
newUsersList = JSON.stringify(newUsersList);
|
||||
//Write the new JSON
|
||||
fs.writeFile(jsonDirectory + '/users.json', newUsersList, (err) => {
|
||||
if (err) console.log(err);
|
||||
});
|
||||
await fs.writeFile(
|
||||
jsonDirectory + '/users.json',
|
||||
newUsersList,
|
||||
(err) => {
|
||||
if (err) console.log(err);
|
||||
}
|
||||
);
|
||||
res.status(200).json({ message: 'Successful API send' });
|
||||
} catch (error) {
|
||||
//Log for backend
|
||||
|
|
96
pages/api/account/updateUsername.js
Normal file
96
pages/api/account/updateUsername.js
Normal file
|
@ -0,0 +1,96 @@
|
|||
//Lib
|
||||
import { promises as fs } from 'fs';
|
||||
import path from 'path';
|
||||
import { authOptions } from '../auth/[...nextauth]';
|
||||
import { getServerSession } from 'next-auth/next';
|
||||
|
||||
export default async function handler(req, res) {
|
||||
if (req.method == 'PUT') {
|
||||
//Verify that the user is logged in.
|
||||
const session = await getServerSession(req, res, authOptions);
|
||||
if (!session) {
|
||||
res.status(401).json({ message: 'You must be logged in.' });
|
||||
return;
|
||||
}
|
||||
|
||||
//The data we expect to receive
|
||||
let { username } = req.body;
|
||||
|
||||
//Read the users file
|
||||
//Find the absolute path of the json directory
|
||||
const jsonDirectory = path.join(process.cwd(), '/config');
|
||||
let usersList = await fs.readFile(
|
||||
jsonDirectory + '/users.json',
|
||||
'utf8'
|
||||
);
|
||||
//Parse the usersList
|
||||
usersList = JSON.parse(usersList);
|
||||
|
||||
//1 : We check that we receive data.
|
||||
if (!username) {
|
||||
//If a variable is empty.
|
||||
res.status(400).json({ message: 'A field is missing.' });
|
||||
return;
|
||||
}
|
||||
|
||||
//2 : control the data
|
||||
const usernameRegex = new RegExp(/^[a-z]{5,15}$/);
|
||||
if (!usernameRegex.test(username)) {
|
||||
res.status(400).json({
|
||||
message: 'Only a-z characters are allowed (5 to 15 char.)',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
//3 : Verify that the user of the session exists
|
||||
const userIndex = usersList
|
||||
.map((user) => user.username)
|
||||
.indexOf(session.user.name);
|
||||
if (userIndex === -1) {
|
||||
res.status(400).json({
|
||||
message:
|
||||
'User is incorrect. Please, logout to update your session.',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
//4 : Change the username
|
||||
try {
|
||||
//Modify the username for the user
|
||||
let newUsersList = usersList.map((user) =>
|
||||
user.username == session.user.name
|
||||
? { ...user, username: username }
|
||||
: user
|
||||
);
|
||||
//Stringify the new users list
|
||||
newUsersList = JSON.stringify(newUsersList);
|
||||
//Write the new JSON
|
||||
await fs.writeFile(
|
||||
jsonDirectory + '/users.json',
|
||||
newUsersList,
|
||||
(err) => {
|
||||
if (err) console.log(err);
|
||||
}
|
||||
);
|
||||
res.status(200).json({ message: 'Successful API send' });
|
||||
} catch (error) {
|
||||
//Log for backend
|
||||
console.log(error);
|
||||
//Log for frontend
|
||||
if (error.code == 'ENOENT') {
|
||||
res.status(500).json({
|
||||
status: 500,
|
||||
message: 'No such file or directory',
|
||||
});
|
||||
} else {
|
||||
res.status(500).json({
|
||||
status: 500,
|
||||
message: 'API error, contact the administrator',
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
res.status(405).json({ message: 'Bad request on API' });
|
||||
}
|
||||
}
|
|
@ -5,11 +5,20 @@ import { verifyPassword } from '../../../helpers/functions/auth';
|
|||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
|
||||
////Use if need getServerSideProps and therefore unstable_getServerSession
|
||||
const logLogin = async (message, req, success = false) => {
|
||||
const ipAddress = req.headers['x-forwarded-for'] || 'unknown';
|
||||
if (success) {
|
||||
console.log(`Login success from ${ipAddress} with user ${message}`);
|
||||
} else {
|
||||
console.log(`Login failed from ${ipAddress} : ${message}`);
|
||||
}
|
||||
};
|
||||
|
||||
////Use if need getServerSideProps and therefore getServerSession
|
||||
export const authOptions = {
|
||||
providers: [
|
||||
CredentialsProvider({
|
||||
async authorize(credentials) {
|
||||
async authorize(credentials, req) {
|
||||
const { username, password } = credentials;
|
||||
//Read the users file
|
||||
//Find the absolute path of the json directory
|
||||
|
@ -21,11 +30,13 @@ export const authOptions = {
|
|||
JSON.stringify([
|
||||
{
|
||||
id: 0,
|
||||
email: 'admin@demo',
|
||||
email: '',
|
||||
username: 'admin',
|
||||
password:
|
||||
'$2a$12$20yqRnuaDBH6AE0EvIUcEOzqkuBtn1wDzJdw2Beg8w9S.vEqdso0a',
|
||||
roles: ['admin'],
|
||||
emailAlert: false,
|
||||
appriseAlert: false,
|
||||
},
|
||||
])
|
||||
);
|
||||
|
@ -40,8 +51,9 @@ export const authOptions = {
|
|||
//Step 1 : does the user exist ?
|
||||
const userIndex = usersList
|
||||
.map((user) => user.username)
|
||||
.indexOf(username);
|
||||
.indexOf(username.toLowerCase());
|
||||
if (userIndex === -1) {
|
||||
await logLogin(`Bad username ${req.body.username}`, req);
|
||||
throw new Error('Incorrect credentials.');
|
||||
}
|
||||
const user = usersList[userIndex];
|
||||
|
@ -49,6 +61,10 @@ export const authOptions = {
|
|||
//Step 2 : Is the password correct ?
|
||||
const isValid = await verifyPassword(password, user.password);
|
||||
if (!isValid) {
|
||||
await logLogin(
|
||||
`Wrong password for ${req.body.username}`,
|
||||
req
|
||||
);
|
||||
throw new Error('Incorrect credentials.');
|
||||
}
|
||||
|
||||
|
@ -60,6 +76,7 @@ export const authOptions = {
|
|||
roles: user.roles,
|
||||
};
|
||||
|
||||
await logLogin(req.body.username, req, true);
|
||||
return account;
|
||||
},
|
||||
}),
|
||||
|
|
|
@ -6,6 +6,8 @@ import { promises as fs } from 'fs';
|
|||
import path from 'path';
|
||||
const util = require('node:util');
|
||||
const exec = util.promisify(require('node:child_process').exec);
|
||||
import nodemailerSMTP from '../../../helpers/functions/nodemailerSMTP';
|
||||
import emailAlertStatus from '../../../helpers/templates/emailAlertStatus';
|
||||
|
||||
export default async function handler(req, res) {
|
||||
if (req.headers.authorization == null) {
|
||||
|
@ -19,9 +21,31 @@ export default async function handler(req, res) {
|
|||
const CRONJOB_KEY = process.env.CRONJOB_KEY;
|
||||
const ACTION_KEY = req.headers.authorization.split(' ')[1];
|
||||
|
||||
try {
|
||||
if (req.method == 'POST' && ACTION_KEY === CRONJOB_KEY) {
|
||||
////Call the shell : getStorageUsed.sh
|
||||
if (req.method == 'POST' && ACTION_KEY === CRONJOB_KEY) {
|
||||
//Var
|
||||
let newRepoList;
|
||||
let repoListToSendAlert = [];
|
||||
let usersList;
|
||||
const date = Math.round(Date.now() / 1000);
|
||||
const jsonDirectory = path.join(process.cwd(), '/config');
|
||||
|
||||
////PART 1 : Status
|
||||
try {
|
||||
//Check if there are some repositories
|
||||
let repoList = await fs.readFile(
|
||||
jsonDirectory + '/repo.json',
|
||||
'utf8'
|
||||
);
|
||||
repoList = JSON.parse(repoList);
|
||||
if (repoList.length === 0) {
|
||||
res.status(200).json({
|
||||
success:
|
||||
'Status cron has been executed. No repository to check.',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
//Call the shell : getLastSave.sh
|
||||
//Find the absolute path of the shells directory
|
||||
const shellsDirectory = path.join(process.cwd(), '/helpers');
|
||||
//Exec the shell
|
||||
|
@ -40,21 +64,12 @@ export default async function handler(req, res) {
|
|||
//Parse the JSON output of getLastSave.sh to use it
|
||||
const lastSave = JSON.parse(stdout);
|
||||
|
||||
//Find the absolute path of the json directory
|
||||
const jsonDirectory = path.join(process.cwd(), '/config');
|
||||
let repoList = await fs.readFile(
|
||||
jsonDirectory + '/repo.json',
|
||||
'utf8'
|
||||
);
|
||||
//Parse the repoList
|
||||
repoList = JSON.parse(repoList);
|
||||
|
||||
//Rebuild a newRepoList with the lasSave timestamp updated
|
||||
const date = Math.round(Date.now() / 1000);
|
||||
let newRepoList = repoList;
|
||||
//Rebuild a newRepoList with the lastSave timestamp updated and the status updated.
|
||||
newRepoList = repoList;
|
||||
for (let index in newRepoList) {
|
||||
const repoFiltered = lastSave.filter(
|
||||
(x) => x.user === newRepoList[index].unixUser
|
||||
(x) =>
|
||||
x.repositoryName === newRepoList[index].repositoryName
|
||||
);
|
||||
if (repoFiltered.length === 1) {
|
||||
//Write the timestamp of the last save
|
||||
|
@ -73,28 +88,161 @@ export default async function handler(req, res) {
|
|||
}
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
res.status(500).json({
|
||||
status: 500,
|
||||
message: "API error : can't update the status.",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
//// PART 2 : check if there is a repo that need an alert
|
||||
try {
|
||||
//Here, a mail is sent every 24H (90000) if a repo has down status
|
||||
for (let index in newRepoList) {
|
||||
if (
|
||||
!newRepoList[index].status &&
|
||||
newRepoList[index].alert !== 0 &&
|
||||
(!newRepoList[index].lastStatusAlertSend ||
|
||||
date - newRepoList[index].lastStatusAlertSend > 90000)
|
||||
) {
|
||||
repoListToSendAlert.push(newRepoList[index].alias);
|
||||
newRepoList[index].lastStatusAlertSend = date;
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
res.status(500).json({
|
||||
status: 500,
|
||||
message:
|
||||
"API error : can't check if a repo needs an email alert.",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
//PART 3 : Save the new repoList
|
||||
try {
|
||||
//Stringify the repoList to write it into the json file.
|
||||
newRepoList = JSON.stringify(newRepoList);
|
||||
//Write the new json
|
||||
fs.writeFile(jsonDirectory + '/repo.json', newRepoList, (err) => {
|
||||
if (err) console.log(err);
|
||||
});
|
||||
|
||||
res.status(200).json({
|
||||
success: 'Status cron has been executed.',
|
||||
});
|
||||
} else {
|
||||
res.status(401).json({
|
||||
status: 401,
|
||||
message: 'Unauthorized',
|
||||
await fs.writeFile(
|
||||
jsonDirectory + '/repo.json',
|
||||
newRepoList,
|
||||
(err) => {
|
||||
if (err) console.log(err);
|
||||
}
|
||||
);
|
||||
} catch (err) {
|
||||
res.status(500).json({
|
||||
status: 500,
|
||||
message: "API error : can't write the new repoList.",
|
||||
});
|
||||
return;
|
||||
}
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
res.status(500).json({
|
||||
status: 500,
|
||||
message: 'API error, contact the administrator.',
|
||||
//PART 4 : Send the alerts
|
||||
if (repoListToSendAlert.length > 0) {
|
||||
// Read user informations
|
||||
try {
|
||||
//Read the email of the user
|
||||
usersList = await fs.readFile(
|
||||
jsonDirectory + '/users.json',
|
||||
'utf8'
|
||||
);
|
||||
//Parse the usersList
|
||||
usersList = JSON.parse(usersList);
|
||||
} catch (err) {
|
||||
res.status(500).json({
|
||||
status: 500,
|
||||
message: "API error : can't read user information.",
|
||||
});
|
||||
return;
|
||||
}
|
||||
////EMAIL
|
||||
// If the user has enabled email alerts
|
||||
if (usersList[0].emailAlert) {
|
||||
//Send mail
|
||||
//Create the SMTP Transporter
|
||||
const transporter = nodemailerSMTP();
|
||||
//Mail options
|
||||
const mailData = emailAlertStatus(
|
||||
usersList[0].email,
|
||||
usersList[0].username,
|
||||
repoListToSendAlert
|
||||
);
|
||||
transporter.sendMail(mailData, function (err, info) {
|
||||
if (err) {
|
||||
console.log(err);
|
||||
} else {
|
||||
console.log(info);
|
||||
}
|
||||
});
|
||||
}
|
||||
////APPRISE
|
||||
// If the user has enabled Apprise alerts
|
||||
if (usersList[0].appriseAlert) {
|
||||
let appriseServicesURLs = '';
|
||||
for (let service of usersList[0].appriseServices) {
|
||||
appriseServicesURLs = appriseServicesURLs + service + ' ';
|
||||
}
|
||||
//Mode : package
|
||||
if (usersList[0].appriseMode === 'package') {
|
||||
try {
|
||||
//Send notification via local package.
|
||||
await exec(
|
||||
`apprise -v -b '🔴 Some repositories on BorgWarehouse need attention !\nList of down repositories :\n ${repoListToSendAlert}' ${appriseServicesURLs}`
|
||||
);
|
||||
} catch (err) {
|
||||
console.log(err.stderr);
|
||||
res.status(500).json({
|
||||
message: 'Error : ' + err.stderr,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
//Mode : stateless
|
||||
} else if (usersList[0].appriseMode === 'stateless') {
|
||||
try {
|
||||
await fetch(
|
||||
usersList[0].appriseStatelessURL + '/notify',
|
||||
{
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
urls: appriseServicesURLs,
|
||||
body:
|
||||
'🔴 Some repositories on BorgWarehouse need attention !\nList of down repositories :\n' +
|
||||
repoListToSendAlert,
|
||||
}),
|
||||
}
|
||||
);
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
res.status(500).json({
|
||||
message: 'Error : ' + err.message,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
//Mode : unknown
|
||||
} else {
|
||||
res.status(422).json({
|
||||
message: 'No Apprise Mode selected or supported.',
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//PART 5 : Sucess
|
||||
res.status(200).json({
|
||||
success: 'Status cron has been executed.',
|
||||
});
|
||||
return;
|
||||
} else {
|
||||
res.status(401).json({
|
||||
status: 401,
|
||||
message: 'Unauthorized',
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -59,7 +59,7 @@ export default async function handler(req, res) {
|
|||
let newRepoList = repoList;
|
||||
for (let index in newRepoList) {
|
||||
const repoFiltered = storageUsed.filter(
|
||||
(x) => x.name === newRepoList[index].unixUser
|
||||
(x) => x.name === newRepoList[index].repositoryName
|
||||
);
|
||||
if (repoFiltered.length === 1) {
|
||||
newRepoList[index].storageUsed = repoFiltered[0].size;
|
||||
|
@ -69,9 +69,13 @@ export default async function handler(req, res) {
|
|||
//Stringify the repoList to write it into the json file.
|
||||
newRepoList = JSON.stringify(newRepoList);
|
||||
//Write the new json
|
||||
fs.writeFile(jsonDirectory + '/repo.json', newRepoList, (err) => {
|
||||
if (err) console.log(err);
|
||||
});
|
||||
await fs.writeFile(
|
||||
jsonDirectory + '/repo.json',
|
||||
newRepoList,
|
||||
(err) => {
|
||||
if (err) console.log(err);
|
||||
}
|
||||
);
|
||||
|
||||
res.status(200).json({
|
||||
success: 'Storage cron has been executed.',
|
||||
|
|
|
@ -1,23 +1,38 @@
|
|||
import { promises as fs } from 'fs';
|
||||
import path from 'path';
|
||||
import { authOptions } from '../../../pages/api/auth/[...nextauth]';
|
||||
import { unstable_getServerSession } from 'next-auth/next';
|
||||
import { getServerSession } from 'next-auth/next';
|
||||
import repoHistory from '../../../helpers/functions/repoHistory';
|
||||
const util = require('node:util');
|
||||
const exec = util.promisify(require('node:child_process').exec);
|
||||
|
||||
export default async function handler(req, res) {
|
||||
if (req.method == 'POST') {
|
||||
//Verify that the user is logged in.
|
||||
const session = await unstable_getServerSession(req, res, authOptions);
|
||||
const session = await getServerSession(req, res, authOptions);
|
||||
if (!session) {
|
||||
res.status(401).json({ message: 'You must be logged in.' });
|
||||
return;
|
||||
}
|
||||
|
||||
//The data we expect to receive
|
||||
const { alias, sshPublicKey, size, comment, alert } = req.body;
|
||||
//We check that we receive data for each variable. Only "comment" is optional in the form.
|
||||
if (!alias || !sshPublicKey || !size || !alert) {
|
||||
const {
|
||||
alias,
|
||||
sshPublicKey,
|
||||
size,
|
||||
comment,
|
||||
alert,
|
||||
lanCommand,
|
||||
appendOnlyMode,
|
||||
} = req.body;
|
||||
//We check that we receive data for each variable. Only "comment" and "lanCommand" are optional in the form.
|
||||
if (
|
||||
!alias ||
|
||||
!sshPublicKey ||
|
||||
!size ||
|
||||
typeof appendOnlyMode !== 'boolean' ||
|
||||
(!alert && alert !== 0)
|
||||
) {
|
||||
//If a variable is empty.
|
||||
res.status(422).json({
|
||||
message: 'Unexpected data',
|
||||
|
@ -48,7 +63,7 @@ export default async function handler(req, res) {
|
|||
const newRepo = {
|
||||
id: newID,
|
||||
alias: alias,
|
||||
repository: 'repo' + newID,
|
||||
repositoryName: '',
|
||||
status: false,
|
||||
lastSave: 0,
|
||||
alert: alert,
|
||||
|
@ -57,36 +72,37 @@ export default async function handler(req, res) {
|
|||
sshPublicKey: sshPublicKey,
|
||||
comment: comment,
|
||||
displayDetails: true,
|
||||
unixUser: '',
|
||||
lanCommand: lanCommand,
|
||||
appendOnlyMode: appendOnlyMode,
|
||||
};
|
||||
|
||||
////Call the shell : createRepo.sh
|
||||
//Find the absolute path of the shells directory
|
||||
const shellsDirectory = path.join(process.cwd(), '/helpers');
|
||||
//Exec the shell
|
||||
const { stdout, stderr } = await exec(
|
||||
`${shellsDirectory}/shells/createRepo.sh ${newRepo.repository} "${newRepo.sshPublicKey}" ${newRepo.storageSize}`
|
||||
const { stdout } = await exec(
|
||||
`${shellsDirectory}/shells/createRepo.sh "${newRepo.sshPublicKey}" ${newRepo.storageSize} ${newRepo.appendOnlyMode}`
|
||||
);
|
||||
if (stderr) {
|
||||
console.log('stderr:', stderr);
|
||||
res.status(500).json({
|
||||
status: 500,
|
||||
message: 'Error on creation, contact the administrator.',
|
||||
});
|
||||
return;
|
||||
}
|
||||
newRepo.unixUser = stdout.trim();
|
||||
|
||||
newRepo.repositoryName = stdout.trim();
|
||||
|
||||
//Create the new repoList with the new repo
|
||||
let newRepoList = [newRepo, ...repoList];
|
||||
|
||||
//History the new repoList
|
||||
await repoHistory(newRepoList);
|
||||
|
||||
//Stringify the newRepoList to write it into the json file.
|
||||
newRepoList = JSON.stringify(newRepoList);
|
||||
|
||||
//Write the new json
|
||||
fs.writeFile(jsonDirectory + '/repo.json', newRepoList, (err) => {
|
||||
if (err) console.log(err);
|
||||
});
|
||||
await fs.writeFile(
|
||||
jsonDirectory + '/repo.json',
|
||||
newRepoList,
|
||||
(err) => {
|
||||
if (err) console.log(err);
|
||||
}
|
||||
);
|
||||
res.status(200).json({ message: 'Envoi API réussi' });
|
||||
} catch (error) {
|
||||
//Log for backend
|
||||
|
@ -100,7 +116,7 @@ export default async function handler(req, res) {
|
|||
} else {
|
||||
res.status(500).json({
|
||||
status: 500,
|
||||
message: 'API error, contact the administrator',
|
||||
message: error.stdout,
|
||||
});
|
||||
}
|
||||
return;
|
||||
|
|
|
@ -1,14 +1,15 @@
|
|||
import { promises as fs } from 'fs';
|
||||
import path from 'path';
|
||||
import { authOptions } from '../../../auth/[...nextauth]';
|
||||
import { unstable_getServerSession } from 'next-auth/next';
|
||||
import { getServerSession } from 'next-auth/next';
|
||||
import repoHistory from '../../../../../helpers/functions/repoHistory';
|
||||
const util = require('node:util');
|
||||
const exec = util.promisify(require('node:child_process').exec);
|
||||
|
||||
export default async function handler(req, res) {
|
||||
if (req.method == 'DELETE') {
|
||||
//Verify that the user is logged in.
|
||||
const session = await unstable_getServerSession(req, res, authOptions);
|
||||
const session = await getServerSession(req, res, authOptions);
|
||||
if (!session) {
|
||||
res.status(401).json({ message: 'You must be logged in.' });
|
||||
return;
|
||||
|
@ -46,11 +47,10 @@ export default async function handler(req, res) {
|
|||
//Find the absolute path of the shells directory
|
||||
const shellsDirectory = path.join(process.cwd(), '/helpers');
|
||||
//Exec the shell
|
||||
const { stderr } = await exec(
|
||||
`${shellsDirectory}/shells/deleteRepo.sh ${repoList[indexToDelete].unixUser}`
|
||||
const { stdout, stderr } = await exec(
|
||||
`${shellsDirectory}/shells/deleteRepo.sh ${repoList[indexToDelete].repositoryName}`
|
||||
);
|
||||
//Ignore this normal error with the command userdel in the shell : "userdel: USERXXX mail spool (/var/mail/USERXXX) not found".
|
||||
if (stderr && !stderr.includes('mail spool')) {
|
||||
if (stderr) {
|
||||
console.log('stderr:', stderr);
|
||||
res.status(500).json({
|
||||
status: 500,
|
||||
|
@ -70,13 +70,19 @@ export default async function handler(req, res) {
|
|||
});
|
||||
return;
|
||||
}
|
||||
|
||||
//History the repoList
|
||||
await repoHistory(repoList);
|
||||
//Stringify the repoList to write it into the json file.
|
||||
repoList = JSON.stringify(repoList);
|
||||
//Write the new json
|
||||
fs.writeFile(jsonDirectory + '/repo.json', repoList, (err) => {
|
||||
if (err) console.log(err);
|
||||
});
|
||||
await fs.writeFile(
|
||||
jsonDirectory + '/repo.json',
|
||||
repoList,
|
||||
(err) => {
|
||||
if (err) console.log(err);
|
||||
}
|
||||
);
|
||||
|
||||
res.status(200).json({ message: 'Envoi API réussi' });
|
||||
} catch (error) {
|
||||
//Log for backend
|
||||
|
|
|
@ -1,76 +0,0 @@
|
|||
import { promises as fs } from 'fs';
|
||||
import path from 'path';
|
||||
import { authOptions } from '../../../auth/[...nextauth]';
|
||||
import { unstable_getServerSession } from 'next-auth/next';
|
||||
|
||||
export default async function handler(req, res) {
|
||||
if (req.method == 'PUT') {
|
||||
//Verify that the user is logged in.
|
||||
const session = await unstable_getServerSession(req, res, authOptions);
|
||||
if (!session) {
|
||||
res.status(401).json({ message: 'You must be logged in.' });
|
||||
return;
|
||||
}
|
||||
|
||||
//The data we expect to receive
|
||||
const { displayDetails } = req.body;
|
||||
//We check that we receive displayDetails and it must be a bool.
|
||||
if (typeof displayDetails != 'boolean') {
|
||||
//If a variable is empty.
|
||||
res.status(422).json({
|
||||
message: 'Unexpected data',
|
||||
});
|
||||
//A return to make sure we don't go any further if data are incorrect.
|
||||
return;
|
||||
}
|
||||
try {
|
||||
//console.log('API call (PUT)');
|
||||
//Find the absolute path of the json directory
|
||||
const jsonDirectory = path.join(process.cwd(), '/config');
|
||||
let repoList = await fs.readFile(
|
||||
jsonDirectory + '/repo.json',
|
||||
'utf8'
|
||||
);
|
||||
//Parse the repoList
|
||||
repoList = JSON.parse(repoList);
|
||||
|
||||
//Find the ID in the data and change the values transmitted by the form
|
||||
let newRepoList = repoList.map((repo) =>
|
||||
repo.id == req.query.slug
|
||||
? {
|
||||
...repo,
|
||||
displayDetails: displayDetails,
|
||||
}
|
||||
: repo
|
||||
);
|
||||
//Stringify the newRepoList to write it into the json file.
|
||||
newRepoList = JSON.stringify(newRepoList);
|
||||
//Write the new json
|
||||
fs.writeFile(jsonDirectory + '/repo.json', newRepoList, (err) => {
|
||||
if (err) console.log(err);
|
||||
});
|
||||
res.status(200).json({ message: 'Envoi API réussi' });
|
||||
} catch (error) {
|
||||
//Log for backend
|
||||
console.log(error);
|
||||
//Log for frontend
|
||||
if (error.code == 'ENOENT') {
|
||||
res.status(500).json({
|
||||
status: 500,
|
||||
message: 'No such file or directory',
|
||||
});
|
||||
} else {
|
||||
res.status(500).json({
|
||||
status: 500,
|
||||
message: 'API error, contact the administrator',
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
res.status(405).json({
|
||||
status: 405,
|
||||
message: 'Method Not Allowed ',
|
||||
});
|
||||
}
|
||||
}
|
|
@ -1,33 +1,45 @@
|
|||
import { promises as fs } from 'fs';
|
||||
import path from 'path';
|
||||
import { authOptions } from '../../../auth/[...nextauth]';
|
||||
import { unstable_getServerSession } from 'next-auth/next';
|
||||
import { getServerSession } from 'next-auth/next';
|
||||
import repoHistory from '../../../../../helpers/functions/repoHistory';
|
||||
const util = require('node:util');
|
||||
const exec = util.promisify(require('node:child_process').exec);
|
||||
|
||||
export default async function handler(req, res) {
|
||||
if (req.method == 'PUT') {
|
||||
//Verify that the user is logged in.
|
||||
const session = await unstable_getServerSession(req, res, authOptions);
|
||||
const session = await getServerSession(req, res, authOptions);
|
||||
if (!session) {
|
||||
res.status(401).json({ message: 'You must be logged in.' });
|
||||
return;
|
||||
}
|
||||
|
||||
//The data we expect to receive
|
||||
const { alias, sshPublicKey, size, comment, alert } = req.body;
|
||||
//We check that we receive data for each variable. Only "comment" is optional in the form.
|
||||
if (!alias || !sshPublicKey || !size || !alert) {
|
||||
//If a variable is empty.
|
||||
const {
|
||||
alias,
|
||||
sshPublicKey,
|
||||
size,
|
||||
comment,
|
||||
alert,
|
||||
lanCommand,
|
||||
appendOnlyMode,
|
||||
} = req.body;
|
||||
//Only "comment" and "lanCommand" are optional in the form.
|
||||
if (
|
||||
!alias ||
|
||||
!sshPublicKey ||
|
||||
!size ||
|
||||
typeof appendOnlyMode !== 'boolean' ||
|
||||
(!alert && alert !== 0)
|
||||
) {
|
||||
res.status(422).json({
|
||||
message: 'Unexpected data',
|
||||
});
|
||||
//A return to make sure we don't go any further if data are incorrect.
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
//console.log('API call (PUT)');
|
||||
//Find the absolute path of the json directory
|
||||
const jsonDirectory = path.join(process.cwd(), '/config');
|
||||
let repoList = await fs.readFile(
|
||||
|
@ -47,17 +59,9 @@ export default async function handler(req, res) {
|
|||
//Find the absolute path of the shells directory
|
||||
const shellsDirectory = path.join(process.cwd(), '/helpers');
|
||||
// //Exec the shell
|
||||
const { stderr } = await exec(
|
||||
`${shellsDirectory}/shells/updateRepo.sh ${repoList[repoIndex].unixUser} "${sshPublicKey}" ${size}`
|
||||
await exec(
|
||||
`${shellsDirectory}/shells/updateRepo.sh ${repoList[repoIndex].repositoryName} "${sshPublicKey}" ${size} ${appendOnlyMode}`
|
||||
);
|
||||
if (stderr) {
|
||||
console.log('stderr:', stderr);
|
||||
res.status(500).json({
|
||||
status: 500,
|
||||
message: 'Error on update, contact the administrator.',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
//Find the ID in the data and change the values transmitted by the form
|
||||
let newRepoList = repoList.map((repo) =>
|
||||
|
@ -66,18 +70,27 @@ export default async function handler(req, res) {
|
|||
...repo,
|
||||
alias: alias,
|
||||
sshPublicKey: sshPublicKey,
|
||||
storageSize: size,
|
||||
storageSize: Number(size),
|
||||
comment: comment,
|
||||
alert: alert,
|
||||
lanCommand: lanCommand,
|
||||
appendOnlyMode: appendOnlyMode,
|
||||
}
|
||||
: repo
|
||||
);
|
||||
//History the new repoList
|
||||
await repoHistory(newRepoList);
|
||||
//Stringify the newRepoList to write it into the json file.
|
||||
newRepoList = JSON.stringify(newRepoList);
|
||||
//Write the new json
|
||||
fs.writeFile(jsonDirectory + '/repo.json', newRepoList, (err) => {
|
||||
if (err) console.log(err);
|
||||
});
|
||||
await fs.writeFile(
|
||||
jsonDirectory + '/repo.json',
|
||||
newRepoList,
|
||||
(err) => {
|
||||
if (err) console.log(err);
|
||||
}
|
||||
);
|
||||
|
||||
res.status(200).json({ message: 'Envoi API réussi' });
|
||||
} catch (error) {
|
||||
//Log for backend
|
||||
|
@ -91,7 +104,7 @@ export default async function handler(req, res) {
|
|||
} else {
|
||||
res.status(500).json({
|
||||
status: 500,
|
||||
message: 'API error, contact the administrator',
|
||||
message: error.stdout,
|
||||
});
|
||||
}
|
||||
return;
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
import { promises as fs } from 'fs';
|
||||
import path from 'path';
|
||||
import { authOptions } from '../../../auth/[...nextauth]';
|
||||
import { unstable_getServerSession } from 'next-auth/next';
|
||||
import { getServerSession } from 'next-auth/next';
|
||||
|
||||
export default async function handler(req, res) {
|
||||
if (req.method == 'GET') {
|
||||
//Verify that the user is logged in.
|
||||
const session = await unstable_getServerSession(req, res, authOptions);
|
||||
const session = await getServerSession(req, res, authOptions);
|
||||
if (!session) {
|
||||
res.status(401).json({ message: 'You must be logged in.' });
|
||||
return;
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { authOptions } from '../../../pages/api/auth/[...nextauth]';
|
||||
import { unstable_getServerSession } from 'next-auth/next';
|
||||
import { getServerSession } from 'next-auth/next';
|
||||
|
||||
export default async function handler(req, res) {
|
||||
if (req.method == 'GET') {
|
||||
//Verify that the user is logged in.
|
||||
const session = await unstable_getServerSession(req, res, authOptions);
|
||||
const session = await getServerSession(req, res, authOptions);
|
||||
if (!session) {
|
||||
// res.status(401).json({ message: 'You must be logged in.' });
|
||||
res.status(401).end();
|
||||
|
|
16
pages/api/version/index.js
Normal file
16
pages/api/version/index.js
Normal file
|
@ -0,0 +1,16 @@
|
|||
import packageInfo from '../../../package.json';
|
||||
|
||||
export default async function handler(req, res) {
|
||||
if (req.method === 'GET') {
|
||||
try {
|
||||
res.status(200).json({ version: packageInfo.version });
|
||||
return;
|
||||
} catch (error) {
|
||||
res.status(500).json({
|
||||
status: 500,
|
||||
message: 'API error, contact the administrator !',
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,6 +1,6 @@
|
|||
//Lib
|
||||
import { authOptions } from '../pages/api/auth/[...nextauth]';
|
||||
import { unstable_getServerSession } from 'next-auth/next';
|
||||
import { getServerSession } from 'next-auth/next';
|
||||
import { useSession } from 'next-auth/react';
|
||||
import Head from 'next/head';
|
||||
|
||||
|
@ -32,7 +32,7 @@ export default function Index() {
|
|||
|
||||
export async function getServerSideProps(context) {
|
||||
//Var
|
||||
const session = await unstable_getServerSession(
|
||||
const session = await getServerSession(
|
||||
context.req,
|
||||
context.res,
|
||||
authOptions
|
||||
|
|
|
@ -5,7 +5,7 @@ import { useState } from 'react';
|
|||
import { SpinnerDotted } from 'spinners-react';
|
||||
import { useRouter } from 'next/router';
|
||||
import { authOptions } from './api/auth/[...nextauth]';
|
||||
import { unstable_getServerSession } from 'next-auth/next';
|
||||
import { getServerSession } from 'next-auth/next';
|
||||
|
||||
//Components
|
||||
import Error from '../Components/UI/Error/Error';
|
||||
|
@ -99,10 +99,14 @@ export default function Login() {
|
|||
placeholder='Username'
|
||||
className='signInInput'
|
||||
{...register('username', {
|
||||
required: true,
|
||||
required: 'This field is required.',
|
||||
pattern: {
|
||||
value: /^[^\s]+$/g,
|
||||
message: 'No space allowed.',
|
||||
},
|
||||
})}
|
||||
/>
|
||||
{errors.email && errors.email.type === 'required' && (
|
||||
{errors.username && (
|
||||
<small
|
||||
style={{
|
||||
color: 'red',
|
||||
|
@ -110,18 +114,7 @@ export default function Login() {
|
|||
marginTop: '3px',
|
||||
}}
|
||||
>
|
||||
This field is required.
|
||||
</small>
|
||||
)}
|
||||
{errors.email && errors.email.type === 'pattern' && (
|
||||
<small
|
||||
style={{
|
||||
color: 'red',
|
||||
display: 'block',
|
||||
marginTop: '3px',
|
||||
}}
|
||||
>
|
||||
Incorrect email address format.
|
||||
{errors.username.message}
|
||||
</small>
|
||||
)}
|
||||
</p>
|
||||
|
@ -131,7 +124,7 @@ export default function Login() {
|
|||
placeholder='Password'
|
||||
className='signInInput'
|
||||
{...register('password', {
|
||||
required: true,
|
||||
required: 'This field is required.',
|
||||
})}
|
||||
/>
|
||||
{errors.password && (
|
||||
|
@ -142,7 +135,7 @@ export default function Login() {
|
|||
marginTop: '3px',
|
||||
}}
|
||||
>
|
||||
This field is required.
|
||||
{errors.password.message}
|
||||
</small>
|
||||
)}
|
||||
</p>
|
||||
|
@ -171,22 +164,13 @@ export default function Login() {
|
|||
</form>
|
||||
</main>
|
||||
</section>
|
||||
<p style={{ color: '#78797d', textAlign: 'center' }}>
|
||||
Made with <span>❤️</span> by{' '}
|
||||
<a
|
||||
style={{ textDecoration: 'none', color: '#5c7fda' }}
|
||||
href='https://r4ven.fr'
|
||||
>
|
||||
Raven
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export async function getServerSideProps(context) {
|
||||
//Var
|
||||
const session = await unstable_getServerSession(
|
||||
const session = await getServerSession(
|
||||
context.req,
|
||||
context.res,
|
||||
authOptions
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue