Compare commits

...

334 commits

Author SHA1 Message Date
Ravinou
0a10003b43 chore: 🧹 remove old package-lock.yml (pnpm migration) 2026-03-01 10:46:44 +01:00
Ravinou
3316be7b3c feat: default select first repo in wizard 2026-03-01 10:46:44 +01:00
Ravinou
3772949fdb chore: 🧹 update packages 2026-03-01 10:15:49 +01:00
Ravinou
80b440e2b9 chore: 🧹 update packages 2026-03-01 09:31:36 +01:00
dependabot[bot]
d7487fbc4a chore(deps-dev): bump @types/node from 25.0.6 to 25.0.9
Bumps [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) from 25.0.6 to 25.0.9.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node)

---
updated-dependencies:
- dependency-name: "@types/node"
  dependency-version: 25.0.9
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-01-24 13:34:02 +01:00
dependabot[bot]
4b76223097 chore(deps): bump next from 16.1.1 to 16.1.3
Bumps [next](https://github.com/vercel/next.js) from 16.1.1 to 16.1.3.
- [Release notes](https://github.com/vercel/next.js/releases)
- [Changelog](https://github.com/vercel/next.js/blob/canary/release.js)
- [Commits](https://github.com/vercel/next.js/compare/v16.1.1...v16.1.3)

---
updated-dependencies:
- dependency-name: next
  dependency-version: 16.1.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-01-24 13:33:51 +01:00
dependabot[bot]
735a043858 chore(deps-dev): bump eslint-config-next from 16.1.1 to 16.1.3
Bumps [eslint-config-next](https://github.com/vercel/next.js/tree/HEAD/packages/eslint-config-next) from 16.1.1 to 16.1.3.
- [Release notes](https://github.com/vercel/next.js/releases)
- [Changelog](https://github.com/vercel/next.js/blob/canary/release.js)
- [Commits](https://github.com/vercel/next.js/commits/v16.1.3/packages/eslint-config-next)

---
updated-dependencies:
- dependency-name: eslint-config-next
  dependency-version: 16.1.3
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-01-24 12:50:42 +01:00
Ravinou
0f2a4a716f chore: 🧹 update packages 2026-01-18 13:01:24 +01:00
Ravinou
29ca7dfeec feat: switches borg serve restriction to use --restrict-to-repository 2026-01-18 12:27:38 +01:00
dependabot[bot]
1ddaa0f648 chore(deps): bump nodemailer and @types/nodemailer
Bumps [nodemailer](https://github.com/nodemailer/nodemailer) and [@types/nodemailer](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/nodemailer). These dependencies needed to be updated together.

Updates `nodemailer` from 7.0.11 to 7.0.12
- [Release notes](https://github.com/nodemailer/nodemailer/releases)
- [Changelog](https://github.com/nodemailer/nodemailer/blob/master/CHANGELOG.md)
- [Commits](https://github.com/nodemailer/nodemailer/compare/v7.0.11...v7.0.12)

Updates `@types/nodemailer` from 7.0.4 to 7.0.5
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/nodemailer)

---
updated-dependencies:
- dependency-name: nodemailer
  dependency-version: 7.0.12
  dependency-type: direct:production
  update-type: version-update:semver-patch
- dependency-name: "@types/nodemailer"
  dependency-version: 7.0.5
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-01-18 11:21:27 +01:00
Ravinou
577fdbcd68 docker: 🐳 improve logs 2026-01-18 10:14:14 +01:00
dependabot[bot]
836450e8db chore(deps-dev): bump @types/react from 19.2.7 to 19.2.8
Bumps [@types/react](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/react) from 19.2.7 to 19.2.8.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/react)

---
updated-dependencies:
- dependency-name: "@types/react"
  dependency-version: 19.2.8
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-01-18 10:02:19 +01:00
dependabot[bot]
efd7083463 chore(deps): bump react-hook-form from 7.68.0 to 7.71.0
Bumps [react-hook-form](https://github.com/react-hook-form/react-hook-form) from 7.68.0 to 7.71.0.
- [Release notes](https://github.com/react-hook-form/react-hook-form/releases)
- [Changelog](https://github.com/react-hook-form/react-hook-form/blob/master/CHANGELOG.md)
- [Commits](https://github.com/react-hook-form/react-hook-form/compare/v7.68.0...v7.71.0)

---
updated-dependencies:
- dependency-name: react-hook-form
  dependency-version: 7.71.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-01-18 10:01:45 +01:00
dependabot[bot]
187f9839f3 chore(deps-dev): bump vitest from 4.0.16 to 4.0.17
Bumps [vitest](https://github.com/vitest-dev/vitest/tree/HEAD/packages/vitest) from 4.0.16 to 4.0.17.
- [Release notes](https://github.com/vitest-dev/vitest/releases)
- [Commits](https://github.com/vitest-dev/vitest/commits/v4.0.17/packages/vitest)

---
updated-dependencies:
- dependency-name: vitest
  dependency-version: 4.0.17
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-01-18 10:01:17 +01:00
dependabot[bot]
afa5c9bb1f chore(deps-dev): bump @types/node from 25.0.3 to 25.0.6
Bumps [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) from 25.0.3 to 25.0.6.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node)

---
updated-dependencies:
- dependency-name: "@types/node"
  dependency-version: 25.0.6
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-01-18 09:58:52 +01:00
Ravinou
a1bd3deb1d fix: 🐛 trim ssh public key value in textarea #599 2026-01-09 13:21:36 +01:00
dependabot[bot]
8f7be1d14e chore(deps-dev): bump @commitlint/config-conventional
Bumps [@commitlint/config-conventional](https://github.com/conventional-changelog/commitlint/tree/HEAD/@commitlint/config-conventional) from 20.2.0 to 20.3.1.
- [Release notes](https://github.com/conventional-changelog/commitlint/releases)
- [Changelog](https://github.com/conventional-changelog/commitlint/blob/master/@commitlint/config-conventional/CHANGELOG.md)
- [Commits](https://github.com/conventional-changelog/commitlint/commits/v20.3.1/@commitlint/config-conventional)

---
updated-dependencies:
- dependency-name: "@commitlint/config-conventional"
  dependency-version: 20.3.1
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-01-09 12:53:46 +01:00
dependabot[bot]
42cca2763f chore(deps-dev): bump vitest from 4.0.15 to 4.0.16
Bumps [vitest](https://github.com/vitest-dev/vitest/tree/HEAD/packages/vitest) from 4.0.15 to 4.0.16.
- [Release notes](https://github.com/vitest-dev/vitest/releases)
- [Commits](https://github.com/vitest-dev/vitest/commits/v4.0.16/packages/vitest)

---
updated-dependencies:
- dependency-name: vitest
  dependency-version: 4.0.16
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-01-09 12:51:33 +01:00
dependabot[bot]
916b562f73 chore(deps-dev): bump eslint-config-next from 16.0.10 to 16.1.1
Bumps [eslint-config-next](https://github.com/vercel/next.js/tree/HEAD/packages/eslint-config-next) from 16.0.10 to 16.1.1.
- [Release notes](https://github.com/vercel/next.js/releases)
- [Changelog](https://github.com/vercel/next.js/blob/canary/release.js)
- [Commits](https://github.com/vercel/next.js/commits/v16.1.1/packages/eslint-config-next)

---
updated-dependencies:
- dependency-name: eslint-config-next
  dependency-version: 16.1.1
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-01-09 12:51:21 +01:00
dependabot[bot]
e34037ebce chore(deps-dev): bump @commitlint/cli from 20.3.0 to 20.3.1
Bumps [@commitlint/cli](https://github.com/conventional-changelog/commitlint/tree/HEAD/@commitlint/cli) from 20.3.0 to 20.3.1.
- [Release notes](https://github.com/conventional-changelog/commitlint/releases)
- [Changelog](https://github.com/conventional-changelog/commitlint/blob/master/@commitlint/cli/CHANGELOG.md)
- [Commits](https://github.com/conventional-changelog/commitlint/commits/v20.3.1/@commitlint/cli)

---
updated-dependencies:
- dependency-name: "@commitlint/cli"
  dependency-version: 20.3.1
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-01-09 12:51:09 +01:00
dependabot[bot]
c819521b05 chore(deps): bump swr from 2.3.7 to 2.3.8
Bumps [swr](https://github.com/vercel/swr) from 2.3.7 to 2.3.8.
- [Release notes](https://github.com/vercel/swr/releases)
- [Commits](https://github.com/vercel/swr/compare/v2.3.7...v2.3.8)

---
updated-dependencies:
- dependency-name: swr
  dependency-version: 2.3.8
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-01-09 12:50:59 +01:00
Ravinou
c5e99cabe5 chore: 🧹 pipeline improvements 2026-01-08 15:34:17 +01:00
Ravinou
079bf35d87 chore: 🧹 migrate from npm to pnpm package manager 2026-01-08 13:58:08 +01:00
Hobbabobba
14dc526177 typo 2026-01-08 13:01:58 +01:00
Hobbabobba
9d6211b0aa adjusted to new version of borgmatic
example configuration of borgmatic adjusted
2026-01-08 13:01:58 +01:00
Ravinou
3fcd39cf40
Merge pull request #602 from Ravinou/dependabot/npm_and_yarn/types/bcryptjs-3.0.0
chore(deps-dev): bump @types/bcryptjs from 2.4.6 to 3.0.0
2026-01-08 12:56:08 +01:00
Ravinou
85b9ccaf04
🥇 Thanking a new sponsor 🥇
Thank you very much @daschmidt1994 for sponsoring ! This commit is dedicated to thanking you. You give me strength to continue the work! ❤️
2026-01-08 12:54:56 +01:00
dependabot[bot]
d944a70497
chore(deps-dev): bump @types/bcryptjs from 2.4.6 to 3.0.0
Bumps [@types/bcryptjs](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/bcryptjs) from 2.4.6 to 3.0.0.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/bcryptjs)

---
updated-dependencies:
- dependency-name: "@types/bcryptjs"
  dependency-version: 3.0.0
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-01-08 11:44:03 +00:00
Ravinou
961d30354d
Merge pull request #612 from Ravinou/dependabot/npm_and_yarn/commitlint/cli-20.3.0
chore(deps-dev): bump @commitlint/cli from 20.2.0 to 20.3.0
2026-01-08 12:36:38 +01:00
Ravinou
ccfb21a790
Merge pull request #611 from Ravinou/dependabot/npm_and_yarn/types/node-25.0.3
chore(deps-dev): bump @types/node from 24.10.2 to 25.0.3
2026-01-08 12:36:07 +01:00
Ravinou
3925c8ab8c
Merge pull request #613 from Ravinou/dependabot/npm_and_yarn/tabler/icons-react-3.36.1
chore(deps): bump @tabler/icons-react from 3.35.0 to 3.36.1
2026-01-08 12:35:42 +01:00
Ravinou
1faa710105
Merge pull request #614 from Ravinou/dependabot/npm_and_yarn/next-16.1.1
chore(deps): bump next from 16.0.7 to 16.1.1
2026-01-08 12:35:18 +01:00
dependabot[bot]
2db5e65f9a
chore(deps): bump next from 16.0.7 to 16.1.1
Bumps [next](https://github.com/vercel/next.js) from 16.0.7 to 16.1.1.
- [Release notes](https://github.com/vercel/next.js/releases)
- [Changelog](https://github.com/vercel/next.js/blob/canary/release.js)
- [Commits](https://github.com/vercel/next.js/compare/v16.0.7...v16.1.1)

---
updated-dependencies:
- dependency-name: next
  dependency-version: 16.1.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-01-02 19:07:46 +00:00
dependabot[bot]
5bb148459c
chore(deps): bump @tabler/icons-react from 3.35.0 to 3.36.1
Bumps [@tabler/icons-react](https://github.com/tabler/tabler-icons/tree/HEAD/packages/icons-react) from 3.35.0 to 3.36.1.
- [Release notes](https://github.com/tabler/tabler-icons/releases)
- [Commits](https://github.com/tabler/tabler-icons/commits/v3.36.1/packages/icons-react)

---
updated-dependencies:
- dependency-name: "@tabler/icons-react"
  dependency-version: 3.36.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-01-02 19:07:29 +00:00
dependabot[bot]
9981b81cf1
chore(deps-dev): bump @commitlint/cli from 20.2.0 to 20.3.0
Bumps [@commitlint/cli](https://github.com/conventional-changelog/commitlint/tree/HEAD/@commitlint/cli) from 20.2.0 to 20.3.0.
- [Release notes](https://github.com/conventional-changelog/commitlint/releases)
- [Changelog](https://github.com/conventional-changelog/commitlint/blob/master/@commitlint/cli/CHANGELOG.md)
- [Commits](https://github.com/conventional-changelog/commitlint/commits/v20.3.0/@commitlint/cli)

---
updated-dependencies:
- dependency-name: "@commitlint/cli"
  dependency-version: 20.3.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-01-02 19:07:21 +00:00
dependabot[bot]
a240f66e90
chore(deps-dev): bump @types/node from 24.10.2 to 25.0.3
Bumps [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) from 24.10.2 to 25.0.3.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node)

---
updated-dependencies:
- dependency-name: "@types/node"
  dependency-version: 25.0.3
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-01-02 19:07:09 +00:00
Ravinou
12be382b75
Merge pull request #606 from Ravinou/dependabot/npm_and_yarn/eslint-9.39.2
chore(deps-dev): bump eslint from 9.39.1 to 9.39.2
2026-01-02 11:59:31 +01:00
Ravinou
311fb04c34
Merge pull request #610 from Ravinou/dependabot/npm_and_yarn/react-dom-19.2.3
chore(deps): bump react-dom from 19.2.1 to 19.2.3
2026-01-02 11:59:07 +01:00
Ravinou
abe1fd74e0
Merge pull request #603 from Ravinou/dependabot/npm_and_yarn/commitlint/cli-20.2.0
chore(deps-dev): bump @commitlint/cli from 20.1.0 to 20.2.0
2026-01-02 11:58:45 +01:00
dependabot[bot]
4d3336ff97
chore(deps): bump react-dom from 19.2.1 to 19.2.3
Bumps [react-dom](https://github.com/facebook/react/tree/HEAD/packages/react-dom) from 19.2.1 to 19.2.3.
- [Release notes](https://github.com/facebook/react/releases)
- [Changelog](https://github.com/facebook/react/blob/main/CHANGELOG.md)
- [Commits](https://github.com/facebook/react/commits/v19.2.3/packages/react-dom)

---
updated-dependencies:
- dependency-name: react-dom
  dependency-version: 19.2.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-29 19:07:05 +00:00
dependabot[bot]
9e6df1e5b3
chore(deps-dev): bump @commitlint/cli from 20.1.0 to 20.2.0
Bumps [@commitlint/cli](https://github.com/conventional-changelog/commitlint/tree/HEAD/@commitlint/cli) from 20.1.0 to 20.2.0.
- [Release notes](https://github.com/conventional-changelog/commitlint/releases)
- [Changelog](https://github.com/conventional-changelog/commitlint/blob/master/@commitlint/cli/CHANGELOG.md)
- [Commits](https://github.com/conventional-changelog/commitlint/commits/v20.2.0/@commitlint/cli)

---
updated-dependencies:
- dependency-name: "@commitlint/cli"
  dependency-version: 20.2.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-29 17:57:55 +00:00
dependabot[bot]
897152d7fc
chore(deps-dev): bump eslint from 9.39.1 to 9.39.2
Bumps [eslint](https://github.com/eslint/eslint) from 9.39.1 to 9.39.2.
- [Release notes](https://github.com/eslint/eslint/releases)
- [Commits](https://github.com/eslint/eslint/compare/v9.39.1...v9.39.2)

---
updated-dependencies:
- dependency-name: eslint
  dependency-version: 9.39.2
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-29 17:57:09 +00:00
Ravinou
3ccf5abc3d
Merge pull request #601 from Ravinou/dependabot/npm_and_yarn/commitlint/config-conventional-20.2.0
chore(deps-dev): bump @commitlint/config-conventional from 20.0.0 to 20.2.0
2025-12-29 18:56:38 +01:00
Ravinou
02cd028c40
Merge pull request #607 from Ravinou/dependabot/npm_and_yarn/eslint-config-next-16.0.10
chore(deps-dev): bump eslint-config-next from 16.0.7 to 16.0.10
2025-12-29 18:55:54 +01:00
Ravinou
5636142104
Merge pull request #598 from Ravinou/dependabot/github_actions/actions/checkout-6
chore(deps): bump actions/checkout from 4 to 6
2025-12-29 18:55:41 +01:00
Ravinou
7c1790eb8c
Merge pull request #581 from Ravinou/dependabot/github_actions/actions/setup-node-6
chore(deps): bump actions/setup-node from 4 to 6
2025-12-29 18:55:19 +01:00
dependabot[bot]
904fa5928b
chore(deps-dev): bump eslint-config-next from 16.0.7 to 16.0.10
Bumps [eslint-config-next](https://github.com/vercel/next.js/tree/HEAD/packages/eslint-config-next) from 16.0.7 to 16.0.10.
- [Release notes](https://github.com/vercel/next.js/releases)
- [Changelog](https://github.com/vercel/next.js/blob/canary/release.js)
- [Commits](https://github.com/vercel/next.js/commits/v16.0.10/packages/eslint-config-next)

---
updated-dependencies:
- dependency-name: eslint-config-next
  dependency-version: 16.0.10
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-15 19:09:11 +00:00
dependabot[bot]
446e75d7aa
chore(deps-dev): bump @commitlint/config-conventional
Bumps [@commitlint/config-conventional](https://github.com/conventional-changelog/commitlint/tree/HEAD/@commitlint/config-conventional) from 20.0.0 to 20.2.0.
- [Release notes](https://github.com/conventional-changelog/commitlint/releases)
- [Changelog](https://github.com/conventional-changelog/commitlint/blob/master/@commitlint/config-conventional/CHANGELOG.md)
- [Commits](https://github.com/conventional-changelog/commitlint/commits/v20.2.0/@commitlint/config-conventional)

---
updated-dependencies:
- dependency-name: "@commitlint/config-conventional"
  dependency-version: 20.2.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-15 12:15:18 +00:00
Ravinou
cf7b43625c
Merge pull request #605 from Ravinou/dependabot/npm_and_yarn/types/node-24.10.2
chore(deps-dev): bump @types/node from 24.10.1 to 24.10.2
2025-12-15 13:14:11 +01:00
dependabot[bot]
d201b290e9
chore(deps-dev): bump @types/node from 24.10.1 to 24.10.2
Bumps [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) from 24.10.1 to 24.10.2.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node)

---
updated-dependencies:
- dependency-name: "@types/node"
  dependency-version: 24.10.2
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-09 19:09:28 +00:00
dependabot[bot]
7b8c4d6017
chore(deps): bump actions/setup-node from 4 to 6
Bumps [actions/setup-node](https://github.com/actions/setup-node) from 4 to 6.
- [Release notes](https://github.com/actions/setup-node/releases)
- [Commits](https://github.com/actions/setup-node/compare/v4...v6)

---
updated-dependencies:
- dependency-name: actions/setup-node
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-07 16:38:16 +00:00
dependabot[bot]
13872ed29e
chore(deps): bump actions/checkout from 4 to 6
Bumps [actions/checkout](https://github.com/actions/checkout) from 4 to 6.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v4...v6)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-07 16:37:41 +00:00
Ravinou
90506c0827
🥇 Thanking a new sponsor 🥇
Thank you very much @shrippen for sponsoring ! This commit is dedicated to thanking you. You give me strength to continue the work! ❤️
2025-12-04 22:14:06 +01:00
Ravinou
9c267e75ec
publish: 📦 version 3.1.2 2025-12-04 21:59:16 +01:00
Ravinou
d9a8ecf70b
Merge pull request #600 from Ravinou/updates
chore: 🧹 update dependencies
2025-12-04 21:56:07 +01:00
Ravinou
2ce7232849
refactor: migrates to the new ESLint configuration 2025-12-04 21:47:29 +01:00
Ravinou
3ff2ace3bb
chore: 🧹 update dependencies 2025-12-04 20:47:39 +01:00
Ravinou
d5e8064348
Merge pull request #573 from Ravinou/dependabot/npm_and_yarn/react-hook-form-7.63.0
chore(deps): bump react-hook-form from 7.60.0 to 7.63.0
2025-10-02 20:05:06 +02:00
dependabot[bot]
dbe7b4081e
chore(deps): bump react-hook-form from 7.60.0 to 7.63.0
Bumps [react-hook-form](https://github.com/react-hook-form/react-hook-form) from 7.60.0 to 7.63.0.
- [Release notes](https://github.com/react-hook-form/react-hook-form/releases)
- [Changelog](https://github.com/react-hook-form/react-hook-form/blob/master/CHANGELOG.md)
- [Commits](https://github.com/react-hook-form/react-hook-form/compare/v7.60.0...v7.63.0)

---
updated-dependencies:
- dependency-name: react-hook-form
  dependency-version: 7.63.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-09-22 19:10:02 +00:00
Ravinou
6a3625ef98
Merge pull request #534 from Ravinou/dependabot/npm_and_yarn/react-select-5.10.2
chore(deps): bump react-select from 5.10.1 to 5.10.2
2025-09-20 13:23:29 +02:00
Ravinou
f1731d769d
Merge pull request #563 from Ravinou/dependabot/npm_and_yarn/eslint-config-next-15.5.3
chore(deps-dev): bump eslint-config-next from 15.3.4 to 15.5.3
2025-09-20 13:22:45 +02:00
dependabot[bot]
63216622a6
chore(deps-dev): bump eslint-config-next from 15.3.4 to 15.5.3
Bumps [eslint-config-next](https://github.com/vercel/next.js/tree/HEAD/packages/eslint-config-next) from 15.3.4 to 15.5.3.
- [Release notes](https://github.com/vercel/next.js/releases)
- [Changelog](https://github.com/vercel/next.js/blob/canary/release.js)
- [Commits](https://github.com/vercel/next.js/commits/v15.5.3/packages/eslint-config-next)

---
updated-dependencies:
- dependency-name: eslint-config-next
  dependency-version: 15.5.3
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-09-11 19:09:26 +00:00
dependabot[bot]
f5af821d47
chore(deps): bump react-select from 5.10.1 to 5.10.2
Bumps [react-select](https://github.com/JedWatson/react-select) from 5.10.1 to 5.10.2.
- [Release notes](https://github.com/JedWatson/react-select/releases)
- [Changelog](https://github.com/JedWatson/react-select/blob/master/docs/CHANGELOG.md)
- [Commits](https://github.com/JedWatson/react-select/compare/react-select@5.10.1...react-select@5.10.2)

---
updated-dependencies:
- dependency-name: react-select
  dependency-version: 5.10.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-07-14 21:58:11 +00:00
Ravinou
340f186a37
Merge pull request #525 from Ravinou/dependabot/npm_and_yarn/react-hook-form-7.60.0
chore(deps): bump react-hook-form from 7.59.0 to 7.60.0
2025-07-13 09:09:51 +02:00
Ravinou
53b29ea6f9
Merge pull request #526 from Ravinou/dependabot/npm_and_yarn/swr-2.3.4
chore(deps): bump swr from 2.3.3 to 2.3.4
2025-07-13 09:09:40 +02:00
dependabot[bot]
665974a15a
chore(deps): bump swr from 2.3.3 to 2.3.4
Bumps [swr](https://github.com/vercel/swr) from 2.3.3 to 2.3.4.
- [Release notes](https://github.com/vercel/swr/releases)
- [Commits](https://github.com/vercel/swr/compare/v2.3.3...v2.3.4)

---
updated-dependencies:
- dependency-name: swr
  dependency-version: 2.3.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-07-07 23:03:00 +00:00
dependabot[bot]
ac3c4c7b1d
chore(deps): bump react-hook-form from 7.59.0 to 7.60.0
Bumps [react-hook-form](https://github.com/react-hook-form/react-hook-form) from 7.59.0 to 7.60.0.
- [Release notes](https://github.com/react-hook-form/react-hook-form/releases)
- [Changelog](https://github.com/react-hook-form/react-hook-form/blob/master/CHANGELOG.md)
- [Commits](https://github.com/react-hook-form/react-hook-form/compare/v7.59.0...v7.60.0)

---
updated-dependencies:
- dependency-name: react-hook-form
  dependency-version: 7.60.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-07-07 22:14:32 +00:00
Ravinou
2d094cfed9
Merge pull request #518 from Ravinou/dependabot/npm_and_yarn/prettier-3.6.2
chore(deps-dev): bump prettier from 3.5.3 to 3.6.2
2025-07-06 20:46:19 +02:00
Ravinou
aeb299ab3b
🥇 Thanking a new sponsor 🥇
Thank you very much @MacH59-cos for sponsoring ! This commit is dedicated to thanking you. You give me strength to continue the work! ❤️
2025-07-05 22:34:48 +02:00
Ravinou
53079bebce
Merge pull request #523 from Ravinou/3.1.1
3.1.1
2025-07-05 22:10:17 +02:00
Ravinou
5a96fb88b6
publish: 📦 version 3.1.1 2025-07-05 21:55:15 +02:00
Ravinou
b988b81089
chore: 🧹 update dependencies 2025-07-05 21:52:01 +02:00
Ravinou
b3d517d791
fix: 🐛 follows symbolic links when calculating storage #478 2025-07-05 21:50:14 +02:00
Ravinou
5ee55d18df
ui: 🎨 improve some label #497 2025-07-05 21:44:26 +02:00
Ravinou
eed58c129e
Merge pull request #520 from Ravinou/dependabot/npm_and_yarn/react-hook-form-7.59.0
chore(deps): bump react-hook-form from 7.58.1 to 7.59.0
2025-07-05 19:50:08 +02:00
dependabot[bot]
d5a440448e
chore(deps): bump react-hook-form from 7.58.1 to 7.59.0
Bumps [react-hook-form](https://github.com/react-hook-form/react-hook-form) from 7.58.1 to 7.59.0.
- [Release notes](https://github.com/react-hook-form/react-hook-form/releases)
- [Changelog](https://github.com/react-hook-form/react-hook-form/blob/master/CHANGELOG.md)
- [Commits](https://github.com/react-hook-form/react-hook-form/compare/v7.58.1...v7.59.0)

---
updated-dependencies:
- dependency-name: react-hook-form
  dependency-version: 7.59.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-07-01 01:24:45 +00:00
dependabot[bot]
b6cc2c351e
chore(deps-dev): bump prettier from 3.5.3 to 3.6.2
Bumps [prettier](https://github.com/prettier/prettier) from 3.5.3 to 3.6.2.
- [Release notes](https://github.com/prettier/prettier/releases)
- [Changelog](https://github.com/prettier/prettier/blob/main/CHANGELOG.md)
- [Commits](https://github.com/prettier/prettier/compare/3.5.3...3.6.2)

---
updated-dependencies:
- dependency-name: prettier
  dependency-version: 3.6.2
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-06-30 22:43:40 +00:00
Ravinou
b38ffd7be7
Merge pull request #513 from Ravinou/dependabot/npm_and_yarn/eslint-config-next-15.3.4
chore(deps-dev): bump eslint-config-next from 15.3.3 to 15.3.4
2025-06-29 14:52:08 +02:00
Ravinou
4009b0cf8b
Merge pull request #514 from Ravinou/dependabot/npm_and_yarn/next-15.3.4
chore(deps): bump next from 15.3.3 to 15.3.4
2025-06-29 14:51:55 +02:00
dependabot[bot]
599fe35ddc
chore(deps): bump next from 15.3.3 to 15.3.4
Bumps [next](https://github.com/vercel/next.js) from 15.3.3 to 15.3.4.
- [Release notes](https://github.com/vercel/next.js/releases)
- [Changelog](https://github.com/vercel/next.js/blob/canary/release.js)
- [Commits](https://github.com/vercel/next.js/compare/v15.3.3...v15.3.4)

---
updated-dependencies:
- dependency-name: next
  dependency-version: 15.3.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-06-23 21:10:01 +00:00
dependabot[bot]
97b31e6bae
chore(deps-dev): bump eslint-config-next from 15.3.3 to 15.3.4
Bumps [eslint-config-next](https://github.com/vercel/next.js/tree/HEAD/packages/eslint-config-next) from 15.3.3 to 15.3.4.
- [Release notes](https://github.com/vercel/next.js/releases)
- [Changelog](https://github.com/vercel/next.js/blob/canary/release.js)
- [Commits](https://github.com/vercel/next.js/commits/v15.3.4/packages/eslint-config-next)

---
updated-dependencies:
- dependency-name: eslint-config-next
  dependency-version: 15.3.4
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-06-23 20:31:29 +00:00
Ravinou
0c6e24f599
Merge pull request #505 from Ravinou/dependabot/npm_and_yarn/types/react-19.1.8
chore(deps-dev): bump @types/react from 19.1.5 to 19.1.8
2025-06-22 10:13:55 +02:00
Ravinou
980399d238
Merge pull request #511 from Ravinou/dependabot/npm_and_yarn/react-hook-form-7.58.1
chore(deps): bump react-hook-form from 7.57.0 to 7.58.1
2025-06-22 10:13:38 +02:00
dependabot[bot]
4189cc34ee
chore(deps): bump react-hook-form from 7.57.0 to 7.58.1
Bumps [react-hook-form](https://github.com/react-hook-form/react-hook-form) from 7.57.0 to 7.58.1.
- [Release notes](https://github.com/react-hook-form/react-hook-form/releases)
- [Changelog](https://github.com/react-hook-form/react-hook-form/blob/master/CHANGELOG.md)
- [Commits](https://github.com/react-hook-form/react-hook-form/compare/v7.57.0...v7.58.1)

---
updated-dependencies:
- dependency-name: react-hook-form
  dependency-version: 7.58.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-06-17 19:59:40 +00:00
dependabot[bot]
6565442042
chore(deps-dev): bump @types/react from 19.1.5 to 19.1.8
Bumps [@types/react](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/react) from 19.1.5 to 19.1.8.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/react)

---
updated-dependencies:
- dependency-name: "@types/react"
  dependency-version: 19.1.8
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-06-11 19:45:10 +00:00
Ravinou
4e7b880624
Merge pull request #490 from Ravinou/dependabot/npm_and_yarn/tabler/icons-react-3.34.0
chore(deps): bump @tabler/icons-react from 3.33.0 to 3.34.0
2025-06-08 10:33:02 +02:00
Ravinou
c2912253df
Merge pull request #493 from Ravinou/dependabot/npm_and_yarn/react-hook-form-7.57.0
chore(deps): bump react-hook-form from 7.56.4 to 7.57.0
2025-06-08 10:32:47 +02:00
Ravinou
4f011f6c48
Merge pull request #496 from Ravinou/dependabot/npm_and_yarn/types/node-22.15.30
chore(deps-dev): bump @types/node from 22.15.21 to 22.15.30
2025-06-08 10:32:01 +02:00
dependabot[bot]
8202bcd2ad
chore(deps-dev): bump @types/node from 22.15.21 to 22.15.30
Bumps [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) from 22.15.21 to 22.15.30.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node)

---
updated-dependencies:
- dependency-name: "@types/node"
  dependency-version: 22.15.30
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-06-05 19:46:39 +00:00
dependabot[bot]
1ea2174f9f
chore(deps): bump react-hook-form from 7.56.4 to 7.57.0
Bumps [react-hook-form](https://github.com/react-hook-form/react-hook-form) from 7.56.4 to 7.57.0.
- [Release notes](https://github.com/react-hook-form/react-hook-form/releases)
- [Changelog](https://github.com/react-hook-form/react-hook-form/blob/master/CHANGELOG.md)
- [Commits](https://github.com/react-hook-form/react-hook-form/compare/v7.56.4...v7.57.0)

---
updated-dependencies:
- dependency-name: react-hook-form
  dependency-version: 7.57.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-06-02 19:34:44 +00:00
dependabot[bot]
e3a973c09c
chore(deps): bump @tabler/icons-react from 3.33.0 to 3.34.0
Bumps [@tabler/icons-react](https://github.com/tabler/tabler-icons/tree/HEAD/packages/icons-react) from 3.33.0 to 3.34.0.
- [Release notes](https://github.com/tabler/tabler-icons/releases)
- [Commits](https://github.com/tabler/tabler-icons/commits/v3.34.0/packages/icons-react)

---
updated-dependencies:
- dependency-name: "@tabler/icons-react"
  dependency-version: 3.34.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-06-02 19:14:25 +00:00
Ravinou
57e4550e42
publish: 📦 3.1.0 2025-06-01 13:23:31 +02:00
Ravinou
ceeb7a3e6a
Merge pull request #483 from Ravinou/dependabot/npm_and_yarn/vitest-3.1.4
chore(deps-dev): bump vitest from 3.1.3 to 3.1.4
2025-06-01 13:17:43 +02:00
Ravinou
3df990f217
Merge pull request #484 from Ravinou/dependabot/npm_and_yarn/react-hook-form-7.56.4
chore(deps): bump react-hook-form from 7.56.3 to 7.56.4
2025-06-01 13:17:30 +02:00
Ravinou
ca9097a2aa
Merge pull request #486 from Ravinou/dependabot/npm_and_yarn/next-15.3.3
chore(deps): bump next from 15.3.1 to 15.3.3
2025-06-01 13:17:18 +02:00
Ravinou
aa8e493e37
Merge pull request #487 from Ravinou/dependabot/npm_and_yarn/eslint-config-next-15.3.3
chore(deps-dev): bump eslint-config-next from 15.3.1 to 15.3.3
2025-06-01 13:17:01 +02:00
Ravinou
b6652a80f5
Merge pull request #489 from Ravinou/develop
Develop
2025-06-01 12:48:41 +02:00
Ravinou
88c8f920d9
fix: 🐛 handle empty SSH_SERVER_PORT with quick command button #470 2025-06-01 12:28:04 +02:00
Ravinou
3c1ff79add
feat: new toolbar and searching on repository list 2025-06-01 12:19:00 +02:00
Ravinou
6acfdbbfa1
refactor: username is now allowed from 1 to 40 char. #479 2025-06-01 11:02:31 +02:00
Ravinou
0263edd44f
ui: 🎨 adapting ui to allow alias with 100 char #485 2025-06-01 10:51:52 +02:00
Ravinou
05ba852371
Merge pull request #488 from Ravinou/develop
fix: 🐛 prevent shell injection by replacing exec with exeFile
2025-05-31 17:30:12 +02:00
dependabot[bot]
f15fbbcb6d
chore(deps-dev): bump eslint-config-next from 15.3.1 to 15.3.3
Bumps [eslint-config-next](https://github.com/vercel/next.js/tree/HEAD/packages/eslint-config-next) from 15.3.1 to 15.3.3.
- [Release notes](https://github.com/vercel/next.js/releases)
- [Changelog](https://github.com/vercel/next.js/blob/canary/release.js)
- [Commits](https://github.com/vercel/next.js/commits/v15.3.3/packages/eslint-config-next)

---
updated-dependencies:
- dependency-name: eslint-config-next
  dependency-version: 15.3.3
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-05-29 19:57:06 +00:00
dependabot[bot]
3f47efc572
chore(deps): bump next from 15.3.1 to 15.3.3
Bumps [next](https://github.com/vercel/next.js) from 15.3.1 to 15.3.3.
- [Release notes](https://github.com/vercel/next.js/releases)
- [Changelog](https://github.com/vercel/next.js/blob/canary/release.js)
- [Commits](https://github.com/vercel/next.js/compare/v15.3.1...v15.3.3)

---
updated-dependencies:
- dependency-name: next
  dependency-version: 15.3.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-05-29 19:56:39 +00:00
Ravinou
1feb666c4c
fix: 🐛 (security) prevent shell injection by replacing exec with execFile 2025-05-29 11:40:45 +02:00
dependabot[bot]
9921d9d40d
chore(deps): bump react-hook-form from 7.56.3 to 7.56.4
Bumps [react-hook-form](https://github.com/react-hook-form/react-hook-form) from 7.56.3 to 7.56.4.
- [Release notes](https://github.com/react-hook-form/react-hook-form/releases)
- [Changelog](https://github.com/react-hook-form/react-hook-form/blob/master/CHANGELOG.md)
- [Commits](https://github.com/react-hook-form/react-hook-form/compare/v7.56.3...v7.56.4)

---
updated-dependencies:
- dependency-name: react-hook-form
  dependency-version: 7.56.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-05-26 19:45:13 +00:00
dependabot[bot]
64fcafdfa0
chore(deps-dev): bump vitest from 3.1.3 to 3.1.4
Bumps [vitest](https://github.com/vitest-dev/vitest/tree/HEAD/packages/vitest) from 3.1.3 to 3.1.4.
- [Release notes](https://github.com/vitest-dev/vitest/releases)
- [Commits](https://github.com/vitest-dev/vitest/commits/v3.1.4/packages/vitest)

---
updated-dependencies:
- dependency-name: vitest
  dependency-version: 3.1.4
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-05-26 19:44:25 +00:00
Ravinou
5e420d04ca
Merge pull request #471 from Ravinou/dependabot/npm_and_yarn/tabler/icons-react-3.33.0
chore(deps): bump @tabler/icons-react from 3.31.0 to 3.33.0
2025-05-25 18:54:04 +02:00
Ravinou
220f88bd6d
Merge pull request #472 from Ravinou/dependabot/npm_and_yarn/commitlint/config-conventional-19.8.1
chore(deps-dev): bump @commitlint/config-conventional from 19.8.0 to 19.8.1
2025-05-25 18:53:51 +02:00
Ravinou
cb64164b01
Merge pull request #476 from Ravinou/dependabot/npm_and_yarn/types/node-22.15.21
chore(deps-dev): bump @types/node from 22.15.18 to 22.15.21
2025-05-25 18:53:39 +02:00
Ravinou
6680f48253
Merge pull request #477 from Ravinou/dependabot/npm_and_yarn/types/react-19.1.5
chore(deps-dev): bump @types/react from 19.1.3 to 19.1.5
2025-05-25 18:53:27 +02:00
dependabot[bot]
d808f464ad
chore(deps-dev): bump @types/react from 19.1.3 to 19.1.5
Bumps [@types/react](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/react) from 19.1.3 to 19.1.5.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/react)

---
updated-dependencies:
- dependency-name: "@types/react"
  dependency-version: 19.1.5
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-05-21 19:51:25 +00:00
dependabot[bot]
3337e9b97a
chore(deps-dev): bump @types/node from 22.15.18 to 22.15.21
Bumps [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) from 22.15.18 to 22.15.21.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node)

---
updated-dependencies:
- dependency-name: "@types/node"
  dependency-version: 22.15.21
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-05-21 19:51:04 +00:00
dependabot[bot]
7edfc75379
chore(deps-dev): bump @commitlint/config-conventional
Bumps [@commitlint/config-conventional](https://github.com/conventional-changelog/commitlint/tree/HEAD/@commitlint/config-conventional) from 19.8.0 to 19.8.1.
- [Release notes](https://github.com/conventional-changelog/commitlint/releases)
- [Changelog](https://github.com/conventional-changelog/commitlint/blob/master/@commitlint/config-conventional/CHANGELOG.md)
- [Commits](https://github.com/conventional-changelog/commitlint/commits/v19.8.1/@commitlint/config-conventional)

---
updated-dependencies:
- dependency-name: "@commitlint/config-conventional"
  dependency-version: 19.8.1
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-05-19 19:56:26 +00:00
dependabot[bot]
e2c74d067b
chore(deps): bump @tabler/icons-react from 3.31.0 to 3.33.0
Bumps [@tabler/icons-react](https://github.com/tabler/tabler-icons/tree/HEAD/packages/icons-react) from 3.31.0 to 3.33.0.
- [Release notes](https://github.com/tabler/tabler-icons/releases)
- [Commits](https://github.com/tabler/tabler-icons/commits/v3.33.0/packages/icons-react)

---
updated-dependencies:
- dependency-name: "@tabler/icons-react"
  dependency-version: 3.33.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-05-19 19:56:18 +00:00
Ravinou
250bf4ef0c
Merge pull request #455 from Ravinou/dependabot/npm_and_yarn/node-mocks-http-1.17.2
chore(deps-dev): bump node-mocks-http from 1.17.1 to 1.17.2
2025-05-18 12:08:16 +02:00
Ravinou
26c784900a
Merge pull request #462 from Ravinou/dependabot/npm_and_yarn/commitlint/cli-19.8.1
chore(deps-dev): bump @commitlint/cli from 19.8.0 to 19.8.1
2025-05-18 12:08:00 +02:00
Ravinou
092a1d8d3d
Merge pull request #469 from Ravinou/dependabot/npm_and_yarn/types/node-22.15.18
chore(deps-dev): bump @types/node from 22.15.3 to 22.15.18
2025-05-18 12:07:49 +02:00
Ravinou
4514d6b7f2
Merge pull request #460 from Ravinou/dependabot/npm_and_yarn/react-hook-form-7.56.3
chore(deps): bump react-hook-form from 7.56.1 to 7.56.3
2025-05-18 12:07:36 +02:00
Ravinou
754eef3b41
Merge pull request #464 from Forceu/betterDates
Changed "last changes" to a more human-readable format
2025-05-18 12:06:56 +02:00
Marc Bulling
db4749479f
Merge branch 'main' into betterDates 2025-05-18 11:40:23 +02:00
Marc Ole Bulling
515535a5b3
ui: 🎨 changed "last changes" to a more human-readable format 2025-05-18 11:31:41 +02:00
Ravinou
aa85cee260
Merge pull request #465 from Forceu/noIds
Move ID from own row in overview to repo name row
2025-05-18 11:15:06 +02:00
Ravinou
62aedfa1e1
ui: 🎨 replace front-end "id" information with repositoryName 2025-05-18 11:02:25 +02:00
Marc Ole Bulling
0f03b26a63
Move ID to repository name 2025-05-15 23:13:40 +02:00
dependabot[bot]
b4c62818e0
chore(deps-dev): bump @types/node from 22.15.3 to 22.15.18
Bumps [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) from 22.15.3 to 22.15.18.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node)

---
updated-dependencies:
- dependency-name: "@types/node"
  dependency-version: 22.15.18
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-05-14 19:58:50 +00:00
dependabot[bot]
964f011a8a
chore(deps-dev): bump @commitlint/cli from 19.8.0 to 19.8.1
Bumps [@commitlint/cli](https://github.com/conventional-changelog/commitlint/tree/HEAD/@commitlint/cli) from 19.8.0 to 19.8.1.
- [Release notes](https://github.com/conventional-changelog/commitlint/releases)
- [Changelog](https://github.com/conventional-changelog/commitlint/blob/master/@commitlint/cli/CHANGELOG.md)
- [Commits](https://github.com/conventional-changelog/commitlint/commits/v19.8.1/@commitlint/cli)

---
updated-dependencies:
- dependency-name: "@commitlint/cli"
  dependency-version: 19.8.1
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-05-09 19:13:47 +00:00
dependabot[bot]
bcd7d25cd2
chore(deps-dev): bump node-mocks-http from 1.17.1 to 1.17.2
Bumps [node-mocks-http](https://github.com/eugef/node-mocks-http) from 1.17.1 to 1.17.2.
- [Release notes](https://github.com/eugef/node-mocks-http/releases)
- [Changelog](https://github.com/eugef/node-mocks-http/blob/master/HISTORY.md)
- [Commits](https://github.com/eugef/node-mocks-http/compare/v1.17.1...v1.17.2)

---
updated-dependencies:
- dependency-name: node-mocks-http
  dependency-version: 1.17.2
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-05-08 20:12:00 +00:00
Ravinou
f792e65b88
Merge pull request #457 from Ravinou/dependabot/npm_and_yarn/vitest-3.1.3
chore(deps-dev): bump vitest from 3.1.2 to 3.1.3
2025-05-08 22:11:06 +02:00
Ravinou
a01980a323
Merge pull request #458 from Ravinou/dependabot/npm_and_yarn/types/react-19.1.3
chore(deps-dev): bump @types/react from 19.1.2 to 19.1.3
2025-05-08 22:10:46 +02:00
dependabot[bot]
4b1c8c5930
chore(deps): bump react-hook-form from 7.56.1 to 7.56.3
Bumps [react-hook-form](https://github.com/react-hook-form/react-hook-form) from 7.56.1 to 7.56.3.
- [Release notes](https://github.com/react-hook-form/react-hook-form/releases)
- [Changelog](https://github.com/react-hook-form/react-hook-form/blob/master/CHANGELOG.md)
- [Commits](https://github.com/react-hook-form/react-hook-form/compare/v7.56.1...v7.56.3)

---
updated-dependencies:
- dependency-name: react-hook-form
  dependency-version: 7.56.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-05-08 20:00:51 +00:00
dependabot[bot]
29e748e0d6
chore(deps-dev): bump @types/react from 19.1.2 to 19.1.3
Bumps [@types/react](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/react) from 19.1.2 to 19.1.3.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/react)

---
updated-dependencies:
- dependency-name: "@types/react"
  dependency-version: 19.1.3
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-05-06 19:30:42 +00:00
dependabot[bot]
680e826a4f
chore(deps-dev): bump vitest from 3.1.2 to 3.1.3
Bumps [vitest](https://github.com/vitest-dev/vitest/tree/HEAD/packages/vitest) from 3.1.2 to 3.1.3.
- [Release notes](https://github.com/vitest-dev/vitest/releases)
- [Commits](https://github.com/vitest-dev/vitest/commits/v3.1.3/packages/vitest)

---
updated-dependencies:
- dependency-name: vitest
  dependency-version: 3.1.3
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-05-05 19:42:08 +00:00
Ravinou
fd0ba8baaa
Merge pull request #453 from Ravinou/dependabot/npm_and_yarn/node-mocks-http-1.17.1
chore(deps-dev): bump node-mocks-http from 1.17.0 to 1.17.1
2025-05-03 18:03:37 +02:00
dependabot[bot]
c65b8a9e5e
chore(deps-dev): bump node-mocks-http from 1.17.0 to 1.17.1
Bumps [node-mocks-http](https://github.com/eugef/node-mocks-http) from 1.17.0 to 1.17.1.
- [Release notes](https://github.com/eugef/node-mocks-http/releases)
- [Changelog](https://github.com/eugef/node-mocks-http/blob/master/HISTORY.md)
- [Commits](https://github.com/eugef/node-mocks-http/compare/v1.17.0...v1.17.1)

---
updated-dependencies:
- dependency-name: node-mocks-http
  dependency-version: 1.17.1
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-05-02 19:27:51 +00:00
Ravinou
73ecb4bf72
Merge pull request #452 from Ravinou/dependabot/npm_and_yarn/types/node-22.15.3
chore(deps-dev): bump @types/node from 22.15.2 to 22.15.3
2025-05-02 11:09:19 +02:00
Ravinou
720328db0c
Merge pull request #451 from Ravinou/dependabot/npm_and_yarn/node-mocks-http-1.17.0
chore(deps-dev): bump node-mocks-http from 1.16.2 to 1.17.0
2025-05-02 11:09:00 +02:00
dependabot[bot]
cb6e1413d7
chore(deps-dev): bump @types/node from 22.15.2 to 22.15.3
Bumps [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) from 22.15.2 to 22.15.3.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node)

---
updated-dependencies:
- dependency-name: "@types/node"
  dependency-version: 22.15.3
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-04-28 20:41:39 +00:00
dependabot[bot]
10e13763fa
chore(deps-dev): bump node-mocks-http from 1.16.2 to 1.17.0
Bumps [node-mocks-http](https://github.com/eugef/node-mocks-http) from 1.16.2 to 1.17.0.
- [Release notes](https://github.com/eugef/node-mocks-http/releases)
- [Changelog](https://github.com/eugef/node-mocks-http/blob/master/HISTORY.md)
- [Commits](https://github.com/eugef/node-mocks-http/compare/v1.16.2...v1.17.0)

---
updated-dependencies:
- dependency-name: node-mocks-http
  dependency-version: 1.17.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-04-28 20:20:56 +00:00
Ravinou
49e1f8c2a2
publish: 📦 version 3.0.0 2025-04-27 13:41:32 +02:00
Ravinou
d2acc5bba0
Update issue templates 2025-04-27 11:32:18 +02:00
Ravinou
05f532c10d
config: github issue template 2025-04-27 11:27:33 +02:00
Ravinou
8be42035e7
config: github issue template
Update issue templates
2025-04-27 11:18:05 +02:00
Ravinou
33a0de52e3
Update issue templates 2025-04-27 11:16:53 +02:00
Ravinou
18925266f8
Merge pull request #448 from Ravinou/dependabot/npm_and_yarn/types/node-22.15.2
chore(deps-dev): bump @types/node from 22.14.1 to 22.15.2
2025-04-27 09:03:33 +02:00
Ravinou
933f5931e2
Merge pull request #447 from Ravinou/dependabot/npm_and_yarn/react-hook-form-7.56.1
chore(deps): bump react-hook-form from 7.56.0 to 7.56.1
2025-04-27 09:03:16 +02:00
dependabot[bot]
d50750033e
chore(deps-dev): bump @types/node from 22.14.1 to 22.15.2
Bumps [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) from 22.14.1 to 22.15.2.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node)

---
updated-dependencies:
- dependency-name: "@types/node"
  dependency-version: 22.15.2
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-04-25 19:09:28 +00:00
dependabot[bot]
f0783a3027
chore(deps): bump react-hook-form from 7.56.0 to 7.56.1
Bumps [react-hook-form](https://github.com/react-hook-form/react-hook-form) from 7.56.0 to 7.56.1.
- [Release notes](https://github.com/react-hook-form/react-hook-form/releases)
- [Changelog](https://github.com/react-hook-form/react-hook-form/blob/master/CHANGELOG.md)
- [Commits](https://github.com/react-hook-form/react-hook-form/compare/v7.56.0...v7.56.1)

---
updated-dependencies:
- dependency-name: react-hook-form
  dependency-version: 7.56.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-04-23 19:39:52 +00:00
Ravinou
939f56137b
doc: 📚 update README 2025-04-21 16:23:19 +02:00
Ravinou
05a76a5f2a
Merge pull request #446 from Ravinou/develop
React v19 + new loader on app
2025-04-21 15:50:51 +02:00
Ravinou
e4f694d383
refactor: watcher on login form is not used anymore 2025-04-21 13:55:32 +02:00
Ravinou
25022778b9
fix: 🐛 little css warning for flex-start 2025-04-21 13:53:21 +02:00
Ravinou
b1cee4486b
chore: 🧹 update toastify to latest major 2025-04-21 13:48:08 +02:00
Ravinou
e6310a5412
chore: 🧹 react to v19 + update dependencies 2025-04-21 13:43:31 +02:00
Ravinou
a0924a8ba9
chore: 🧹 remove spinners-react lib 2025-04-21 13:36:10 +02:00
Ravinou
daa199dfb0
refactor: remove spinners for nprogress 2025-04-21 13:34:41 +02:00
Ravinou
799bf03cd5
feat: add a new progress bar layout 2025-04-21 10:54:10 +02:00
Ravinou
d601c6dadf
release 3.0.0 - breaking changes 2025-04-20 23:25:12 +02:00
Ravinou
d6245d65c4
chore: 🧹 update dependencies 2025-04-20 23:08:43 +02:00
Ravinou
56ff17853a
refactor: improves repo deletion error handling 2025-04-20 23:08:43 +02:00
Ravinou
1e4a34edce
refactor: improves User Settings component 2025-04-20 23:08:42 +02:00
Ravinou
afe828fc1a
refactor: update cron message to be consistent 2025-04-20 23:08:42 +02:00
Ravinou
5567cddfdb
refactor: improves error handling in repository management 2025-04-20 23:08:41 +02:00
Ravinou
766a63d524
refactor: email template attachement path for typescript build 2025-04-20 23:08:40 +02:00
Ravinou
a62e55b42a
refactor: repoManage component now use repositoryName API 2025-04-20 23:08:40 +02:00
Ravinou
a3d156bdbf
refactor: next-auth does not support api versioning 2025-04-20 23:08:39 +02:00
Ravinou
144bea3947
config: 🔧 add ESlint to github action CI 2025-04-20 23:08:39 +02:00
Ravinou
6f24a63077
chore: 🧹 eslint fixes 2025-04-20 23:08:38 +02:00
Ravinou
58f55fa9fc
refactor: update new api url to components 2025-04-20 23:08:37 +02:00
Ravinou
fb61846bbb
test: add test to lanCommandOption util 2025-04-20 23:08:37 +02:00
Ravinou
4de1884de8
!breaking: 💣 repositoryName is now used instead of id #343
For every repository actions, to be idempotent
2025-04-20 23:08:36 +02:00
Ravinou
c5e206a818
refactor: versioning API 2025-04-20 23:08:36 +02:00
Ravinou
f7faada494
refactor: repositories API rest compliant 2025-04-20 23:08:35 +02:00
Ravinou
3d66ff18e6
refactor: cron API rest compliant 2025-04-20 23:08:34 +02:00
Ravinou
03e4b175df
refactor: account API rest compliant 2025-04-20 23:08:34 +02:00
Ravinou
0ee771f64a
refactor: email API rest compliant 2025-04-20 23:08:33 +02:00
Ravinou
533bfce0d0
refactor: apprise API rest compliant 2025-04-20 23:08:33 +02:00
Ravinou
fec9ba21ad
feat: prevent the cronjob from being executed multiple times 2025-04-20 23:08:32 +02:00
Ravinou
a9dadb9a53
fix: 🐛 handle Apprise to fail gracefully on check status 2025-04-20 23:08:31 +02:00
Ravinou
ae27636dac
ui: 🎨 repo manage dialog 2025-04-20 23:08:31 +02:00
Ravinou
86133a64b0
ui: 🎨 log more info through toast 2025-04-20 23:08:30 +02:00
Ravinou
db36c806b6
fix: 🐛 wizardEnv fetching 2025-04-20 23:08:30 +02:00
Ravinou
ff25907bb3
ui: 🎨 new borgwarehouse logo and favicon 2025-04-20 23:08:29 +02:00
Ravinou
e939b704ef
docker: 🐳 typescript migration 2025-04-20 23:08:28 +02:00
Ravinou
90816bd705
config: 🔧 add docker to husky 2025-04-20 23:08:28 +02:00
Ravinou
c6911e77d2
config: 🔧 add shellcheck to develop PR 2025-04-20 23:08:27 +02:00
Ravinou
26f8864ebf
config: 🔧 add vitest to CI/CD 2025-04-20 23:08:27 +02:00
Ravinou
8237b428bc
chore(deps): 🧹 package rebase with main 2025-04-20 23:08:26 +02:00
Ravinou
73842a8d62
chore: 🧹 clean up some imports 2025-04-20 23:08:25 +02:00
Ravinou
7266ea464e
test: auth service 2025-04-20 23:08:25 +02:00
Ravinou
837b5f01f9
refactor: notif service 2025-04-20 23:08:24 +02:00
Ravinou
8b16a713a5
refactor: auth service 2025-04-20 23:08:23 +02:00
Ravinou
3815109958
refactor: config service 2025-04-20 23:08:23 +02:00
Ravinou
785413eec7
refactor: shell service 2025-04-20 23:08:22 +02:00
Ravinou
e1f234d54b
test: adding some API tests 2025-04-20 23:08:22 +02:00
Ravinou
52d8bca2ad
test: migration from Jest to Vitest 2025-04-20 23:08:21 +02:00
Ravinou
9e2ae9f0fa
refactor: create config service 2025-04-20 23:08:20 +02:00
Ravinou
201f5b41a1
feat: new config service with lowdb and mutex 2025-04-20 23:08:20 +02:00
Ravinou
2463a61943
config: 🔧 add lowdb and async-mutex 2025-04-20 23:08:19 +02:00
Ravinou
ca8199ca33
refactor: cleaning up the centralisation of json reading 2025-04-20 23:08:19 +02:00
Ravinou
7ec99a75c7
config: 🔧 jest and next to typescript 2025-04-20 23:08:18 +02:00
Ravinou
3105963b11
fix: 🐛 wizardEnv data fetch 2025-04-20 23:08:17 +02:00
Ravinou
11aa62a548
refactor: tokenManager API 2025-04-20 23:08:17 +02:00
Ravinou
e4dc585fe5
test: add repo API 2025-04-20 23:08:16 +02:00
Ravinou
49cfbf44e0
refactor: add repo API 2025-04-20 23:08:16 +02:00
Ravinou
ddbb629d75
test: repoList index API 2025-04-20 23:08:15 +02:00
Ravinou
fb68c4331b
refactor: repoList index 2025-04-20 23:08:14 +02:00
Ravinou
8b4ca5d7bc
refactor: create repo.json if not exist 2025-04-20 23:08:14 +02:00
Ravinou
2316fb573e
test: clean some test and add one for repo index 2025-04-20 23:08:13 +02:00
Ravinou
cb2032f309
refactor: repository index 2025-04-20 23:08:13 +02:00
Ravinou
1ae96c8f9a
test: edit API 2025-04-20 23:08:12 +02:00
Ravinou
b7d3aec3b1
refactor: edit API 2025-04-20 23:08:11 +02:00
Ravinou
93000d4406
test: delete API 2025-04-20 23:08:11 +02:00
Ravinou
149fad13ec
refactor: delete API 2025-04-20 23:08:10 +02:00
Ravinou
35ad73fd23
refactor: storage API 2025-04-20 23:08:10 +02:00
Ravinou
dac0c41df4
refactor: status API + misc 2025-04-20 23:08:10 +02:00
Ravinou
da60d50dcb
test: wizardEnv API 2025-04-20 23:08:09 +02:00
Ravinou
d753df49a0
test: apprise, email and wizard API 2025-04-20 23:08:09 +02:00
Ravinou
b40c7d7343
refactor: version API and some pages index 2025-04-20 23:08:08 +02:00
Ravinou
73c8350442
refactor: user settings API 2025-04-20 23:08:08 +02:00
Ravinou
6a661a4f6a
refactor: updateAppriseServices API 2025-04-20 23:08:07 +02:00
Ravinou
c6111329de
refactor: updateAppriseMode API 2025-04-20 23:08:07 +02:00
Ravinou
8a0a69b7dc
refactor: updateAppriseAlert API 2025-04-20 23:08:06 +02:00
Ravinou
5ce6e2c19c
test: adding supertest (jest) to test API 2025-04-20 23:08:06 +02:00
Ravinou
e1cd8e1642
refactor: nodemailer types 2025-04-20 23:08:05 +02:00
Ravinou
f9856e5689
refactor: auth.ts type 2025-04-20 23:08:05 +02:00
Ravinou
70eaa38f1f
refactor: improve type for nexauth jwt 2025-04-20 23:08:04 +02:00
Ravinou
acdaaffc16
refactor: sendTestEmail API 2025-04-20 23:08:04 +02:00
Ravinou
b266787295
refactor: sendTestApprise API 2025-04-20 23:08:03 +02:00
Ravinou
7625e5af02
refactor: getEmailAlert API 2025-04-20 23:08:03 +02:00
Ravinou
73e35295dc
chore: 🧹 add date-fns lib 2025-04-20 23:08:03 +02:00
Ravinou
313a2f30f9
refactor: getAppriseServices API 2025-04-20 23:07:29 +02:00
Ravinou
21330fa672
refactor: getAppriseMode API 2025-04-20 23:07:29 +02:00
Ravinou
7aa47195f1
refactor: getAppriseAlert API 2025-04-20 23:07:28 +02:00
Ravinou
086ae6dad3
fix: 🐛 all notifications methods can be undefined (old versions) 2025-04-20 23:07:28 +02:00
Ravinou
67861260f8
refactor: index for account and monitoring page 2025-04-20 23:07:27 +02:00
Ravinou
d7bd79b5b4
refactor: _app, 404 and index 2025-04-20 23:07:27 +02:00
Ravinou
0c4d5a898b
refactor: login page 2025-04-20 23:07:26 +02:00
Ravinou
46b923da77
refactor: password and username components 2025-04-20 23:07:26 +02:00
Ravinou
80277dbe75
refactor: integrations form 2025-04-20 23:07:25 +02:00
Ravinou
448781c3c3
refactor: email setting 2025-04-20 23:07:25 +02:00
Ravinou
c4f59c905b
refactor: user settings 2025-04-20 23:07:24 +02:00
Ravinou
8a64fe16da
refactor: email alert settings 2025-04-20 23:07:24 +02:00
Ravinou
940367e6b2
refactor: apprise alert and switch component 2025-04-20 23:07:24 +02:00
Ravinou
233b621bc7
refactor: apprise services form 2025-04-20 23:07:23 +02:00
Ravinou
0d3377baa6
refactor: apprise mode component 2025-04-20 23:07:23 +02:00
Ravinou
d66e7a2263
refactor: component repoList 2025-04-20 23:07:22 +02:00
Ravinou
b32318ccc7
refactor: storage chart 2025-04-20 23:07:22 +02:00
Ravinou
349275b908
refactor: switch and storagebar ui 2025-04-20 23:07:21 +02:00
Ravinou
12de337017
refactor: layout 2025-04-20 23:07:21 +02:00
Ravinou
d245e30af7
refactor: wizard 2025-04-20 23:07:20 +02:00
Ravinou
7b6d1a2785
refactor: info component 2025-04-20 23:07:20 +02:00
Ravinou
f228117720
refactor: error component 2025-04-20 23:07:19 +02:00
Ravinou
d9500df622
refactor: copybutton component 2025-04-20 23:07:19 +02:00
Ravinou
4f175114ff
refactor: components Repo and auth config 2025-04-20 23:07:19 +02:00
Ravinou
e323cdd9b8
refactor: component Repo in TS 2025-04-20 23:07:18 +02:00
Ravinou
5e66de2f64
config: 🔧 components and containers to TS 2025-04-20 23:07:17 +02:00
Ravinou
e485d6f394
config: 🔧 typescript init 2025-04-20 23:07:17 +02:00
Ravinou
cd24d4479f
Merge pull request #442 from Ravinou/dependabot/npm_and_yarn/commitlint/cli-19.8.0
build(deps-dev): bump @commitlint/cli from 19.7.1 to 19.8.0
2025-04-20 22:57:27 +02:00
Ravinou
d84215df4b
Merge pull request #444 from Ravinou/dependabot/npm_and_yarn/eslint-config-next-15.3.1
build(deps-dev): bump eslint-config-next from 15.1.6 to 15.3.1
2025-04-20 22:57:14 +02:00
dependabot[bot]
a5f3530431
build(deps-dev): bump eslint-config-next from 15.1.6 to 15.3.1
Bumps [eslint-config-next](https://github.com/vercel/next.js/tree/HEAD/packages/eslint-config-next) from 15.1.6 to 15.3.1.
- [Release notes](https://github.com/vercel/next.js/releases)
- [Changelog](https://github.com/vercel/next.js/blob/canary/release.js)
- [Commits](https://github.com/vercel/next.js/commits/v15.3.1/packages/eslint-config-next)

---
updated-dependencies:
- dependency-name: eslint-config-next
  dependency-version: 15.3.1
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-04-17 19:15:35 +00:00
dependabot[bot]
5da8e61b8f
build(deps-dev): bump @commitlint/cli from 19.7.1 to 19.8.0
Bumps [@commitlint/cli](https://github.com/conventional-changelog/commitlint/tree/HEAD/@commitlint/cli) from 19.7.1 to 19.8.0.
- [Release notes](https://github.com/conventional-changelog/commitlint/releases)
- [Changelog](https://github.com/conventional-changelog/commitlint/blob/master/@commitlint/cli/CHANGELOG.md)
- [Commits](https://github.com/conventional-changelog/commitlint/commits/v19.8.0/@commitlint/cli)

---
updated-dependencies:
- dependency-name: "@commitlint/cli"
  dependency-version: 19.8.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-04-10 19:17:52 +00:00
Ravinou
82aa9015c8
Merge pull request #437 from Ravinou/dependabot/npm_and_yarn/bcryptjs-3.0.2
build(deps): bump bcryptjs from 2.4.3 to 3.0.2
2025-04-10 10:53:24 +02:00
Ravinou
42a6f0f551
Merge pull request #440 from Ravinou/dependabot/npm_and_yarn/next-15.2.5
build(deps): bump next from 15.2.3 to 15.2.5
2025-04-10 10:52:38 +02:00
dependabot[bot]
e984dcf17b
build(deps): bump next from 15.2.3 to 15.2.5
Bumps [next](https://github.com/vercel/next.js) from 15.2.3 to 15.2.5.
- [Release notes](https://github.com/vercel/next.js/releases)
- [Changelog](https://github.com/vercel/next.js/blob/canary/release.js)
- [Commits](https://github.com/vercel/next.js/compare/v15.2.3...v15.2.5)

---
updated-dependencies:
- dependency-name: next
  dependency-version: 15.2.5
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-04-09 19:51:29 +00:00
dependabot[bot]
4e78e65d2d
build(deps): bump bcryptjs from 2.4.3 to 3.0.2
Bumps [bcryptjs](https://github.com/dcodeIO/bcrypt.js) from 2.4.3 to 3.0.2.
- [Release notes](https://github.com/dcodeIO/bcrypt.js/releases)
- [Commits](https://github.com/dcodeIO/bcrypt.js/compare/2.4.3...v3.0.2)

---
updated-dependencies:
- dependency-name: bcryptjs
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-03-31 20:34:28 +00:00
Ravinou
e652a95a0b
Merge pull request #419 from Ravinou/dependabot/npm_and_yarn/react-select-5.10.1
build(deps): bump react-select from 5.10.0 to 5.10.1
2025-03-30 15:42:15 +02:00
Ravinou
ff4a676f32
Merge pull request #430 from Ravinou/dependabot/npm_and_yarn/commitlint/config-conventional-19.8.0
build(deps-dev): bump @commitlint/config-conventional from 19.7.1 to 19.8.0
2025-03-30 15:42:05 +02:00
Ravinou
1ba028ad14
Merge pull request #431 from Ravinou/dependabot/npm_and_yarn/prettier-3.5.3
build(deps-dev): bump prettier from 3.5.1 to 3.5.3
2025-03-30 15:41:43 +02:00
dependabot[bot]
b501cbe93c
build(deps-dev): bump prettier from 3.5.1 to 3.5.3
Bumps [prettier](https://github.com/prettier/prettier) from 3.5.1 to 3.5.3.
- [Release notes](https://github.com/prettier/prettier/releases)
- [Changelog](https://github.com/prettier/prettier/blob/main/CHANGELOG.md)
- [Commits](https://github.com/prettier/prettier/compare/3.5.1...3.5.3)

---
updated-dependencies:
- dependency-name: prettier
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-03-24 20:11:15 +00:00
dependabot[bot]
85eb0891c6
build(deps-dev): bump @commitlint/config-conventional
Bumps [@commitlint/config-conventional](https://github.com/conventional-changelog/commitlint/tree/HEAD/@commitlint/config-conventional) from 19.7.1 to 19.8.0.
- [Release notes](https://github.com/conventional-changelog/commitlint/releases)
- [Changelog](https://github.com/conventional-changelog/commitlint/blob/master/@commitlint/config-conventional/CHANGELOG.md)
- [Commits](https://github.com/conventional-changelog/commitlint/commits/v19.8.0/@commitlint/config-conventional)

---
updated-dependencies:
- dependency-name: "@commitlint/config-conventional"
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-03-24 20:10:18 +00:00
dependabot[bot]
b29c6ba7b3
build(deps): bump react-select from 5.10.0 to 5.10.1
Bumps [react-select](https://github.com/JedWatson/react-select) from 5.10.0 to 5.10.1.
- [Release notes](https://github.com/JedWatson/react-select/releases)
- [Changelog](https://github.com/JedWatson/react-select/blob/master/docs/CHANGELOG.md)
- [Commits](https://github.com/JedWatson/react-select/compare/react-select@5.10.0...react-select@5.10.1)

---
updated-dependencies:
- dependency-name: react-select
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-03-22 09:44:59 +00:00
Ravinou
3afeeb7134
Merge pull request #421 from Ravinou/dependabot/npm_and_yarn/swr-2.3.3
build(deps): bump swr from 2.3.0 to 2.3.3
2025-03-22 10:44:02 +01:00
Ravinou
70faeba69a
Merge pull request #429 from Ravinou/dependabot/npm_and_yarn/next-15.2.3
build(deps): bump next from 15.1.6 to 15.2.3
2025-03-22 10:43:50 +01:00
Ravinou
2a862e23bd
🥇 Thanking a new sponsor 🥇
Thank you very much @fphammerle for sponsoring ! This commit is dedicated to thanking you. You give me strength to continue the work! ❤️
2025-03-19 21:01:17 +01:00
dependabot[bot]
7477dcfdbd
build(deps): bump next from 15.1.6 to 15.2.3
Bumps [next](https://github.com/vercel/next.js) from 15.1.6 to 15.2.3.
- [Release notes](https://github.com/vercel/next.js/releases)
- [Changelog](https://github.com/vercel/next.js/blob/canary/release.js)
- [Commits](https://github.com/vercel/next.js/compare/v15.1.6...v15.2.3)

---
updated-dependencies:
- dependency-name: next
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-03-18 19:57:35 +00:00
Ravinou
fa3e2067c9
Merge pull request #423 from Ravinou/doc/readme-update
doc: 📚 update readme with new logo
2025-03-08 11:22:30 +01:00
Ravinou
2664f2b3d7
doc: 📚 update readme with new logo 2025-03-08 11:17:59 +01:00
dependabot[bot]
08bb4ebe70
build(deps): bump swr from 2.3.0 to 2.3.3
Bumps [swr](https://github.com/vercel/swr) from 2.3.0 to 2.3.3.
- [Release notes](https://github.com/vercel/swr/releases)
- [Commits](https://github.com/vercel/swr/compare/v2.3.0...v2.3.3)

---
updated-dependencies:
- dependency-name: swr
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-03-07 19:43:07 +00:00
Ravinou
edbaf23f3f
Merge pull request #413 from Ravinou/dependabot/npm_and_yarn/tabler/icons-react-3.30.0
build(deps): bump @tabler/icons-react from 3.29.0 to 3.30.0
2025-03-02 14:07:18 +01:00
Ravinou
53d749e529
Merge pull request #414 from Ravinou/dependabot/npm_and_yarn/prettier-3.5.1
build(deps-dev): bump prettier from 3.4.2 to 3.5.1
2025-02-15 19:43:39 +01:00
dependabot[bot]
2bab3f1180
build(deps-dev): bump prettier from 3.4.2 to 3.5.1
Bumps [prettier](https://github.com/prettier/prettier) from 3.4.2 to 3.5.1.
- [Release notes](https://github.com/prettier/prettier/releases)
- [Changelog](https://github.com/prettier/prettier/blob/main/CHANGELOG.md)
- [Commits](https://github.com/prettier/prettier/compare/3.4.2...3.5.1)

---
updated-dependencies:
- dependency-name: prettier
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-02-13 20:02:33 +00:00
dependabot[bot]
d003d2173b
build(deps): bump @tabler/icons-react from 3.29.0 to 3.30.0
Bumps [@tabler/icons-react](https://github.com/tabler/tabler-icons/tree/HEAD/packages/icons-react) from 3.29.0 to 3.30.0.
- [Release notes](https://github.com/tabler/tabler-icons/releases)
- [Commits](https://github.com/tabler/tabler-icons/commits/v3.30.0/packages/icons-react)

---
updated-dependencies:
- dependency-name: "@tabler/icons-react"
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-02-11 20:01:29 +00:00
Ravinou
4e0dc479bd
Merge pull request #407 from Ravinou/dependabot/npm_and_yarn/commitlint/cli-19.7.1
build(deps-dev): bump @commitlint/cli from 19.6.1 to 19.7.1
2025-02-08 16:56:32 +01:00
dependabot[bot]
e1263867f8
build(deps-dev): bump @commitlint/cli from 19.6.1 to 19.7.1
Bumps [@commitlint/cli](https://github.com/conventional-changelog/commitlint/tree/HEAD/@commitlint/cli) from 19.6.1 to 19.7.1.
- [Release notes](https://github.com/conventional-changelog/commitlint/releases)
- [Changelog](https://github.com/conventional-changelog/commitlint/blob/master/@commitlint/cli/CHANGELOG.md)
- [Commits](https://github.com/conventional-changelog/commitlint/commits/v19.7.1/@commitlint/cli)

---
updated-dependencies:
- dependency-name: "@commitlint/cli"
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-02-08 10:34:15 +00:00
Ravinou
f7b7f62069
Merge pull request #408 from Ravinou/dependabot/npm_and_yarn/commitlint/config-conventional-19.7.1
build(deps-dev): bump @commitlint/config-conventional from 19.6.0 to 19.7.1
2025-02-08 11:33:10 +01:00
dependabot[bot]
31a1ad9769
build(deps-dev): bump @commitlint/config-conventional
Bumps [@commitlint/config-conventional](https://github.com/conventional-changelog/commitlint/tree/HEAD/@commitlint/config-conventional) from 19.6.0 to 19.7.1.
- [Release notes](https://github.com/conventional-changelog/commitlint/releases)
- [Changelog](https://github.com/conventional-changelog/commitlint/blob/master/@commitlint/config-conventional/CHANGELOG.md)
- [Commits](https://github.com/conventional-changelog/commitlint/commits/v19.7.1/@commitlint/config-conventional)

---
updated-dependencies:
- dependency-name: "@commitlint/config-conventional"
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-02-03 19:15:36 +00:00
Ravinou
09f2b9b599
Merge pull request #405 from Ravinou/dependabot/npm_and_yarn/nodemailer-6.10.0
build(deps): bump nodemailer from 6.9.16 to 6.10.0
2025-02-01 14:53:12 +01:00
Ravinou
6eedd52a70
Merge pull request #406 from Ravinou/dependabot/npm_and_yarn/react-select-5.10.0
build(deps): bump react-select from 5.9.0 to 5.10.0
2025-01-31 21:57:56 +01:00
dependabot[bot]
b22dcdaa4d
build(deps): bump react-select from 5.9.0 to 5.10.0
Bumps [react-select](https://github.com/JedWatson/react-select) from 5.9.0 to 5.10.0.
- [Release notes](https://github.com/JedWatson/react-select/releases)
- [Changelog](https://github.com/JedWatson/react-select/blob/master/docs/CHANGELOG.md)
- [Commits](https://github.com/JedWatson/react-select/compare/react-select@5.9.0...react-select@5.10.0)

---
updated-dependencies:
- dependency-name: react-select
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-01-27 19:19:07 +00:00
dependabot[bot]
1b6c0ae4be
build(deps): bump nodemailer from 6.9.16 to 6.10.0
Bumps [nodemailer](https://github.com/nodemailer/nodemailer) from 6.9.16 to 6.10.0.
- [Release notes](https://github.com/nodemailer/nodemailer/releases)
- [Changelog](https://github.com/nodemailer/nodemailer/blob/master/CHANGELOG.md)
- [Commits](https://github.com/nodemailer/nodemailer/compare/v6.9.16...v6.10.0)

---
updated-dependencies:
- dependency-name: nodemailer
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-01-27 19:18:57 +00:00
Ravinou
39a8335787
Merge pull request #402 from Ravinou/dependabot/npm_and_yarn/next-15.1.6
build(deps): bump next from 15.1.4 to 15.1.6
2025-01-26 11:36:40 +01:00
Ravinou
28ae1ff42c
Merge pull request #404 from Ravinou/dependabot/npm_and_yarn/eslint-config-next-15.1.6
build(deps-dev): bump eslint-config-next from 15.1.4 to 15.1.6
2025-01-26 11:36:31 +01:00
dependabot[bot]
36bc28d85a
build(deps): bump next from 15.1.4 to 15.1.6
Bumps [next](https://github.com/vercel/next.js) from 15.1.4 to 15.1.6.
- [Release notes](https://github.com/vercel/next.js/releases)
- [Changelog](https://github.com/vercel/next.js/blob/canary/release.js)
- [Commits](https://github.com/vercel/next.js/compare/v15.1.4...v15.1.6)

---
updated-dependencies:
- dependency-name: next
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-01-25 14:58:10 +00:00
Ravinou
ed247c592f
Merge pull request #403 from Ravinou/dependabot/npm_and_yarn/tabler/icons-react-3.29.0
build(deps): bump @tabler/icons-react from 3.28.1 to 3.29.0
2025-01-25 15:56:57 +01:00
dependabot[bot]
5150257441
build(deps-dev): bump eslint-config-next from 15.1.4 to 15.1.6
Bumps [eslint-config-next](https://github.com/vercel/next.js/tree/HEAD/packages/eslint-config-next) from 15.1.4 to 15.1.6.
- [Release notes](https://github.com/vercel/next.js/releases)
- [Changelog](https://github.com/vercel/next.js/blob/canary/release.js)
- [Commits](https://github.com/vercel/next.js/commits/v15.1.6/packages/eslint-config-next)

---
updated-dependencies:
- dependency-name: eslint-config-next
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-01-22 19:28:15 +00:00
dependabot[bot]
22779c590e
build(deps): bump @tabler/icons-react from 3.28.1 to 3.29.0
Bumps [@tabler/icons-react](https://github.com/tabler/tabler-icons/tree/HEAD/packages/icons-react) from 3.28.1 to 3.29.0.
- [Release notes](https://github.com/tabler/tabler-icons/releases)
- [Commits](https://github.com/tabler/tabler-icons/commits/v3.29.0/packages/icons-react)

---
updated-dependencies:
- dependency-name: "@tabler/icons-react"
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-01-22 19:28:04 +00:00
Ravinou
c3d1469d5a
Merge pull request #394 from Ravinou/dependabot/npm_and_yarn/next-15.1.4
build(deps): bump next from 15.1.3 to 15.1.4
2025-01-17 20:05:27 +01:00
Ravinou
8e80bfa49c
Merge pull request #395 from Ravinou/dependabot/npm_and_yarn/eslint-config-next-15.1.4
build(deps-dev): bump eslint-config-next from 15.1.3 to 15.1.4
2025-01-17 20:05:16 +01:00
dependabot[bot]
5af9e0e2e3
build(deps-dev): bump eslint-config-next from 15.1.3 to 15.1.4
Bumps [eslint-config-next](https://github.com/vercel/next.js/tree/HEAD/packages/eslint-config-next) from 15.1.3 to 15.1.4.
- [Release notes](https://github.com/vercel/next.js/releases)
- [Changelog](https://github.com/vercel/next.js/blob/canary/release.js)
- [Commits](https://github.com/vercel/next.js/commits/v15.1.4/packages/eslint-config-next)

---
updated-dependencies:
- dependency-name: eslint-config-next
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-01-13 19:51:55 +00:00
dependabot[bot]
2ac4fa3b2a
build(deps): bump next from 15.1.3 to 15.1.4
Bumps [next](https://github.com/vercel/next.js) from 15.1.3 to 15.1.4.
- [Release notes](https://github.com/vercel/next.js/releases)
- [Changelog](https://github.com/vercel/next.js/blob/canary/release.js)
- [Commits](https://github.com/vercel/next.js/compare/v15.1.3...v15.1.4)

---
updated-dependencies:
- dependency-name: next
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-01-13 19:51:42 +00:00
Ravinou
a19bb130f0
Merge pull request #392 from Ravinou/dependabot/npm_and_yarn/tabler/icons-react-3.28.1
build(deps): bump @tabler/icons-react from 3.26.0 to 3.28.1
2025-01-12 19:19:11 +01:00
Ravinou
307b2a5676
Merge pull request #386 from Ravinou/dependabot/npm_and_yarn/commitlint/cli-19.6.1
build(deps-dev): bump @commitlint/cli from 19.6.0 to 19.6.1
2025-01-11 11:16:17 +01:00
Ravinou
b8b20ebc7c
Merge pull request #393 from Ravinou/dependabot/npm_and_yarn/uuid-11.0.5
build(deps): bump uuid from 11.0.3 to 11.0.5
2025-01-11 11:15:55 +01:00
dependabot[bot]
1dca17b50f
build(deps): bump uuid from 11.0.3 to 11.0.5
Bumps [uuid](https://github.com/uuidjs/uuid) from 11.0.3 to 11.0.5.
- [Release notes](https://github.com/uuidjs/uuid/releases)
- [Changelog](https://github.com/uuidjs/uuid/blob/main/CHANGELOG.md)
- [Commits](https://github.com/uuidjs/uuid/compare/v11.0.3...v11.0.5)

---
updated-dependencies:
- dependency-name: uuid
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-01-10 20:02:10 +00:00
dependabot[bot]
7415039a2b
build(deps): bump @tabler/icons-react from 3.26.0 to 3.28.1
Bumps [@tabler/icons-react](https://github.com/tabler/tabler-icons/tree/HEAD/packages/icons-react) from 3.26.0 to 3.28.1.
- [Release notes](https://github.com/tabler/tabler-icons/releases)
- [Commits](https://github.com/tabler/tabler-icons/commits/v3.28.1/packages/icons-react)

---
updated-dependencies:
- dependency-name: "@tabler/icons-react"
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-01-08 19:46:09 +00:00
Ravinou
333d7fa119
publish: 📦 release 2.4.4 2025-01-05 13:41:24 +01:00
Ravinou
7c3632a131
Merge pull request #389 from Ravinou/develop
fix: 🐛 allow smtp settings without credentials #364
2025-01-05 13:35:31 +01:00
Ravinou
4d32f1e002
fix: 🐛 allow smtp settings without credentials #364 2025-01-05 11:37:57 +01:00
Ravinou
4f9234e325
Merge pull request #388 from Ravinou/dependabot/npm_and_yarn/react-chartjs-2-5.3.0
build(deps): bump react-chartjs-2 from 5.2.0 to 5.3.0
2025-01-03 17:19:53 +01:00
dependabot[bot]
9a33efc121
build(deps): bump react-chartjs-2 from 5.2.0 to 5.3.0
Bumps [react-chartjs-2](https://github.com/reactchartjs/react-chartjs-2) from 5.2.0 to 5.3.0.
- [Release notes](https://github.com/reactchartjs/react-chartjs-2/releases)
- [Changelog](https://github.com/reactchartjs/react-chartjs-2/blob/master/CHANGELOG.md)
- [Commits](https://github.com/reactchartjs/react-chartjs-2/compare/v5.2.0...v5.3.0)

---
updated-dependencies:
- dependency-name: react-chartjs-2
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-01-01 19:15:17 +00:00
dependabot[bot]
f5bbae9c0d
build(deps-dev): bump @commitlint/cli from 19.6.0 to 19.6.1
Bumps [@commitlint/cli](https://github.com/conventional-changelog/commitlint/tree/HEAD/@commitlint/cli) from 19.6.0 to 19.6.1.
- [Release notes](https://github.com/conventional-changelog/commitlint/releases)
- [Changelog](https://github.com/conventional-changelog/commitlint/blob/master/@commitlint/cli/CHANGELOG.md)
- [Commits](https://github.com/conventional-changelog/commitlint/commits/v19.6.1/@commitlint/cli)

---
updated-dependencies:
- dependency-name: "@commitlint/cli"
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-01-01 17:30:42 +00:00
Ravinou
2d2446ea51
Merge pull request #387 from Ravinou/dependabot/npm_and_yarn/eslint-config-next-15.1.3
build(deps-dev): bump eslint-config-next from 15.1.2 to 15.1.3
2025-01-01 18:29:42 +01:00
Ravinou
97d909aaed
Merge pull request #382 from Ravinou/dependabot/npm_and_yarn/next-15.1.3
build(deps): bump next from 15.1.2 to 15.1.3
2025-01-01 18:29:30 +01:00
dependabot[bot]
0198051eb0
build(deps-dev): bump eslint-config-next from 15.1.2 to 15.1.3
Bumps [eslint-config-next](https://github.com/vercel/next.js/tree/HEAD/packages/eslint-config-next) from 15.1.2 to 15.1.3.
- [Release notes](https://github.com/vercel/next.js/releases)
- [Changelog](https://github.com/vercel/next.js/blob/canary/release.js)
- [Commits](https://github.com/vercel/next.js/commits/v15.1.3/packages/eslint-config-next)

---
updated-dependencies:
- dependency-name: eslint-config-next
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-12-30 19:13:42 +00:00
dependabot[bot]
28c7e60481
build(deps): bump next from 15.1.2 to 15.1.3
Bumps [next](https://github.com/vercel/next.js) from 15.1.2 to 15.1.3.
- [Release notes](https://github.com/vercel/next.js/releases)
- [Changelog](https://github.com/vercel/next.js/blob/canary/release.js)
- [Commits](https://github.com/vercel/next.js/compare/v15.1.2...v15.1.3)

---
updated-dependencies:
- dependency-name: next
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-12-29 11:25:01 +00:00
Ravinou
cedcdb2418
Merge pull request #383 from Ravinou/dependabot/npm_and_yarn/swr-2.3.0
build(deps): bump swr from 2.2.5 to 2.3.0
2024-12-29 12:24:21 +01:00
Ravinou
55fd807e8e
Merge pull request #384 from Ravinou/dependabot/npm_and_yarn/tabler/icons-react-3.26.0
build(deps): bump @tabler/icons-react from 3.24.0 to 3.26.0
2024-12-29 12:23:50 +01:00
dependabot[bot]
c546bc7bc2
build(deps): bump @tabler/icons-react from 3.24.0 to 3.26.0
Bumps [@tabler/icons-react](https://github.com/tabler/tabler-icons/tree/HEAD/packages/icons-react) from 3.24.0 to 3.26.0.
- [Release notes](https://github.com/tabler/tabler-icons/releases)
- [Commits](https://github.com/tabler/tabler-icons/commits/v3.26.0/packages/icons-react)

---
updated-dependencies:
- dependency-name: "@tabler/icons-react"
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-12-26 19:53:22 +00:00
dependabot[bot]
c8dbaac92e
build(deps): bump swr from 2.2.5 to 2.3.0
Bumps [swr](https://github.com/vercel/swr) from 2.2.5 to 2.3.0.
- [Release notes](https://github.com/vercel/swr/releases)
- [Commits](https://github.com/vercel/swr/compare/v2.2.5...v2.3.0)

---
updated-dependencies:
- dependency-name: swr
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-12-26 19:53:13 +00:00
Ravinou
0c407cd732
publish: 📦 release 2.4.3 2024-12-26 17:51:25 +01:00
Ravinou
1e08e64ce6
config: 🔧 prepare husky to v10 2024-12-26 17:51:08 +01:00
Ravinou
8e9fa1eb56
Merge pull request #380 from Ravinou/develop
release 2.4.3
2024-12-26 16:24:41 +01:00
Ravinou
66047df78a
config: 🔧 update CI/CD to include commit version on develop version 2024-12-26 15:48:13 +01:00
Ravinou
96ae9b8f65
feat: include 'id' and 'repositoryName' in 'add' API response #342 2024-12-26 14:49:20 +01:00
Ravinou
88a80f42e8
fix: 🐛 allow self-signed cert and no-auth for smtp settings #367 #364 2024-12-26 14:19:13 +01:00
Ravinou
0d645fc461
Merge pull request #373 from Ravinou/dependabot/npm_and_yarn/react-select-5.9.0
build(deps): bump react-select from 5.8.3 to 5.9.0
2024-12-26 10:37:13 +01:00
dependabot[bot]
bdc0104c9a
build(deps): bump react-select from 5.8.3 to 5.9.0
Bumps [react-select](https://github.com/JedWatson/react-select) from 5.8.3 to 5.9.0.
- [Release notes](https://github.com/JedWatson/react-select/releases)
- [Changelog](https://github.com/JedWatson/react-select/blob/master/docs/CHANGELOG.md)
- [Commits](https://github.com/JedWatson/react-select/compare/react-select@5.8.3...react-select@5.9.0)

---
updated-dependencies:
- dependency-name: react-select
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-12-26 09:26:15 +00:00
Ravinou
0935409d08
Merge pull request #379 from Ravinou/dependabot/npm_and_yarn/react-hook-form-7.54.2
build(deps): bump react-hook-form from 7.54.0 to 7.54.2
2024-12-26 10:25:11 +01:00
Ravinou
a70e39bd6d
Merge pull request #377 from Ravinou/dependabot/npm_and_yarn/eslint-config-next-15.1.2
build(deps-dev): bump eslint-config-next from 15.0.4 to 15.1.2
2024-12-26 10:24:55 +01:00
Ravinou
904eb1db60
Merge pull request #378 from Ravinou/dependabot/npm_and_yarn/next-15.1.2
build(deps): bump next from 15.0.4 to 15.1.2
2024-12-26 10:24:40 +01:00
dependabot[bot]
94ce693c54
build(deps): bump react-hook-form from 7.54.0 to 7.54.2
Bumps [react-hook-form](https://github.com/react-hook-form/react-hook-form) from 7.54.0 to 7.54.2.
- [Release notes](https://github.com/react-hook-form/react-hook-form/releases)
- [Changelog](https://github.com/react-hook-form/react-hook-form/blob/master/CHANGELOG.md)
- [Commits](https://github.com/react-hook-form/react-hook-form/compare/v7.54.0...v7.54.2)

---
updated-dependencies:
- dependency-name: react-hook-form
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-12-23 19:26:31 +00:00
dependabot[bot]
14ba99028e
build(deps): bump next from 15.0.4 to 15.1.2
Bumps [next](https://github.com/vercel/next.js) from 15.0.4 to 15.1.2.
- [Release notes](https://github.com/vercel/next.js/releases)
- [Changelog](https://github.com/vercel/next.js/blob/canary/release.js)
- [Commits](https://github.com/vercel/next.js/compare/v15.0.4...v15.1.2)

---
updated-dependencies:
- dependency-name: next
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-12-19 19:15:01 +00:00
dependabot[bot]
b2c9d6d9ae
build(deps-dev): bump eslint-config-next from 15.0.4 to 15.1.2
Bumps [eslint-config-next](https://github.com/vercel/next.js/tree/HEAD/packages/eslint-config-next) from 15.0.4 to 15.1.2.
- [Release notes](https://github.com/vercel/next.js/releases)
- [Changelog](https://github.com/vercel/next.js/blob/canary/release.js)
- [Commits](https://github.com/vercel/next.js/commits/v15.1.2/packages/eslint-config-next)

---
updated-dependencies:
- dependency-name: eslint-config-next
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-12-19 19:14:29 +00:00
194 changed files with 14130 additions and 11250 deletions

View file

@ -1,4 +1,4 @@
export default {
const config = {
extends: ['@commitlint/config-conventional'],
rules: {
'type-enum': [
@ -19,8 +19,12 @@ export default {
'ui',
'wip',
'publish',
'docker',
'WIP',
],
],
},
ignores: [(message) => message.includes('WIP'), (message) => message.includes('wip')],
};
export default config;

View file

@ -23,8 +23,6 @@ CONFIG_PATH=./config
SSH_PATH=./ssh
SSH_HOST=./ssh_host
BORG_REPOSITORY_PATH=./repos
TMP_PATH=./tmp
LOGS_PATH=./logs
## Optional variables section ##

35
.github/ISSUE_TEMPLATE/bug_report.md vendored Normal file
View file

@ -0,0 +1,35 @@
---
name: Bug report
about: Create a report a bug
title: ''
labels: ''
assignees: ''
---
**BorgWarehouse version :**
**Installation type :**
- [ ] Docker
- [ ] Baremetal (Debian/Ubuntu)
- [ ] Other environment :
-------
**Describe the bug**
A clear and concise description of what the bug is.
**To Reproduce**
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
**Expected behavior**
A clear and concise description of what you expected to happen.
**Additional context**
Add any other context about the problem here.
**Please, [BorgWarehouse's documentation](https://borgwarehouse.com/)
is up to date and comprehensive, so take the time to look for answers. You can also look for answers in the project's historical [github issues](https://github.com/Ravinou/borgwarehouse/issues?q=is%3Aissue%20state%3Aclosed). I take time to answer each issue, but it's always less time for BorgWarehouse development. Thanks in advance.**

21
.github/ISSUE_TEMPLATE/i-need-help.md vendored Normal file
View file

@ -0,0 +1,21 @@
---
name: I need help
about: You need help about installation, usage, or specific cases.
title: ''
labels: help wanted
assignees: ''
---
**BorgWarehouse version :**
**Installation type :**
- [ ] Docker
- [ ] Baremetal (Debian/Ubuntu)
- [ ] Other environment :
-------
Describe your problem here.
**Please, [BorgWarehouse's documentation](https://borgwarehouse.com/)
is up to date and comprehensive, so take the time to look for answers. You can also look for answers in the project's historical [github issues](https://github.com/Ravinou/borgwarehouse/issues?q=is%3Aissue%20state%3Aclosed). I take time to answer each issue, but it's always less time for BorgWarehouse development. Thanks in advance.**

View file

@ -1,16 +1,18 @@
version: 2
updates:
- package-ecosystem: "docker"
directory: "/"
- package-ecosystem: 'docker'
directory: '/'
schedule:
interval: "daily"
- package-ecosystem: "npm"
directory: "/"
interval: 'daily'
# Note: Dependabot uses "npm" ecosystem but automatically detects pnpm-lock.yaml
# Make sure package-lock.json is gitignored to prevent confusion
- package-ecosystem: 'npm'
directory: '/'
schedule:
interval: "daily"
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: "/"
- package-ecosystem: 'github-actions'
directory: '/'
schedule:
interval: "daily"
interval: 'daily'

View file

@ -1,5 +1,8 @@
name: Bats
permissions:
contents: read
on:
push:
branches:
@ -16,7 +19,7 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v6
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3

View file

@ -1,29 +1,38 @@
name: Build and Push Docker Image for Develop Branch
on:
push:
branches:
- 'develop'
push:
branches:
- 'develop'
permissions:
contents: read
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@v6
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
docker:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v6
- name: Update package.json version
run: |
COMMIT=$(git rev-parse --short HEAD)
echo "Current Commit: $COMMIT"
jq '.version = "develop-'$COMMIT'"' package.json > package.tmp.json
mv package.tmp.json package.json
- 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@v6
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

View file

@ -1,4 +1,6 @@
name: Build and Push Docker Image
permissions:
contents: read
on:
push:
@ -10,7 +12,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v6
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx

View file

@ -5,12 +5,15 @@ on:
types:
- published
permissions:
contents: read
jobs:
docker:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v6
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx

View file

@ -1,5 +1,8 @@
name: Test to build docker container on Pull Request
permissions:
contents: read
on:
pull_request:
branches:
@ -11,7 +14,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v6
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx

View file

@ -4,19 +4,21 @@ on:
- main
- develop
pull_request:
branches: main
branches:
- main
- develop
name: "Shellcheck"
name: 'Shellcheck'
permissions: {}
jobs:
shellcheck:
name: Shellcheck
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- name: Run ShellCheck
uses: ludeeus/action-shellcheck@master
env:

63
.github/workflows/vitest.yml vendored Normal file
View file

@ -0,0 +1,63 @@
name: Vitest & ESLint CI
on:
push:
branches:
- main
- develop
pull_request:
branches:
- main
- develop
permissions:
contents: read
jobs:
test:
name: Run Vitest
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v6
- name: Setup Node.js
uses: actions/setup-node@v6
with:
node-version: 22
- name: Setup pnpm
uses: pnpm/action-setup@v4
with:
version: 9
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Run Vitest
run: pnpm run test
lint:
name: Run ESLint
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v6
- name: Setup Node.js
uses: actions/setup-node@v6
with:
node-version: 22
- name: Setup pnpm
uses: pnpm/action-setup@v4
with:
version: 9
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Run ESLint
run: pnpm exec eslint

8
.gitignore vendored
View file

@ -50,6 +50,14 @@ typings/
# Optional npm cache directory
.npm
# pnpm
.pnpm-store/
pnpm-debug.log*
# Lock files (pnpm-lock.yaml is used)
package-lock.json
yarn.lock
# Optional eslint cache
.eslintcache

View file

@ -23,41 +23,43 @@ function checkBreakingChangeInBody() {
}
function findTypeIcon() {
# get message from 1st param
message="$1"
# declare an icons for each authorized enum-type from `.commitlintrc.js`
declare -A icons
icons[build]='🤖'
icons[chore]='🧹'
icons["chore(deps)"]='🧹'
icons[config]='🔧'
icons[deploy]='🚀'
icons[doc]='📚'
icons[feat]='✨'
icons[fix]='🐛'
icons[hotfix]='🚑'
icons[i18n]='💬'
icons[publish]='📦'
icons[refactor]='⚡'
icons[revert]='⏪'
icons[test]='✅'
icons[ui]='🎨'
icons[wip]='🚧'
icons[WIP]='🚧'
if [[ "$message" =~ ^.*!:\ .* ]]; then
echo "$boomIcon"
return 0
fi
for type in "${!icons[@]}"; do
# check if message subject contains breaking change pattern
if [[ "$message" =~ ^(.*)(!:){1}(.*)$ ]]; then
echo "$boomIcon"
return 0
# else find corresponding type icon
elif [[ "$message" == "$type"* ]]; then
echo "${icons[$type]}"
return 0
fi
done
return 1
declare -A icons=(
[build]='🤖'
[chore]='🧹'
["chore(deps)"]='🧹'
[config]='🔧'
[deploy]='🚀'
[doc]='📚'
[feat]='✨'
[fix]='🐛'
[hotfix]='🚑'
[i18n]='💬'
[publish]='📦'
[refactor]='⚡'
[revert]='⏪'
[test]='✅'
[ui]='🎨'
[wip]='🚧'
[WIP]='🚧'
[docker]='🐳'
)
commit_type="${message%%:*}"
icon="${icons[$commit_type]}"
if [[ -n "$icon" ]]; then
echo "$icon"
return 0
else
return 1
fi
}
# extract original message from the first line of file

View file

@ -1,6 +1,3 @@
#!/bin/bash
. "$(dirname "$0")/_/husky.sh"
# Check if it's an amend commit
if [ "$2" = "commit" ]; then
echo "Amendment detected, appending icon..."

7
.npmrc Normal file
View file

@ -0,0 +1,7 @@
# Configuration pnpm
auto-install-peers=true
strict-peer-dependencies=false
shamefully-hoist=false
# Force pnpm usage (prevent npm/yarn)
package-manager=pnpm

View file

@ -2,7 +2,7 @@
display: flex;
align-items: center;
align-self: flex-start;
margin: auto 47px auto auto;
margin: auto 25px auto auto;
}
.icons {
@ -31,7 +31,7 @@
width: 100%;
height: 100%;
border: 1px solid #6d4aff21;
background-color: #f5f5f5;
background-color: #fafafa;
border-radius: 5px;
box-shadow: 0 0px 1px rgba(0, 0, 0, 0.1) inset;
color: #65748b;
@ -50,6 +50,7 @@
.copyValid {
margin: auto 8px auto auto;
padding: 6px 6px;
font-size: 0.95rem;
color: #6d4aff;
animation: scale-in-center 0.5s cubic-bezier(0.25, 0.46, 0.45, 0.94) both;
@ -76,7 +77,7 @@
width: 100%;
height: 100%;
border: 1px solid #6d4aff21;
background-color: #f5f5f5;
background-color: #fafafa;
border-radius: 5px;
box-shadow: 0 0px 1px rgba(0, 0, 0, 0.1) inset;
color: #65748b;

View file

@ -1,26 +1,30 @@
//Lib
import React from 'react';
import { useState } from 'react';
import classes from './QuickCommands.module.css';
import { IconSettingsAutomation, IconCopy } from '@tabler/icons-react';
import lanCommandOption from '../../../helpers/functions/lanCommandOption';
import { lanCommandOption } from '~/helpers/functions';
import { WizardEnvType } from '~/types/domain/config.types';
export default function QuickCommands(props) {
////Vars
type QuickCommandsProps = {
repositoryName: string;
wizardEnv?: WizardEnvType;
lanCommand?: boolean;
};
export default function QuickCommands(props: QuickCommandsProps) {
const wizardEnv = props.wizardEnv;
//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.lanCommand);
//State
const [isCopied, setIsCopied] = useState(false);
//Functions
const handleCopy = async () => {
// Asynchronously call copy to clipboard
navigator.clipboard
.writeText(`ssh://${wizardEnv.UNIX_USER}@${FQDN}${SSH_SERVER_PORT}/./${props.repositoryName}`)
.writeText(
`ssh://${wizardEnv?.UNIX_USER}@${FQDN}${SSH_SERVER_PORT ? SSH_SERVER_PORT : ''}/./${props.repositoryName}`
)
.then(() => {
// If successful, update the isCopied state value
setIsCopied(true);
setTimeout(() => {
setIsCopied(false);
@ -37,8 +41,8 @@ export default function QuickCommands(props) {
<div className={classes.copyValid}>Copied !</div>
) : (
<div className={classes.tooltip}>
ssh://{wizardEnv.UNIX_USER}@{FQDN}
{SSH_SERVER_PORT}/./
ssh://{wizardEnv?.UNIX_USER}@{FQDN}
{SSH_SERVER_PORT ? SSH_SERVER_PORT : ''}/./
{props.repositoryName}
</div>
)}

View file

@ -1,180 +0,0 @@
//Lib
import { useState } from 'react';
import classes from './Repo.module.css';
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 (
<>
{displayDetails ? (
<>
<div className={classes.RepoOpen}>
<div className={classes.openFlex}>
<div className={statusIndicator()} />
<div className={classes.alias}>{props.alias}</div>
{appendOnlyModeIndicator()}
{alertIndicator()}
{props.comment && (
<div className={classes.comment}>
<IconInfoCircle size={16} color='grey' />
<div className={classes.toolTip}>{props.comment}</div>
</div>
)}
<QuickCommands
repositoryName={props.repositoryName}
lanCommand={props.lanCommand}
wizardEnv={props.wizardEnv}
/>
</div>
<table className={classes.tabInfo}>
<thead>
<tr>
<th style={{ width: '15%' }}>Repository</th>
<th style={{ width: '10%' }}>Storage Size</th>
<th style={{ width: '30%' }}>Storage Used</th>
<th style={{ width: '15%' }}>Last change</th>
<th style={{ width: '5%' }}>ID</th>
<th style={{ width: '5%' }}>Edit</th>
</tr>
</thead>
<tbody>
<tr>
<th>{props.repositoryName}</th>
<th>{props.storageSize} GB</th>
<th style={{ padding: '0 4% 0 4%' }}>
<StorageBar storageUsed={props.storageUsed} storageSize={props.storageSize} />
</th>
<th>
<div className={classes.lastSave}>
{props.lastSave === 0 ? '-' : timestampConverter(props.lastSave)}
</div>
</th>
<th>#{props.id}</th>
<th>
<div className={classes.editButton}>
<IconSettings
width={24}
color='#6d4aff'
onClick={() => props.repoManageEditHandler()}
/>
</div>
</th>
</tr>
</tbody>
</table>
</div>
</>
) : (
<>
<div className={classes.RepoClose}>
<div className={classes.closeFlex}>
<div className={statusIndicator()} />
<div className={classes.alias}>{props.alias}</div>
{appendOnlyModeIndicator()}
{alertIndicator()}
{props.comment && (
<div className={classes.comment}>
<IconInfoCircle size={16} color='#637381' />
<div className={classes.toolTip}>{props.comment}</div>
</div>
)}
</div>
<div className={classes.lastSave}>
{props.lastSave === 0 ? null : timestampConverter(props.lastSave)}
<span style={{ marginLeft: '20px', color: '#637381' }}>#{props.id}</span>
</div>
</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>
)}
</>
);
}

View file

@ -13,16 +13,39 @@
overflow: visible;
/* Need to display comment on hover (which is position : absolute) */
position: relative;
background: #fff;
}
.closeFlex {
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
padding: 15px;
gap: 10px;
}
.RepoClose .lastSave {
padding: 15px;
white-space: nowrap;
}
.RepoClose .leftGroup {
display: flex;
align-items: center;
flex: 1;
min-width: 0;
gap: 10px;
}
.RepoClose .alias {
font-weight: bold;
color: #111827;
font-size: 1.05em;
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
flex: 1;
min-width: 0;
}
/* REPO OPEN */
@ -35,7 +58,6 @@
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;
@ -43,15 +65,29 @@
overflow: visible;
/* Need to display comment on hover (which is position : absolute) */
position: relative;
background: #fff;
}
.openFlex {
display: flex;
align-items: center;
align-self: flex-start;
display: block;
width: 100%;
}
.aliasFlex {
display: flex;
flex-direction: row;
align-items: center;
width: 100%;
}
.indicatorsFlex {
display: flex;
flex-direction: row;
align-items: center;
justify-content: flex-end;
width: 100%;
gap: 15px;
}
.tabInfo {
width: 100%;
overflow-wrap: break-word;
@ -59,7 +95,7 @@
background: #fff;
border-radius: 10px;
overflow: hidden;
margin: 25px auto;
margin: 15px auto;
table-layout: fixed;
}
@ -73,7 +109,7 @@
font-size: 1em;
color: #fff;
line-height: 1.2;
font-weight: normal;
font-weight: 500;
}
.tabInfo tbody tr {
@ -88,80 +124,52 @@
}
/*STATUS*/
.statusIndicatorGreen {
background: rgb(9, 255, 0);
.statusIndicatorGreen,
.statusIndicatorRed {
border-radius: 50%;
margin: 10px;
height: 15px;
width: 15px;
box-shadow: 0 0 0 0 rgb(9, 255, 0);
transform: scale(1);
animation: pulseGreen 5s infinite;
animation-delay: 1s;
height: 16px;
width: 16px;
flex-shrink: 0;
animation: pulse 5s infinite;
}
@keyframes pulseGreen {
0% {
transform: scale(0.95);
box-shadow: 0 0 0 0 rgba(17, 255, 0, 0.7);
}
10% {
transform: scale(1);
box-shadow: 0 0 0 10px rgba(17, 255, 0, 0);
}
90% {
transform: scale(0.95);
box-shadow: 0 0 0 0 rgba(17, 255, 0, 0);
}
.statusIndicatorGreen {
background: #00d26a;
box-shadow: 0 0 0 0 rgba(0, 210, 106, 0.7);
}
.statusIndicatorRed {
background: rgb(255, 0, 0);
border-radius: 50%;
margin: 10px;
height: 15px;
width: 15px;
box-shadow: 0 0 0 0 rgb(255, 0, 0);
transform: scale(1);
animation: pulseRed 5s infinite;
background: #ff3d3d;
box-shadow: 0 0 0 0 rgba(255, 61, 61, 0.7);
animation-delay: 0.5s;
}
@keyframes pulseRed {
@keyframes pulse {
0% {
transform: scale(0.95);
box-shadow: 0 0 0 0 rgba(255, 0, 0, 0.7);
box-shadow: 0 0 0 0 rgba(0, 0, 0, 0.4);
}
10% {
transform: scale(1);
box-shadow: 0 0 0 10px rgba(255, 0, 0, 0);
box-shadow: 0 0 0 10px rgba(0, 0, 0, 0);
}
90% {
transform: scale(0.95);
box-shadow: 0 0 0 0 rgba(255, 0, 0, 0);
box-shadow: 0 0 0 0 rgba(0, 0, 0, 0);
}
}
/* 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 */
@ -169,6 +177,13 @@
font-weight: bold;
color: #111827;
font-size: 1.05em;
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
}
.RepoOpen .alias {
margin-top: 5px;
}
.lastSave {
@ -184,7 +199,6 @@
display: flex;
flex-direction: row;
align-items: center;
margin-left: 10px;
}
.toolTip {
@ -227,23 +241,69 @@
/* MOBILE */
@media all and (max-width: 1000px) {
.tabInfo {
display: none;
.openFlex,
.tabInfo,
.toolTip,
.comment,
.chevron {
display: none !important;
}
.toolTip {
display: none;
}
.comment {
display: none;
}
.lastSave {
display: none;
.RepoOpen,
.RepoClose {
display: flex !important;
flex-direction: row !important;
align-items: center !important;
justify-content: space-between !important;
max-height: 65px !important;
padding: 15px !important;
margin: 20px 0 0 0 !important;
}
.closeFlex {
margin: auto;
display: flex !important;
align-items: center;
justify-content: space-between;
width: 100%;
padding: 0 !important;
margin: 0 !important;
}
.openFlex {
margin: auto;
width: auto;
.alias {
flex: 1;
min-width: 0;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
.leftGroup {
display: flex;
align-items: center;
flex: 1;
min-width: 0;
gap: 10px;
}
.rightGroup {
display: flex;
align-items: center;
gap: 10px;
flex-shrink: 0;
}
.lastSave {
display: block !important;
color: #65748b;
white-space: nowrap;
font-size: 0.85em;
flex-shrink: 0;
margin-left: 10px;
}
.appendOnlyModeIcon,
.alertIcon {
display: flex;
align-items: center;
}
}

261
Components/Repo/Repo.tsx Normal file
View file

@ -0,0 +1,261 @@
import { useState, useMemo } from 'react';
import classes from './Repo.module.css';
import {
IconSettings,
IconInfoCircle,
IconChevronDown,
IconChevronUp,
IconBellOff,
IconLockPlus,
} from '@tabler/icons-react';
import StorageBar from '../UI/StorageBar/StorageBar';
import QuickCommands from './QuickCommands/QuickCommands';
import { Repository, WizardEnvType, Optional } from '~/types';
import { fromUnixTime, formatDistanceStrict } from 'date-fns';
import useMedia from 'use-media';
type RepoProps = Omit<Repository, 'unixUser' | 'displayDetails'> & {
repoManageEditHandler: () => void;
wizardEnv: Optional<WizardEnvType>;
};
export default function Repo(props: RepoProps) {
const isMobile = useMedia({ maxWidth: 1000 });
const currentDate = useMemo(() => new Date(), []);
//Load displayDetails from LocalStorage
const displayDetailsFromLS = (): boolean => {
const key = `displayDetailsRepo${props.id}`;
try {
const storedValue = localStorage.getItem(key);
if (storedValue === null) {
const defaultValue = true;
localStorage.setItem(key, JSON.stringify(defaultValue));
return defaultValue;
}
const parsedValue = JSON.parse(storedValue);
if (typeof parsedValue === 'boolean') {
return parsedValue;
}
localStorage.removeItem(key);
return true;
} catch (error) {
localStorage.removeItem(key);
return true;
}
};
//States
const [displayDetails, setDisplayDetails] = useState(displayDetailsFromLS);
//BUTTON : Display or not repo details for ONE repo
const displayDetailsForOneHandler = (boolean: 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>
);
}
};
const mobileView = () => {
return (
<>
<div className={classes.RepoClose}>
<div className={classes.closeFlex}>
<div className={classes.leftGroup}>
<div className={statusIndicator()} />
<div className={classes.alias}>{props.alias}</div>
</div>
{appendOnlyModeIndicator()}
{alertIndicator()}
{props.comment && (
<div className={classes.comment}>
<IconInfoCircle size={16} color='#637381' />
<div className={classes.toolTip}>{props.comment}</div>
</div>
)}
<div className={classes.lastSave}>
<span
title={
props.lastSave === 0 ? undefined : fromUnixTime(props.lastSave).toLocaleString()
}
>
{props.lastSave === 0
? '-'
: formatDistanceStrict(fromUnixTime(props.lastSave), currentDate, {
addSuffix: true,
})}
</span>
</div>
</div>
</div>
</>
);
};
if (isMobile) {
return mobileView();
} else {
return (
<>
{displayDetails ? (
<>
<div className={classes.RepoOpen}>
<div className={classes.indicatorsFlex}>
<div className={statusIndicator()} />
{props.comment && (
<div className={classes.comment}>
<IconInfoCircle size={16} color='grey' />
<div className={classes.toolTip}>{props.comment}</div>
</div>
)}
{appendOnlyModeIndicator()}
{alertIndicator()}
<QuickCommands
repositoryName={props.repositoryName}
lanCommand={props.lanCommand}
wizardEnv={props.wizardEnv}
/>
</div>
<div className={classes.aliasFlex}>
<div className={classes.alias}>{props.alias}</div>
</div>
<table className={classes.tabInfo}>
<thead>
<tr>
<th style={{ width: '15%' }}>Repository</th>
<th style={{ width: '10%' }}>Storage Size</th>
<th style={{ width: '30%' }}>Storage Used</th>
<th style={{ width: '15%' }}>Last change</th>
<th style={{ width: '10%' }}>Edit</th>
</tr>
</thead>
<tbody>
<tr>
<th>{props.repositoryName}</th>
<th>{props.storageSize} GB</th>
<th style={{ padding: '0 4% 0 4%' }}>
<StorageBar storageUsed={props.storageUsed} storageSize={props.storageSize} />
</th>
<th>
<div
className={classes.lastSave}
title={
props.lastSave === 0
? undefined
: fromUnixTime(props.lastSave).toLocaleString()
}
>
{props.lastSave === 0
? '-'
: formatDistanceStrict(fromUnixTime(props.lastSave), currentDate, {
addSuffix: true,
})}
</div>
</th>
<th>
<div className={classes.editButton}>
<IconSettings
width={24}
color='#6d4aff'
onClick={() => props.repoManageEditHandler()}
/>
</div>
</th>
</tr>
</tbody>
</table>
</div>
</>
) : (
<>
<div className={classes.RepoClose}>
<div className={classes.closeFlex}>
<div className={classes.leftGroup}>
<div className={statusIndicator()} />
<div className={classes.alias}>{props.alias}</div>
</div>
{appendOnlyModeIndicator()}
{alertIndicator()}
{props.comment && (
<div className={classes.comment}>
<IconInfoCircle size={16} color='#637381' />
<div className={classes.toolTip}>{props.comment}</div>
</div>
)}
<div className={classes.lastSave}>
<span
title={
props.lastSave === 0
? undefined
: fromUnixTime(props.lastSave).toLocaleString()
}
>
{props.lastSave === 0
? '-'
: formatDistanceStrict(fromUnixTime(props.lastSave), currentDate, {
addSuffix: true,
})}
</span>
</div>
</div>
</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>
)}
</>
);
}
}

View file

@ -1,14 +1,19 @@
//Lib
import classes from './CopyButton.module.css';
import { useState } from 'react';
import { useState, ReactNode } from 'react';
import { IconChecks, IconCopy } from '@tabler/icons-react';
export default function CopyButton(props) {
//State
type CopyButtonProps = {
dataToCopy: string;
children?: ReactNode;
displayIconConfirmation?: boolean;
size?: number;
stroke?: number;
};
export default function CopyButton(props: CopyButtonProps) {
const [isCopied, setIsCopied] = useState(false);
//Function
const handleCopy = async (data) => {
const handleCopy = async (data: string) => {
navigator.clipboard
.writeText(data)
.then(() => {

View file

@ -1,6 +1,9 @@
//Lib
import classes from './Error.module.css';
export default function Error(props) {
type ErrorProps = {
message: string;
};
export default function Error(props: ErrorProps) {
return <div className={classes.errorMessage}>{props.message}</div>;
}

View file

@ -1,7 +1,13 @@
//Lib
import { ReactNode } from 'react';
import classes from './Info.module.css';
export default function Info(props) {
type InfoProps = {
message: string;
color?: string;
children?: ReactNode;
};
export default function Info(props: InfoProps) {
return (
<div className={classes.infoMessage} style={{ backgroundColor: props.color }}>
{props.message}

View file

@ -1,6 +1,5 @@
//Lib
import classes from './Footer.module.css';
import packageInfo from '../../../../package.json';
import packageInfo from '~/package.json';
function Footer() {
return (

View file

@ -27,5 +27,5 @@
font-weight: bold;
color: #6d4aff;
text-shadow: #6d4aff 0px 0px 18px;
margin-left: 20px;
margin-left: 70px;
}

View file

@ -1,14 +1,21 @@
//Lib
import Image from 'next/image';
import classes from './Header.module.css';
//Components
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}>
<Image
src='/borgwarehouse-logo-violet.svg'
alt='BorgWarehouse'
width={225}
height={40}
className={classes.logoImage}
priority
/>
</div>
<nav>
<Nav />

View file

@ -1,4 +1,3 @@
//Lib
import classes from './Nav.module.css';
import { IconUser, IconLogout } from '@tabler/icons-react';
import Link from 'next/link';
@ -6,13 +5,10 @@ import { useRouter } from 'next/router';
import { useSession, signOut } from 'next-auth/react';
export default function Nav() {
////Var
//Get the current route to light the right Item
const router = useRouter();
const currentRoute = router.pathname;
const { status, data } = useSession();
//Function
const onLogoutClickedHandler = async () => {
//This bug is open : https://github.com/nextauthjs/next-auth/issues/1542
//I put redirect to false and redirect with router.
@ -25,12 +21,12 @@ export default function Nav() {
return (
<ul className={classes.Nav}>
<li style={{ margin: '0px 15px 0px 0px' }} className={classes.account}>
<Link href='/account' className={currentRoute === '/account' ? classes.active : null}>
<Link href='/account' className={currentRoute === '/account' ? classes.active : undefined}>
<div className={classes.user}>
<div>
<IconUser size={28} />
</div>
<div className={classes.username}>{status === 'authenticated' && data.user.name}</div>
<div className={classes.username}>{status === 'authenticated' && data.user?.name}</div>
</div>
</Link>
</li>

View file

@ -1,12 +1,14 @@
//Lib
import Footer from './Footer/Footer';
import Header from './Header/Header';
import NavSide from './NavSide/NavSide';
import classes from './Layout.module.css';
import { useSession } from 'next-auth/react';
function Layout(props) {
//Var
type LayoutProps = {
children: React.ReactNode;
};
function Layout(props: LayoutProps) {
const { status } = useSession();
if (status === 'authenticated') {

View file

@ -1,21 +1,16 @@
//Lib
import classes from './NavSide.module.css';
import { IconServer, IconSettingsAutomation, IconActivityHeartbeat } from '@tabler/icons-react';
import Link from 'next/link';
import { useRouter } from 'next/router';
//Composants
export default function NavSide() {
////Var
//Get the current route to light the right Item
const router = useRouter();
const currentRoute = router.pathname;
return (
<ul className={classes.NavSide}>
<li className={classes.NavSideItem}>
<Link href='/' className={currentRoute === '/' ? classes.active : null}>
<Link href='/' className={currentRoute === '/' ? classes.active : undefined}>
<IconServer size={40} />
</Link>
<span className={classes.tooltip}>Repositories</span>
@ -23,14 +18,17 @@ export default function NavSide() {
<li className={classes.NavSideItem}>
<Link
href='/setup-wizard/1'
className={currentRoute === '/setup-wizard/[slug]' ? classes.active : null}
className={currentRoute === '/setup-wizard/[slug]' ? classes.active : undefined}
>
<IconSettingsAutomation size={40} />
</Link>
<span className={classes.tooltip}>Setup Wizard</span>
</li>
<li className={classes.NavSideItem}>
<Link href='/monitoring' className={currentRoute === '/monitoring' ? classes.active : null}>
<Link
href='/monitoring'
className={currentRoute === '/monitoring' ? classes.active : undefined}
>
<IconActivityHeartbeat size={40} />
</Link>
<span className={classes.tooltip}>Monitoring</span>

View file

@ -1,6 +1,11 @@
//Lib
import classes from './ShimmerRepoList.module.css';
const LOADING_REPO_COUNT = 5;
function ShimmerRepoItem() {
return <div className={classes.repoIsLoading} />;
}
export default function ShimmerRepoList() {
return (
<div className={classes.container}>
@ -8,11 +13,9 @@ export default function ShimmerRepoList() {
<div className={classes.buttonIsLoading} />
</div>
<div className={classes.loadingRepoContainer}>
<div className={classes.repoIsLoading} />
<div className={classes.repoIsLoading} />
<div className={classes.repoIsLoading} />
<div className={classes.repoIsLoading} />
<div className={classes.repoIsLoading} />
{Array.from({ length: LOADING_REPO_COUNT }, (_, i) => (
<ShimmerRepoItem key={i} />
))}
</div>
</div>
);

View file

@ -1,8 +1,11 @@
//Lib
import classes from './StorageBar.module.css';
export default function StorageBar(props) {
//Var
type StorageBarProps = {
storageUsed: number;
storageSize: number;
};
export default function StorageBar(props: StorageBarProps) {
//storageUsed is in kB, storageSize is in GB. Round to 1 decimal for %.
const storageUsedPercent = (((props.storageUsed / 1024 ** 2) * 100) / props.storageSize).toFixed(
1

View file

@ -1,26 +0,0 @@
//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>
</>
);
}

View file

@ -1,157 +1,84 @@
/* Wrapper styles */
.switchWrapper {
display: flex;
flex-direction: column;
gap: 8px;
margin-bottom: 20px;
}
/* Switch container */
.switch {
display: flex;
align-items: center;
gap: 10px;
}
.switchDescription {
display: flex;
margin: 8px 0px 0px 0px;
color: #6c737f;
font-size: 0.875rem;
}
.pureMaterialSwitch {
z-index: 0;
/* Label */
.switchLabel {
display: inline-flex;
align-items: center;
gap: 10px;
cursor: pointer;
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;
user-select: none;
}
/* 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%;
.switchLabel input {
display: none;
}
/* Slider */
.switchSlider {
position: relative;
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;
height: 20px;
background: #ccc;
border-radius: 12px;
transition: #ccc 0.3s ease;
}
/* 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 {
.switchSlider::after {
content: '';
position: absolute;
top: 2px;
right: 16px;
left: 2px;
width: 16px;
height: 16px;
background: #fff;
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;
transition: transform 0.3s ease;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
}
/* Checked */
.pureMaterialSwitch > input:checked {
right: -10px;
background-color: rgb(var(--pure-material-primary-rgb, 109, 74, 255));
/* Checked styles */
.switchLabel input:checked + .switchSlider {
background: #704dff;
}
.pureMaterialSwitch > input:checked + span::before {
background-color: rgba(var(--pure-material-primary-rgb, 109, 74, 255), 0.6);
.switchLabel input:checked + .switchSlider::after {
transform: translateX(20px);
}
.pureMaterialSwitch > input:checked + span::after {
background-color: rgb(var(--pure-material-primary-rgb, 109, 74, 255));
transform: translateX(16px);
/* Disabled styles */
.switchLabel input:disabled + .switchSlider {
background: #e0e0e0;
}
/* Active */
.pureMaterialSwitch > input:active {
opacity: 1;
transform: scale(0);
transition:
transform 0s,
opacity 0s;
.switchLabel input:disabled + .switchSlider::after {
background: #bdbdbd;
}
.pureMaterialSwitch > input:active + span::before {
background-color: rgba(var(--pure-material-primary-rgb, 109, 74, 255), 0.6);
/* Switch text */
.switchText {
font-size: 1rem;
color: #494b7a;
font-weight: 500;
}
.pureMaterialSwitch > input:checked:active + span::before {
background-color: rgba(var(--pure-material-onsurface-rgb, 0, 0, 0), 0.38);
/* Description */
.switchDescription {
font-size: 0.875rem;
color: #6c737f;
margin-top: 4px;
}
/* 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);
} */

View file

@ -0,0 +1,45 @@
import { Optional } from '~/types';
import classes from './Switch.module.css';
import { useLoader } from '~/contexts/LoaderContext';
import { useEffect } from 'react';
type SwitchProps = {
switchName: string;
switchDescription: string;
checked: Optional<boolean>;
disabled: boolean;
loading?: boolean;
onChange: (checked: boolean) => void;
};
export default function Switch(props: SwitchProps) {
const { start, stop } = useLoader();
useEffect(() => {
if (props.loading) {
start();
} else {
stop();
}
}, [props.loading, start, stop]);
return (
<div className={classes.switchWrapper}>
<div className={classes.switch}>
<label className={classes.switchLabel}>
<>
<input
type='checkbox'
checked={props.checked || false}
disabled={props.disabled}
onChange={(e) => props.onChange(e.target.checked)}
/>
<span className={classes.switchSlider}></span>
</>
<span className={classes.switchText}>{props.switchName}</span>
</label>
</div>
<p className={classes.switchDescription}>{props.switchDescription}</p>
</div>
);
}

View file

@ -1,4 +1,3 @@
//Lib
import React from 'react';
import classes from '../WizardStep1/WizardStep1.module.css';
import { IconDeviceDesktopAnalytics, IconTerminal2 } from '@tabler/icons-react';
@ -49,15 +48,7 @@ function WizardStep1() {
Vorta
</a>
.
<br />
Vorta runs on Linux, MacOS and Windows (via Windows Linux Subsystem (WSL)). Find the right
way to install it{' '}
<a href='https://vorta.borgbase.com/install/' target='_blank' rel='noreferrer'>
just here
</a>
.
</div>
<img src='/vorta-demo.gif' alt='Vorta GIF' />
</div>
);
}

View file

@ -1,16 +1,14 @@
//Lib
import React from 'react';
import classes from '../WizardStep1/WizardStep1.module.css';
import { IconTool, IconAlertCircle } from '@tabler/icons-react';
import { IconAlertCircle, IconTool } from '@tabler/icons-react';
import CopyButton from '../../UI/CopyButton/CopyButton';
import lanCommandOption from '../../../helpers/functions/lanCommandOption';
import { WizardStepProps } from '~/types';
import classes from '../WizardStep1/WizardStep1.module.css';
import { lanCommandOption } from '~/helpers/functions';
function WizardStep2(props) {
////Vars
function WizardStep2(props: WizardStepProps) {
const wizardEnv = props.wizardEnv;
const UNIX_USER = wizardEnv.UNIX_USER;
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 { FQDN, SSH_SERVER_PORT } = lanCommandOption(wizardEnv, props.selectedRepo?.lanCommand);
return (
<div className={classes.container}>
@ -32,10 +30,10 @@ function WizardStep2(props) {
borg init -e repokey-blake2 ssh://
{UNIX_USER}@{FQDN}
{SSH_SERVER_PORT}/./
{props.selectedOption.repositoryName}
{props.selectedRepo?.repositoryName}
</div>
<CopyButton
dataToCopy={`borg init -e repokey-blake2 ssh://${UNIX_USER}@${FQDN}${SSH_SERVER_PORT}/./${props.selectedOption.repositoryName}`}
dataToCopy={`borg init -e repokey-blake2 ssh://${UNIX_USER}@${FQDN}${SSH_SERVER_PORT}/./${props.selectedRepo?.repositoryName}`}
/>
</div>
<div className={classes.note}>
@ -71,8 +69,8 @@ function WizardStep2(props) {
<h2>Pika, Vorta...</h2>
<div className={classes.description}>
To "Initialize a new repository" or "Add existing repository", copy this into the field
"Repository URL" of your graphical client :
To &quot;Initialize a new repository&quot; or &quot;Add existing repository&quot;, copy this
into the field &quot;Repository URL&quot; of your graphical client :
<br />
<div
style={{
@ -85,10 +83,10 @@ function WizardStep2(props) {
ssh://
{UNIX_USER}@{FQDN}
{SSH_SERVER_PORT}/./
{props.selectedOption.repositoryName}
{props.selectedRepo?.repositoryName}
</div>
<CopyButton
dataToCopy={`ssh://${UNIX_USER}@${FQDN}${SSH_SERVER_PORT}/./${props.selectedOption.repositoryName}`}
dataToCopy={`ssh://${UNIX_USER}@${FQDN}${SSH_SERVER_PORT}/./${props.selectedRepo?.repositoryName}`}
/>
</div>
</div>
@ -100,19 +98,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 one of the
following key's fingerprint when you first connect :
following key&apos;s fingerprint when you first connect :
<li>
<span className={classes.sshPublicKey}>
ECDSA : {wizardEnv.SSH_SERVER_FINGERPRINT_ECDSA}
ECDSA : {wizardEnv?.SSH_SERVER_FINGERPRINT_ECDSA}
</span>
</li>
<li>
<span className={classes.sshPublicKey}>
ED25519 : {wizardEnv.SSH_SERVER_FINGERPRINT_ED25519}
ED25519 : {wizardEnv?.SSH_SERVER_FINGERPRINT_ED25519}
</span>
</li>
<li>
<span className={classes.sshPublicKey}>RSA : {wizardEnv.SSH_SERVER_FINGERPRINT_RSA}</span>
<span className={classes.sshPublicKey}>
RSA : {wizardEnv?.SSH_SERVER_FINGERPRINT_RSA}
</span>
</li>
</div>

View file

@ -1,16 +1,15 @@
//Lib
import React from 'react';
import classes from '../WizardStep1/WizardStep1.module.css';
import { IconChecks, IconPlayerPlay } from '@tabler/icons-react';
import CopyButton from '../../UI/CopyButton/CopyButton';
import lanCommandOption from '../../../helpers/functions/lanCommandOption';
import { WizardStepProps } from '~/types';
import { lanCommandOption } from '~/helpers/functions';
function WizardStep3(props) {
////Vars
function WizardStep3(props: WizardStepProps) {
const wizardEnv = props.wizardEnv;
const UNIX_USER = wizardEnv.UNIX_USER;
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 { FQDN, SSH_SERVER_PORT } = lanCommandOption(wizardEnv, props.selectedRepo?.lanCommand);
return (
<div className={classes.container}>
@ -31,11 +30,11 @@ function WizardStep3(props) {
borg create ssh://
{UNIX_USER}@{FQDN}
{SSH_SERVER_PORT}/./
{props.selectedOption.repositoryName}
{props.selectedRepo?.repositoryName}
::archive1 /your/pathToBackup
</div>
<CopyButton
dataToCopy={`borg create ssh://${UNIX_USER}@${FQDN}${SSH_SERVER_PORT}/./${props.selectedOption.repositoryName}::archive1 /your/pathToBackup`}
dataToCopy={`borg create ssh://${UNIX_USER}@${FQDN}${SSH_SERVER_PORT}/./${props.selectedRepo?.repositoryName}::archive1 /your/pathToBackup`}
/>
</div>
</div>
@ -70,10 +69,10 @@ function WizardStep3(props) {
borg check -v --progress ssh://
{UNIX_USER}@{FQDN}
{SSH_SERVER_PORT}/./
{props.selectedOption.repositoryName}
{props.selectedRepo?.repositoryName}
</div>
<CopyButton
dataToCopy={`borg check -v --progress ssh://${UNIX_USER}@${FQDN}${SSH_SERVER_PORT}/./${props.selectedOption.repositoryName}`}
dataToCopy={`borg check -v --progress ssh://${UNIX_USER}@${FQDN}${SSH_SERVER_PORT}/./${props.selectedRepo?.repositoryName}`}
/>
</div>
<li>List the remote archives with :</li>
@ -88,10 +87,10 @@ function WizardStep3(props) {
borg list ssh://
{UNIX_USER}@{FQDN}
{SSH_SERVER_PORT}/./
{props.selectedOption.repositoryName}
{props.selectedRepo?.repositoryName}
</div>
<CopyButton
dataToCopy={`borg list ssh://${UNIX_USER}@${FQDN}${SSH_SERVER_PORT}/./${props.selectedOption.repositoryName}`}
dataToCopy={`borg list ssh://${UNIX_USER}@${FQDN}${SSH_SERVER_PORT}/./${props.selectedRepo?.repositoryName}`}
/>
</div>
<li>Download a remote archive with the following command :</li>
@ -103,14 +102,14 @@ function WizardStep3(props) {
}}
>
<div className={classes.code}>
borg export-tar --tar-filter="gzip -9" ssh://
borg export-tar --tar-filter=&quot;gzip -9&quot; ssh://
{UNIX_USER}@{FQDN}
{SSH_SERVER_PORT}/./
{props.selectedOption.repositoryName}
{props.selectedRepo?.repositoryName}
::archive1 archive1.tar.gz
</div>
<CopyButton
dataToCopy={`borg export-tar --tar-filter="gzip -9" ssh://${UNIX_USER}@${FQDN}${SSH_SERVER_PORT}/./${props.selectedOption.repositoryName}::archive1 archive1.tar.gz`}
dataToCopy={`borg export-tar --tar-filter="gzip -9" ssh://${UNIX_USER}@${FQDN}${SSH_SERVER_PORT}/./${props.selectedRepo?.repositoryName}::archive1 archive1.tar.gz`}
/>
</div>
<li>Mount an archive to compare or backup some files without download all the archive :</li>
@ -125,11 +124,11 @@ function WizardStep3(props) {
borg mount ssh://
{UNIX_USER}@{FQDN}
{SSH_SERVER_PORT}/./
{props.selectedOption.repositoryName}
{props.selectedRepo?.repositoryName}
::archive1 /tmp/yourMountPoint
</div>
<CopyButton
dataToCopy={`borg mount ssh://${UNIX_USER}@${FQDN}${SSH_SERVER_PORT}/./${props.selectedOption.repositoryName}::archive1 /tmp/yourMountPoint`}
dataToCopy={`borg mount ssh://${UNIX_USER}@${FQDN}${SSH_SERVER_PORT}/./${props.selectedRepo?.repositoryName}::archive1 /tmp/yourMountPoint`}
/>
</div>
<br />

View file

@ -1,18 +1,17 @@
//Lib
import React from 'react';
import classes from '../WizardStep1/WizardStep1.module.css';
import { IconWand } from '@tabler/icons-react';
import CopyButton from '../../UI/CopyButton/CopyButton';
import lanCommandOption from '../../../helpers/functions/lanCommandOption';
import { WizardStepProps } from '~/types';
import { lanCommandOption } from '~/helpers/functions';
function WizardStep4(props) {
////Vars
function WizardStep4(props: WizardStepProps) {
const wizardEnv = props.wizardEnv;
const UNIX_USER = wizardEnv.UNIX_USER;
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 { FQDN, SSH_SERVER_PORT } = lanCommandOption(wizardEnv, props.selectedRepo?.lanCommand);
const configBorgmatic = `location:
const configBorgmatic = `
# List of source directories to backup.
source_directories:
- /your-repo-to-backup
@ -20,24 +19,21 @@ function WizardStep4(props) {
repositories:
# Paths of local or remote repositories to backup to.
- ssh://${UNIX_USER}@${FQDN}${SSH_SERVER_PORT}/./${props.selectedOption.repositoryName}
- path: ssh://${UNIX_USER}@${FQDN}${SSH_SERVER_PORT}/./${props.selectedRepo?.repositoryName}
storage:
archive_name_format: '{FQDN}-documents-{now}'
encryption_passphrase: "YOUR PASSPHRASE"
archive_name_format: '{FQDN}-documents-{now}'
encryption_passphrase: "YOUR PASSPHRASE"
retention:
# Retention policy for how many backups to keep.
keep_daily: 7
keep_weekly: 4
keep_monthly: 6
# Retention policy for how many backups to keep.
keep_daily: 7
keep_weekly: 4
keep_monthly: 6
consistency:
# List of checks to run to validate your backups.
checks:
- name: repository
- name: archives
frequency: 2 weeks
# List of checks to run to validate your backups.
checks:
- name: repository
- name: archives
frequency: 2 weeks
#hooks:
# Custom preparation scripts to run.

View file

@ -1,12 +1,17 @@
//Lib
import React from 'react';
import classes from './WizardStepBar.module.css';
import { IconChevronLeft, IconChevronRight } from '@tabler/icons-react';
function WizardStepBar(props) {
////Functions
type WizardStepBarProps = {
step: number;
setStep: (step: number) => void;
previousStepHandler: () => void;
nextStepHandler: () => void;
};
function WizardStepBar(props: WizardStepBarProps) {
//Color onClick on a step
const colorHandler = (step) => {
const colorHandler = (step: number) => {
if (step <= props.step) {
return classes.active;
} else {

View file

@ -1,4 +1,3 @@
//Lib
import {
Chart as ChartJS,
CategoryScale,
@ -10,16 +9,15 @@ import {
} from 'chart.js';
import { Bar } from 'react-chartjs-2';
import { useState, useEffect } from 'react';
import { Repository, Optional } from '~/types';
export default function StorageUsedChartBar() {
//States
const [data, setData] = useState([]);
const [data, setData] = useState<Optional<Array<Repository>>>();
//LifeCycle
useEffect(() => {
const dataFetch = async () => {
try {
const response = await fetch('/api/repo', {
const response = await fetch('/api/v1/repositories', {
method: 'GET',
headers: {
'Content-type': 'application/json',
@ -41,10 +39,10 @@ export default function StorageUsedChartBar() {
responsive: true,
plugins: {
legend: {
position: 'bottom',
position: 'bottom' as const,
},
title: {
position: 'bottom',
position: 'bottom' as const,
display: true,
text: 'Storage used for each repository',
},
@ -55,7 +53,7 @@ export default function StorageUsedChartBar() {
min: 0,
ticks: {
// Include a dollar sign in the ticks
callback: function (value) {
callback: function (value: number | string) {
return value + '%';
},
stepSize: 10,
@ -64,7 +62,7 @@ export default function StorageUsedChartBar() {
},
};
const labels = data.map((repo) => repo.alias);
const labels = data?.map((repo) => repo.alias);
const dataChart = {
labels,
@ -72,7 +70,7 @@ export default function StorageUsedChartBar() {
{
label: 'Storage used (%)',
//storageUsed is in kB, storageSize is in GB. Round to 1 decimal for %.
data: data.map((repo) =>
data: data?.map((repo) =>
(((repo.storageUsed / 1024 ** 2) * 100) / repo.storageSize).toFixed(1)
),
backgroundColor: '#704dff',

View file

@ -1,159 +0,0 @@
//Lib
import classes from './RepoList.module.css';
import React, { 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';
import { ToastContainer, toast } from 'react-toastify';
import 'react-toastify/dist/ReactToastify.css';
//Composants
import Repo from '../../Components/Repo/Repo';
import RepoManage from '../RepoManage/RepoManage';
import ShimmerRepoList from '../../Components/UI/ShimmerRepoList/ShimmerRepoList';
export default function RepoList() {
////Var
const router = useRouter();
const { mutate } = useSWRConfig();
const toastOptions = {
position: 'top-right',
autoClose: 8000,
hideProgressBar: false,
closeOnClick: true,
pauseOnHover: true,
draggable: true,
progress: undefined,
};
////Datas
//Write a fetcher function to wrap the native fetch function and return the result of a call to url in json format
const fetcher = async (url) => await fetch(url).then((res) => res.json());
const { data, error } = useSWR('/api/repo', fetcher);
////LifeCycle
//Component did mount
useEffect(() => {
//If the route is home/manage-repo/add, open the RepoAdd box.
if (router.pathname === '/manage-repo/add') {
setDisplayRepoAdd(!displayRepoAdd);
}
//If the route is home/manage-repo/edit, open the RepoAdd box.
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 [wizardEnv, setWizardEnv] = useState({});
////Functions
//Firstly, check the availability of data and condition it.
if (!data) {
//Force mutate after login (force a API GET on /api/repo to load repoList)
mutate('/api/repo');
return <ShimmerRepoList />;
}
if (error) {
toast.error('An error has occurred.', toastOptions);
return <ToastContainer />;
}
if (data.status == 500) {
toast.error('API Error !', toastOptions);
return <ToastContainer />;
}
//BUTTON : Display RepoManage component box for ADD
const manageRepoAddHandler = () => {
router.replace('/manage-repo/add');
};
//BUTTON : Display RepoManage component box for EDIT
const repoManageEditHandler = (id) => {
router.replace('/manage-repo/edit/' + id);
};
//BUTTON : Close RepoManage component box (when cross is clicked)
const closeRepoManageBoxHandler = () => {
router.replace('/');
};
// UI EFFECT : Display blur when display add repo modale
const displayBlur = () => {
if (displayRepoAdd || displayRepoEdit) {
return classes.containerBlur;
} else {
return classes.container;
}
};
//Dynamic list of repositories (with a map of Repo components)
const renderRepoList = data.repoList.map((repo, index) => {
return (
<React.Fragment key={repo.id}>
<Repo
key={repo.id}
id={repo.id}
alias={repo.alias}
status={repo.status}
lastSave={repo.lastSave}
alert={repo.alert}
repositoryName={repo.repositoryName}
storageSize={repo.storageSize}
storageUsed={repo.storageUsed}
sshPublicKey={repo.sshPublicKey}
comment={repo.comment}
lanCommand={repo.lanCommand}
appendOnlyMode={repo.appendOnlyMode}
repoManageEditHandler={() => repoManageEditHandler(repo.id)}
wizardEnv={wizardEnv}
></Repo>
</React.Fragment>
);
});
return (
<>
<div className={displayBlur()}>
<div className={classes.containerAddRepo}>
<Link
href='/manage-repo/add'
className={classes.newRepoButton}
onClick={manageRepoAddHandler}
>
<IconPlus className={classes.plusIcon} size={24} stroke={2} />
<span>Add a repository</span>
</Link>
</div>
<div className={classes.containerRepoList}>
<div className={classes.RepoList}>{renderRepoList}</div>
</div>
</div>
{displayRepoAdd ? (
<RepoManage mode='add' repoList={data.repoList} closeHandler={closeRepoManageBoxHandler} />
) : null}
{displayRepoEdit ? (
<RepoManage mode='edit' repoList={data.repoList} closeHandler={closeRepoManageBoxHandler} />
) : null}
</>
);
}

View file

@ -86,7 +86,6 @@
flex-direction: column;
width: 90%;
margin: 5px auto;
padding: 15px;
}
.unfoldButton {
@ -123,3 +122,77 @@
display: none;
}
}
/* Toolbar */
.toolbar {
display: flex;
justify-content: space-between;
align-items: center;
width: 90%;
margin: 20px auto 10px;
flex-wrap: wrap;
gap: 10px;
}
.searchInput {
padding: 10px 15px;
border: 1px solid #ccc;
border-radius: 8px;
font-size: 14px;
width: 100%;
max-width: 300px;
}
.sortIcons {
display: flex;
gap: 10px;
align-items: center;
}
.icon {
cursor: pointer;
color: #a6a6b8;
transition: transform 0.2s ease;
}
.icon:hover {
transform: scale(1.1);
color: #6d4aff;
}
.iconActive {
color: #6d4aff;
transform: scale(1.2);
}
.searchContainer {
position: relative;
display: flex;
align-items: center;
width: 100%;
max-width: 300px;
}
.searchInput {
width: 100%;
padding: 8px 32px 8px 12px;
border-radius: 8px;
border: 1px solid #ccc;
font-size: 14px;
outline: none;
background-color: white;
}
.clearButton {
position: absolute;
right: 8px;
background: transparent;
border: none;
cursor: pointer;
color: #999;
}
.clearButton:hover {
color: #333;
}

View file

@ -0,0 +1,279 @@
import classes from './RepoList.module.css';
import React, { useState, useEffect } from 'react';
import {
IconPlus,
IconSortAscendingLetters,
IconSortDescendingLetters,
IconSortAscending2,
IconSortDescending2,
IconDatabase,
IconX,
IconClock,
IconCalendarUp,
IconCalendarDown,
IconSortAscendingSmallBig,
IconSortDescendingSmallBig,
IconSortDescending2Filled,
} from '@tabler/icons-react';
import { useRouter } from 'next/router';
import Link from 'next/link';
import useSWR, { useSWRConfig } from 'swr';
import { ToastContainer, ToastOptions, toast } from 'react-toastify';
import 'react-toastify/dist/ReactToastify.css';
import Repo from '~/Components/Repo/Repo';
import RepoManage from '../RepoManage/RepoManage';
import ShimmerRepoList from '~/Components/UI/ShimmerRepoList/ShimmerRepoList';
import { Repository, WizardEnvType, Optional } from '~/types';
type SortOption =
| 'alias-asc'
| 'alias-desc'
| 'status-true'
| 'status-false'
| 'storage-used-asc'
| 'storage-used-desc'
| 'last-save-asc'
| 'last-save-desc';
export default function RepoList() {
const router = useRouter();
const { mutate } = useSWRConfig();
const [displayRepoAdd, setDisplayRepoAdd] = useState(false);
const [displayRepoEdit, setDisplayRepoEdit] = useState(false);
const [wizardEnv, setWizardEnv] = useState<Optional<WizardEnvType>>();
const [sortOption, setSortOption] = useState<SortOption>(() => {
const savedSort = localStorage.getItem('repoSort');
return (savedSort as SortOption) || 'alias-asc';
});
const [searchQuery, setSearchQuery] = useState(() => {
const savedSearch = localStorage.getItem('repoSearch');
return savedSearch || '';
});
const toastOptions: ToastOptions = {
position: 'top-right',
autoClose: 8000,
hideProgressBar: false,
closeOnClick: true,
pauseOnHover: true,
draggable: true,
progress: undefined,
};
useEffect(() => {
// eslint-disable-next-line react-hooks/set-state-in-effect
setDisplayRepoAdd(router.pathname === '/manage-repo/add');
setDisplayRepoEdit(router.pathname.startsWith('/manage-repo/edit'));
const fetchWizardEnv = async () => {
try {
const response = await fetch('/api/v1/account/wizard-env');
const data: WizardEnvType = await response.json();
setWizardEnv(data);
} catch (error) {
console.log('Fetching wizard env error');
}
};
fetchWizardEnv();
}, [router.pathname]);
const fetcher = async (url: string) => await fetch(url).then((res) => res.json());
const { data, error } = useSWR('/api/v1/repositories', fetcher);
if (!data) {
mutate('/api/v1/repositories');
return <ShimmerRepoList />;
}
if (error || data.status == 500) {
toast.error('Error loading repositories.', toastOptions);
return <ToastContainer />;
}
const handleSortChange = (option: SortOption) => {
setSortOption(option);
localStorage.setItem('repoSort', option);
};
const handleSearchChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const query = e.target.value;
setSearchQuery(query);
localStorage.setItem('repoSearch', query);
};
const getSortedRepoList = () => {
let repoList = [...data.repoList];
// Filter
if (searchQuery) {
repoList = repoList.filter((repo) =>
`${repo.alias} ${repo.comment} ${repo.repositoryName}`
.toLowerCase()
.includes(searchQuery.toLowerCase())
);
}
// Sort
switch (sortOption) {
case 'alias-asc':
return repoList.sort((a, b) => a.alias.localeCompare(b.alias));
case 'alias-desc':
return repoList.sort((a, b) => b.alias.localeCompare(a.alias));
case 'status-true':
return repoList.sort((a, b) => Number(b.status) - Number(a.status));
case 'status-false':
return repoList.sort((a, b) => Number(a.status) - Number(b.status));
case 'storage-used-asc':
return repoList.sort((a, b) => {
const aRatio = a.storageSize ? a.storageUsed / a.storageSize : 0;
const bRatio = b.storageSize ? b.storageUsed / b.storageSize : 0;
return aRatio - bRatio;
});
case 'storage-used-desc':
return repoList.sort((a, b) => {
const aRatio = a.storageSize ? a.storageUsed / a.storageSize : 0;
const bRatio = b.storageSize ? b.storageUsed / b.storageSize : 0;
return bRatio - aRatio;
});
case 'last-save-asc':
return repoList.sort((a, b) => {
const aDate = a.lastSave ? new Date(a.lastSave).getTime() : 0;
const bDate = b.lastSave ? new Date(b.lastSave).getTime() : 0;
return aDate - bDate;
});
case 'last-save-desc':
return repoList.sort((a, b) => {
const aDate = a.lastSave ? new Date(a.lastSave).getTime() : 0;
const bDate = b.lastSave ? new Date(b.lastSave).getTime() : 0;
return bDate - aDate;
});
default:
return repoList;
}
};
const manageRepoAddHandler = () => router.replace('/manage-repo/add');
const manageRepoEditHandler = (id: number) => router.replace('/manage-repo/edit/' + id);
const closeRepoManageBoxHandler = () => router.replace('/');
const displayBlur = () =>
displayRepoAdd || displayRepoEdit ? classes.containerBlur : classes.container;
const renderRepoList = getSortedRepoList().map((repo: Repository) => (
<Repo
key={repo.id}
id={repo.id}
alias={repo.alias}
status={repo.status}
lastSave={repo.lastSave}
alert={repo.alert}
repositoryName={repo.repositoryName}
storageUsed={repo.storageUsed}
storageSize={repo.storageSize}
sshPublicKey={repo.sshPublicKey}
comment={repo.comment}
lanCommand={repo.lanCommand}
appendOnlyMode={repo.appendOnlyMode}
repoManageEditHandler={() => manageRepoEditHandler(repo.id)}
wizardEnv={wizardEnv}
/>
));
return (
<>
<div className={displayBlur()}>
<div className={classes.containerAddRepo}>
<Link
href='/manage-repo/add'
className={classes.newRepoButton}
onClick={manageRepoAddHandler}
>
<IconPlus className={classes.plusIcon} size={24} stroke={2} />
<span>Add a repository</span>
</Link>
</div>
<div className={classes.toolbar}>
<div className={classes.searchContainer}>
<input
type='text'
placeholder='Alias, comment, repository name...'
value={searchQuery}
onChange={handleSearchChange}
className={classes.searchInput}
/>
{searchQuery && (
<button
onClick={() =>
handleSearchChange({
target: { value: '' },
} as React.ChangeEvent<HTMLInputElement>)
}
className={classes.clearButton}
title='Clear search'
>
<IconX size={16} stroke={2} />
</button>
)}
</div>
<div className={classes.sortIcons}>
<IconSortAscendingLetters
className={sortOption === 'alias-asc' ? classes.iconActive : classes.icon}
onClick={() => handleSortChange('alias-asc')}
title='Alias A-Z'
/>
<IconSortDescendingLetters
className={sortOption === 'alias-desc' ? classes.iconActive : classes.icon}
onClick={() => handleSortChange('alias-desc')}
title='Alias Z-A'
/>
<IconSortDescending2Filled
className={sortOption === 'status-true' ? classes.iconActive : classes.icon}
onClick={() => handleSortChange('status-true')}
title='Status OK → KO'
/>
<IconSortDescending2
className={sortOption === 'status-false' ? classes.iconActive : classes.icon}
onClick={() => handleSortChange('status-false')}
title='Status KO → OK'
/>
<IconCalendarDown
className={sortOption === 'last-save-desc' ? classes.iconActive : classes.icon}
onClick={() => handleSortChange('last-save-desc')}
title='Last save (recent → old)'
/>
<IconCalendarUp
className={sortOption === 'last-save-asc' ? classes.iconActive : classes.icon}
onClick={() => handleSortChange('last-save-asc')}
title='Last save (old → recent)'
/>
<IconSortAscendingSmallBig
className={sortOption === 'storage-used-asc' ? classes.iconActive : classes.icon}
onClick={() => handleSortChange('storage-used-asc')}
title='Storage usage % low → high'
/>
<IconSortDescendingSmallBig
className={sortOption === 'storage-used-desc' ? classes.iconActive : classes.icon}
onClick={() => handleSortChange('storage-used-desc')}
title='Storage usage % high → low'
/>
</div>
</div>
<div className={classes.containerRepoList}>
<div className={classes.RepoList}>{renderRepoList}</div>
</div>
</div>
{displayRepoAdd && (
<RepoManage mode='add' repoList={data.repoList} closeHandler={closeRepoManageBoxHandler} />
)}
{displayRepoEdit && (
<RepoManage mode='edit' repoList={data.repoList} closeHandler={closeRepoManageBoxHandler} />
)}
</>
);
}

View file

@ -11,7 +11,7 @@
.modale {
position: fixed;
top: 10%;
width: 1000px;
width: 800px;
height: auto;
max-width: 75%;
max-height: 85%;
@ -24,6 +24,11 @@
animation: append-animate 0.3s linear;
}
.modale h2 {
margin-top: 0;
color: #374151;
}
@keyframes append-animate {
from {
transform: scale(0);
@ -47,89 +52,98 @@
.repoManageForm {
margin: auto;
width: 80%;
padding: 15px 30px 30px 30px;
border-radius: 5px;
text-align: left;
width: 100%;
max-width: 600px;
border-radius: 8px;
background-color: #ffffff;
font-family: Inter, sans-serif;
color: #1f2937;
}
.formWrapper {
text-align: center;
margin: auto;
width: 100%;
height: auto;
color: #494b7a;
margin: 0 auto;
color: inherit;
}
.repoManageForm label {
display: block;
margin-bottom: 8px;
text-align: center;
margin-top: 20px;
color: #494b7a;
margin-top: 0.9rem;
margin-bottom: 0.5rem;
font-weight: 600;
font-size: 0.95rem;
color: #374151;
text-align: left;
}
.repoManageForm input,
.repoManageForm textarea,
.repoManageForm select {
border: 1px solid #6d4aff21;
font-size: 16px;
height: auto;
margin: 0;
margin-bottom: 0px;
outline: 0;
padding: 15px;
width: 100%;
background-color: #f5f5f5;
border-radius: 5px;
/* color: #1b1340; */
color: #494b7a;
box-shadow: 0 1px 0 rgba(0, 0, 0, 0.03) inset;
padding: 0.75rem 1rem;
border: 1px solid #d1d5db;
border-radius: 6px;
font-size: 0.9rem;
background-color: #f9fafb;
color: #111827;
font-family: Inter;
}
.repoManageForm textarea {
resize: vertical;
min-height: 80px;
}
.repoManageForm textarea:focus,
.repoManageForm input:focus,
.repoManageForm textarea:focus,
.repoManageForm select:focus {
outline: 1px solid #6d4aff;
box-shadow: 0 0 10px 3px rgba(110, 74, 255, 0.605);
border-color: #6d4aff;
background-color: #ffffff;
outline: none;
box-shadow: 0 0 0 2px rgba(109, 74, 255, 0.3);
}
.repoManageForm .invalid {
background: #f3c7c7;
border: 1px solid #e45454;
outline: 1px solid #ff4a4a;
background-color: #fef2f2;
border-color: #ef4444;
}
.repoManageForm .invalid:focus {
background: #f3c7c7;
border: 1px solid #e45454;
outline: 1px solid #ff4a4a;
box-shadow: 0 0 10px 3px rgba(255, 74, 74, 0.605);
box-shadow: 0 0 0 2px rgba(239, 68, 68, 0.3);
}
.repoManageForm button {
display: block;
margin: 15px auto;
margin: 2rem auto 0 auto;
background-color: #6d4aff;
color: #ffffff;
padding: 0.75rem 1.5rem;
border: none;
border-radius: 6px;
font-size: 1rem;
font-weight: 500;
cursor: pointer;
transition: background-color 0.2s ease;
}
.repoManageForm button:hover {
display: block;
margin: 15px auto;
background-color: #5c3dff;
}
.errorMessage {
color: red;
display: block;
margin-top: 3px;
color: #dc2626;
font-size: 0.875rem;
margin-top: 0.3rem;
}
.optionCommandWrapper {
display: flex;
margin-top: 20px;
color: #494b7a;
gap: 0.5rem;
align-items: center;
margin-top: 1.5rem;
font-size: 0.95rem;
color: #374151;
}
.optionCommandWrapper label {
@ -137,15 +151,33 @@
}
.optionCommandWrapper input[type='checkbox'] {
width: auto;
margin-right: 8px;
width: 18px;
height: 18px;
accent-color: #6d4aff;
cursor: pointer;
accent-color: #6d4aff;
}
.optionCommandWrapper input[type='checkbox']:focus {
outline: 0;
box-shadow: none;
accent-color: #6d4aff;
outline: none;
box-shadow: 0 0 0 2px rgba(109, 74, 255, 0.4);
}
.selectAlert {
max-width: 160px;
}
.selectAlertWrapper label {
margin: 0;
}
.selectAlertWrapper {
display: flex;
flex-direction: row;
justify-content: flex-start;
align-items: center;
margin-top: 1.5rem;
gap: 0.5rem;
font-size: 0.95rem;
color: #374151;
}
/* DELETE DIALOG */
@ -254,6 +286,7 @@
}
.littleDeleteButton {
margin-top: 10px;
border: none;
font-weight: 300;
color: red;
@ -261,8 +294,3 @@
background: none;
cursor: pointer;
}
.selectAlert {
margin: auto auto 35px auto;
max-width: 160px;
}

View file

@ -1,29 +1,46 @@
//Lib
import classes from './RepoManage.module.css';
import { IconAlertCircle, IconX } from '@tabler/icons-react';
import { useState } from 'react';
import { useRouter } from 'next/router';
import { toast } from 'react-toastify';
import 'react-toastify/dist/ReactToastify.css';
import { useForm, Controller } from 'react-hook-form';
import { SpinnerDotted } from 'spinners-react';
import Select from 'react-select';
import { IconAlertCircle, IconExternalLink, IconX } from '@tabler/icons-react';
import Link from 'next/link';
import { IconExternalLink } from '@tabler/icons-react';
import { alertOptions } from '../../domain/constants';
import { useRouter } from 'next/router';
import { useState } from 'react';
import { Controller, useForm } from 'react-hook-form';
import Select from 'react-select';
import { toast, ToastOptions } from 'react-toastify';
import 'react-toastify/dist/ReactToastify.css';
import { useLoader } from '~/contexts/LoaderContext';
import { alertOptions, Optional, Repository } from '~/types';
import classes from './RepoManage.module.css';
export default function RepoManage(props) {
////Var
let targetRepo;
type RepoManageProps = {
mode: 'add' | 'edit';
repoList: Optional<Array<Repository>>;
closeHandler: () => void;
};
type DataForm = {
alias: string;
storageSize: string;
sshkey: string;
comment: string;
alert: { value: Optional<number>; label: string };
lanCommand: boolean;
appendOnlyMode: boolean;
};
export default function RepoManage(props: RepoManageProps) {
const router = useRouter();
const targetRepo =
props.mode === 'edit' && router.query.slug
? props.repoList?.find((repo) => repo.id.toString() === router.query.slug)
: undefined;
const {
register,
handleSubmit,
control,
formState: { errors, isSubmitting, isValid },
} = useForm({ mode: 'onChange' });
} = useForm<DataForm>({ mode: 'onChange' });
const toastOptions = {
const toastOptions: ToastOptions = {
position: 'top-right',
autoClose: 5000,
hideProgressBar: false,
@ -33,108 +50,113 @@ export default function RepoManage(props) {
progress: undefined,
};
////State
const [deleteDialog, setDeleteDialog] = useState(false);
const [isLoading, setIsLoading] = useState(false);
const { start, stop } = useLoader();
////Functions
//router.query.slug is undefined for few milliseconds on first render for a direct URL access (https://github.com/vercel/next.js/discussions/11484).
//If I call repoManage with edit mode (props), i'm firstly waiting that router.query.slug being available before rendering.
if (!router.query.slug && props.mode == 'edit') {
return <SpinnerDotted size={30} thickness={100} speed={180} color='rgba(109, 74, 255, 1)' />;
} else if (props.mode == 'edit') {
for (let element in props.repoList) {
if (props.repoList[element].id == router.query.slug) {
targetRepo = props.repoList[element];
}
}
//If the ID does not exist > 404
if (!targetRepo) {
if (props.mode === 'edit') {
if (!router.query.slug) {
start();
return;
} else if (!targetRepo) {
stop();
router.push('/404');
return null;
}
}
//Delete a repo
const deleteHandler = async () => {
const deleteHandler = async (repositoryName?: string) => {
start();
if (!repositoryName) {
stop();
toast.error('Repository name not found', toastOptions);
router.replace('/');
return;
}
//API Call for delete
fetch('/api/repo/id/' + router.query.slug + '/delete', {
await fetch('/api/v1/repositories/' + repositoryName, {
method: 'DELETE',
headers: {
'Content-type': 'application/json',
},
})
.then((response) => {
.then(async (response) => {
if (response.ok) {
toast.success(
'🗑 The repository #' + router.query.slug + ' has been successfully deleted',
'🗑 The repository ' + repositoryName + ' has been successfully deleted',
toastOptions
);
router.replace('/');
} else {
if (response.status == 403)
if (response.status == 403) {
toast.warning(
'🔒 The server is currently protected against repository deletion.',
toastOptions
);
else toast.error('An error has occurred', toastOptions);
router.replace('/');
console.log('Fail to delete');
setIsLoading(false);
router.replace('/');
} else {
const errorMessage = await response.json();
toast.error(`An error has occurred : ${errorMessage.message.stderr}`, toastOptions);
router.replace('/');
console.log('Fail to delete');
}
}
})
.catch((error) => {
toast.error('An error has occurred', toastOptions);
router.replace('/');
console.log(error);
})
.finally(() => {
stop();
});
};
//Verify that the SSH key is unique
const isSSHKeyUnique = async (sshPublicKey) => {
let isUnique = true;
const isSSHKeyUnique = async (sshPublicKey: string): Promise<boolean> => {
try {
// Extract the first two columns of the SSH key in the form
const publicKeyPrefix = sshPublicKey.split(' ').slice(0, 2).join(' ');
// Extract the first two columns of the SSH key in the form
const publicKeyPrefix = sshPublicKey.split(' ').slice(0, 2).join(' ');
const response = await fetch('/api/v1/repositories', { method: 'GET' });
const data: { repoList: Repository[] } = await response.json();
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;
const conflictingRepo = data.repoList.find((repo: { sshPublicKey: string; id: number }) => {
const repoPublicKeyPrefix = repo.sshPublicKey.split(' ').slice(0, 2).join(' ');
return (
repoPublicKeyPrefix === publicKeyPrefix && (!targetRepo || repo.id !== targetRepo.id)
);
});
return isUnique;
if (conflictingRepo) {
toast.error(
`The SSH key is already used in repository ${conflictingRepo.repositoryName}. Please use another key or delete the key from the other repository.`,
toastOptions
);
return false;
}
return true;
} catch (error) {
console.log(error);
toast.error('An error has occurred', toastOptions);
return false;
}
};
//Form submit Handler for ADD or EDIT a repo
const formSubmitHandler = async (dataForm) => {
//Loading button on submit to avoid multiple send.
const formSubmitHandler = async (dataForm: DataForm) => {
setIsLoading(true);
start();
// Clean SSH key by removing leading/trailing whitespace and line breaks
const cleanedSSHKey = dataForm.sshkey.trim();
//Verify that the SSH key is unique
if (!(await isSSHKeyUnique(dataForm.sshkey))) {
if (!(await isSSHKeyUnique(cleanedSSHKey))) {
stop();
setIsLoading(false);
return;
}
@ -143,14 +165,14 @@ export default function RepoManage(props) {
const newRepo = {
alias: dataForm.alias,
storageSize: parseInt(dataForm.storageSize),
sshPublicKey: dataForm.sshkey,
sshPublicKey: cleanedSSHKey,
comment: dataForm.comment,
alert: dataForm.alert.value,
lanCommand: dataForm.lanCommand,
appendOnlyMode: dataForm.appendOnlyMode,
};
//POST API to send new repo
await fetch('/api/repo/add', {
await fetch('/api/v1/repositories', {
method: 'POST',
headers: {
'Content-type': 'application/json',
@ -163,7 +185,7 @@ export default function RepoManage(props) {
router.replace('/');
} else {
const errorMessage = await response.json();
toast.error(`An error has occurred : ${errorMessage.message}`, toastOptions);
toast.error(`An error has occurred : ${errorMessage.message.stderr}`, toastOptions);
router.replace('/');
console.log(`Fail to ${props.mode}`);
}
@ -172,19 +194,23 @@ export default function RepoManage(props) {
toast.error('An error has occurred', toastOptions);
router.replace('/');
console.log(error);
})
.finally(() => {
stop();
setIsLoading(false);
});
//EDIT a repo
} else if (props.mode == 'edit') {
const dataEdited = {
alias: dataForm.alias,
storageSize: parseInt(dataForm.storageSize),
sshPublicKey: dataForm.sshkey,
sshPublicKey: cleanedSSHKey,
comment: dataForm.comment,
alert: dataForm.alert.value,
lanCommand: dataForm.lanCommand,
appendOnlyMode: dataForm.appendOnlyMode,
};
await fetch('/api/repo/id/' + router.query.slug + '/edit', {
await fetch('/api/v1/repositories/' + targetRepo?.repositoryName, {
method: 'PATCH',
headers: {
'Content-type': 'application/json',
@ -194,13 +220,13 @@ export default function RepoManage(props) {
.then(async (response) => {
if (response.ok) {
toast.success(
'The repository #' + targetRepo.id + ' has been successfully edited !',
'The repository ' + targetRepo?.repositoryName + ' has been successfully edited !',
toastOptions
);
router.replace('/');
} else {
const errorMessage = await response.json();
toast.error(`An error has occurred : ${errorMessage.message}`, toastOptions);
toast.error(`An error has occurred : ${errorMessage.message.stderr}`, toastOptions);
router.replace('/');
console.log(`Fail to ${props.mode}`);
}
@ -209,6 +235,10 @@ export default function RepoManage(props) {
toast.error('An error has occurred', toastOptions);
router.replace('/');
console.log(error);
})
.finally(() => {
stop();
setIsLoading(false);
});
}
};
@ -231,54 +261,54 @@ export default function RepoManage(props) {
color: 'rgba(99, 115, 129, 0.38)',
}}
>
#{targetRepo.id}
{targetRepo?.repositoryName}
</span>{' '}
?
</h1>
</div>
<div className={classes.deleteDialogMessage}>
<div style={{ marginBottom: '5px' }}>
You are about to permanently delete the repository <b>#{targetRepo.id}</b> and all
the backups it contains.
You are about to permanently delete the repository{' '}
<b>{targetRepo?.repositoryName}</b> and all the backups it contains.
</div>
<div>The data will not be recoverable and it will not be possible to go back.</div>
</div>
<div className={classes.deleteDialogButtonWrapper}>
{isLoading ? (
<SpinnerDotted size={30} thickness={150} speed={100} color='#6d4aff' />
) : (
<>
<button onClick={props.closeHandler} className={classes.cancelButton}>
Cancel
</button>
<button
onClick={() => {
deleteHandler();
setIsLoading(true);
}}
className={classes.deleteButton}
>
Yes, delete it !
</button>
</>
)}
<>
<button
onClick={props.closeHandler}
disabled={isLoading}
className={classes.cancelButton}
>
Cancel
</button>
<button
onClick={() => {
deleteHandler(targetRepo?.repositoryName);
setIsLoading(true);
}}
className={classes.deleteButton}
>
Yes, delete it !
</button>
</>
</div>
</div>
) : (
<div className={classes.formWrapper}>
{props.mode == 'edit' && (
<h1>
<h2>
Edit the repository{' '}
<span
style={{
color: 'rgba(99, 115, 129, 0.38)',
color: '#6d4aff',
}}
>
#{targetRepo.id}
{targetRepo?.repositoryName}
</span>
</h1>
</h2>
)}
{props.mode == 'add' && <h1>Add a repository</h1>}
{props.mode == 'add' && <h2>Add a repository</h2>}
<form className={classes.repoManageForm} onSubmit={handleSubmit(formSubmitHandler)}>
{/* ALIAS */}
<label htmlFor='alias'>Alias</label>
@ -286,16 +316,16 @@ export default function RepoManage(props) {
className='form-control is-invalid'
placeholder='Alias for the repository, e.g."Server 1"'
type='text'
defaultValue={props.mode == 'edit' ? targetRepo.alias : null}
defaultValue={props.mode == 'edit' ? targetRepo?.alias : undefined}
{...register('alias', {
required: 'An alias is required.',
minLength: {
value: 2,
message: '2 characters min',
value: 1,
message: '1 character min',
},
maxLength: {
value: 40,
message: '40 characters max',
value: 100,
message: '100 characters max',
},
})}
/>
@ -304,15 +334,17 @@ export default function RepoManage(props) {
<label htmlFor='sshkey'>SSH public key</label>
<textarea
placeholder='Public key in OpenSSH format (rsa, ed25519, ed25519-sk)'
type='text'
defaultValue={props.mode == 'edit' ? targetRepo.sshPublicKey : null}
defaultValue={props.mode == 'edit' ? targetRepo?.sshPublicKey : undefined}
{...register('sshkey', {
required: 'SSH public key is required.',
pattern: {
value:
/^(ssh-ed25519 AAAAC3NzaC1lZDI1NTE5|sk-ssh-ed25519@openssh.com AAAAGnNrLXNzaC1lZDI1NTE5QG9wZW5zc2guY29t|ssh-rsa AAAAB3NzaC1yc2)[0-9A-Za-z+/]+[=]{0,3}(\s.*)?$/,
message:
'Invalid public key. The SSH key needs to be in OpenSSH format (rsa, ed25519, ed25519-sk)',
validate: (value) => {
const trimmedValue = value.trim();
const pattern =
/^(ssh-ed25519 AAAAC3NzaC1lZDI1NTE5|sk-ssh-ed25519@openssh.com AAAAGnNrLXNzaC1lZDI1NTE5QG9wZW5zc2guY29t|ssh-rsa AAAAB3NzaC1yc2)[0-9A-Za-z+/]+[=]{0,3}(\s.*)?$/;
return (
pattern.test(trimmedValue) ||
'Invalid public key. The key needs to be in OpenSSH format (rsa, ed25519, ed25519-sk)'
);
},
})}
/>
@ -323,8 +355,9 @@ export default function RepoManage(props) {
<label htmlFor='storageSize'>Storage Size (GB)</label>
<input
type='number'
placeholder='1000'
min='1'
defaultValue={props.mode == 'edit' ? targetRepo.storageSize : null}
defaultValue={props.mode == 'edit' ? targetRepo?.storageSize : undefined}
{...register('storageSize', {
required: 'A storage size is required.',
})}
@ -335,14 +368,12 @@ export default function RepoManage(props) {
{/* COMMENT */}
<label htmlFor='comment'>Comment</label>
<textarea
type='text'
placeholder='Little comment for your repository...'
defaultValue={props.mode == 'edit' ? targetRepo.comment : null}
defaultValue={props.mode == 'edit' ? targetRepo?.comment : undefined}
{...register('comment', {
required: false,
maxLength: {
value: 200,
message: '200 characters maximum.',
value: 500,
message: '500 characters maximum.',
},
})}
/>
@ -353,16 +384,11 @@ export default function RepoManage(props) {
<div className={classes.optionCommandWrapper}>
<input
type='checkbox'
name='lanCommand'
defaultChecked={props.mode == 'edit' ? targetRepo.lanCommand : false}
defaultChecked={props.mode == 'edit' ? targetRepo?.lanCommand : false}
{...register('lanCommand')}
/>
<label htmlFor='lanCommand'>Generates commands for use over LAN.</label>
<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'
@ -374,16 +400,11 @@ export default function RepoManage(props) {
<div className={classes.optionCommandWrapper}>
<input
type='checkbox'
name='appendOnlyMode'
defaultChecked={props.mode == 'edit' ? targetRepo.appendOnlyMode : false}
defaultChecked={props.mode == 'edit' ? targetRepo?.appendOnlyMode : false}
{...register('appendOnlyMode')}
/>
<label htmlFor='appendOnlyMode'>Enable append-only mode.</label>
<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'
@ -392,57 +413,75 @@ export default function RepoManage(props) {
</Link>
</div>
{/* ALERT */}
<label style={{ margin: '25px auto 10px auto' }} htmlFor='alert'>
Alert if there is no backup since :
</label>
<div className={classes.selectAlert}>
<Controller
name='alert'
defaultValue={
props.mode == 'edit'
? alertOptions.find((x) => x.value === targetRepo.alert) || {
value: targetRepo.alert,
label: `${targetRepo.alert} seconds (custom)`,
}
: alertOptions[4]
}
control={control}
render={({ field: { onChange, value } }) => (
<Select
onChange={onChange}
value={value}
options={alertOptions}
isSearchable={false}
maxMenuHeight={150}
menuPlacement='top'
theme={(theme) => ({
...theme,
borderRadius: '5px',
colors: {
...theme.colors,
primary25: '#c3b6fa',
primary: '#6d4aff',
},
})}
/>
)}
/>
</div>
{isLoading ? (
<div
style={{
textAlign: 'center',
marginTop: '8px',
}}
>
<SpinnerDotted size={30} thickness={150} speed={100} color='#6d4aff' />
<div className={classes.selectAlertWrapper}>
<label htmlFor='alert'>Alert if there is no backup since :</label>
<div className={classes.selectAlert}>
<Controller
name='alert'
defaultValue={
props.mode == 'edit'
? alertOptions.find((x) => x.value === targetRepo?.alert) || {
value: targetRepo?.alert,
label: `Custom value (${targetRepo?.alert} seconds)`,
}
: alertOptions[4]
}
control={control}
render={({ field: { onChange, value } }) => (
<Select
onChange={onChange}
value={value}
options={alertOptions}
isSearchable={false}
maxMenuHeight={300}
menuPlacement='top'
styles={{
control: (base) => ({
...base,
minHeight: '35px',
height: '35px',
}),
valueContainer: (base) => ({
...base,
height: '35px',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
flexDirection: 'column',
padding: '0 8px',
}),
input: (base) => ({
...base,
margin: 0,
}),
indicatorsContainer: (base) => ({
...base,
height: '35px',
}),
}}
theme={(theme) => ({
...theme,
borderRadius: 5,
colors: {
...theme.colors,
primary25: '#c3b6fa',
primary: '#6d4aff',
},
})}
/>
)}
/>
</div>
) : (
<button type='submit' className='defaultButton' disabled={!isValid || isSubmitting}>
{props.mode == 'edit' && 'Edit'}
{props.mode == 'add' && 'Add'}
</button>
)}
</div>
<button
type='submit'
className='defaultButton'
disabled={!isValid || isSubmitting || isLoading}
>
{props.mode == 'edit' && 'Save'}
{props.mode == 'add' && 'Add repository'}
</button>
</form>
{props.mode == 'edit' ? (
<button className={classes.littleDeleteButton} onClick={() => setDeleteDialog(true)}>

View file

@ -1,144 +0,0 @@
//Lib
import React from 'react';
import classes from './SetupWizard.module.css';
import { useState, useEffect } from 'react';
import { useRouter } from 'next/router';
import Select from 'react-select';
//Components
import WizardStepBar from '../../Components/WizardSteps/WizardStepBar/WizardStepBar';
import WizardStep1 from '../../Components/WizardSteps/WizardStep1/WizardStep1';
import WizardStep2 from '../../Components/WizardSteps/WizardStep2/WizardStep2';
import WizardStep3 from '../../Components/WizardSteps/WizardStep3/WizardStep3';
import WizardStep4 from '../../Components/WizardSteps/WizardStep4/WizardStep4';
function SetupWizard(props) {
////Var
const router = useRouter();
////States
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',
});
////LifeCycle
//ComponentDidMount
useEffect(() => {
//retrieve the repository list
const repoList = async () => {
try {
const response = await fetch('/api/repo', {
method: 'GET',
headers: {
'Content-type': 'application/json',
},
});
setList((await response.json()).repoList);
setListIsLoading(false);
} catch (error) {
console.log('Fetching datas error');
}
};
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(() => {
//Go to the step in the URL param when URL change
setStep(props.step);
}, [props.step]);
////Functions
//Options for react-select
const options = list.map((repo) => ({
label: `${repo.alias} - #${repo.id}`,
value: `${repo.alias} - #${repo.id}`,
id: repo.id,
repositoryName: repo.repositoryName,
lanCommand: repo.lanCommand,
}));
//Step button (free selection of user)
const changeStepHandler = (x) => router.push('/setup-wizard/' + x);
//Next Step button
const nextStepHandler = () => {
if (step < 4) {
router.push('/setup-wizard/' + `${Number(step) + 1}`);
}
};
//Previous Step button
const previousStepHandler = () => {
if (step > 1) {
router.push('/setup-wizard/' + `${Number(step) - 1}`);
}
};
//Change Step with State
const wizardStep = (step) => {
if (step == 1) {
return <WizardStep1 />;
} else if (step == 2) {
return <WizardStep2 selectedOption={selectedOption} wizardEnv={wizardEnv} />;
} else if (step == 3) {
return <WizardStep3 selectedOption={selectedOption} wizardEnv={wizardEnv} />;
} else {
return <WizardStep4 selectedOption={selectedOption} wizardEnv={wizardEnv} />;
}
};
return (
<div className={classes.container}>
<WizardStepBar
setStep={(x) => changeStepHandler(x)}
step={step}
nextStepHandler={() => nextStepHandler()}
previousStepHandler={() => previousStepHandler()}
/>
<div className={classes.selectRepo}>
<Select
onChange={setSelectedOption}
isLoading={listIsLoading}
isDisabled={listIsLoading}
options={options}
isSearchable
placeholder='Select your repository...'
theme={(theme) => ({
...theme,
borderRadius: '5px',
colors: {
...theme.colors,
primary25: '#c3b6fa',
primary: '#6d4aff',
},
})}
/>
</div>
{wizardStep(step)}
</div>
);
}
export default SetupWizard;

View file

@ -0,0 +1,170 @@
import { useRouter } from 'next/router';
import { useEffect, useMemo, useState } from 'react';
import Select, { SingleValue } from 'react-select';
import classes from './SetupWizard.module.css';
import { Optional, SelectedRepoWizard, Repository, WizardEnvType } from '~/types';
//Components
import WizardStep1 from '../../Components/WizardSteps/WizardStep1/WizardStep1';
import WizardStep2 from '../../Components/WizardSteps/WizardStep2/WizardStep2';
import WizardStep3 from '../../Components/WizardSteps/WizardStep3/WizardStep3';
import WizardStep4 from '../../Components/WizardSteps/WizardStep4/WizardStep4';
import WizardStepBar from '../../Components/WizardSteps/WizardStepBar/WizardStepBar';
type SetupWizardProps = {
step?: number;
};
function SetupWizard(props: SetupWizardProps) {
const router = useRouter();
const [repoList, setRepoList] = useState<Optional<Array<Repository>>>();
const [repoListIsLoading, setRepoListIsLoading] = useState<boolean>(true);
const [step, setStep] = useState<number>(1);
const [wizardEnv, setWizardEnv] = useState<Optional<WizardEnvType>>();
const [selectedItem, setSelectedItem] = useState<Optional<SelectedRepoWizard>>();
////LifeCycle
//ComponentDidMount
useEffect(() => {
//retrieve the repository list
const fetchRepoList = async () => {
try {
const response = await fetch('/api/v1/repositories', {
method: 'GET',
headers: {
'Content-type': 'application/json',
},
});
const data = await response.json();
const repos = data.repoList;
setRepoList(repos);
setRepoListIsLoading(false);
// Auto-select first repository if available
if (repos && repos.length > 0) {
setSelectedItem({
label: `${repos[0].alias} - ${repos[0].repositoryName}`,
value: `${repos[0].alias} - ${repos[0].repositoryName}`,
id: repos[0].id.toString(),
repositoryName: repos[0].repositoryName,
lanCommand: repos[0].lanCommand ? repos[0].lanCommand : false,
});
}
} catch (error) {
console.log('Fetching datas error');
}
};
fetchRepoList();
//Fetch wizardEnv to hydrate Wizard' steps
const fetchWizardEnv = async () => {
try {
const response = await fetch('/api/v1/account/wizard-env', {
method: 'GET',
headers: {
'Content-type': 'application/json',
},
});
const data: WizardEnvType = await response.json();
setWizardEnv(data);
} catch (error) {
console.log('Fetching datas error');
}
};
fetchWizardEnv();
}, []);
//Component did update
useEffect(() => {
//Go to the step in the URL param when URL change
if (props.step) {
// eslint-disable-next-line react-hooks/set-state-in-effect
setStep(props.step);
}
}, [props.step]);
//Options for react-select
const options: Optional<Array<SelectedRepoWizard>> = useMemo(
() =>
repoList?.map((repo) => ({
label: `${repo.alias} - ${repo.repositoryName}`,
value: `${repo.alias} - ${repo.repositoryName}`,
id: repo.id.toString(),
repositoryName: repo.repositoryName,
lanCommand: repo.lanCommand ? repo.lanCommand : false,
})),
[repoList]
);
//Step button (free selection of user)
const changeStepHandler = (x: number) => router.push('/setup-wizard/' + x.toString());
//Next Step button
const nextStepHandler = () => {
if (step && step < 4) {
router.push('/setup-wizard/' + `${step + 1}`);
}
};
//Previous Step button
const previousStepHandler = () => {
if (step && step > 1) {
router.push('/setup-wizard/' + `${step - 1}`);
}
};
const onChangeSelect = (option: SingleValue<SelectedRepoWizard>) => {
if (option) {
setSelectedItem(option);
} else {
setSelectedItem(undefined);
}
};
//Change Step with State
const wizardStep = (step?: number) => {
if (!step || step === 1) {
return <WizardStep1 />;
} else if (step === 2) {
return <WizardStep2 selectedRepo={selectedItem} wizardEnv={wizardEnv} />;
} else if (step === 3) {
return <WizardStep3 selectedRepo={selectedItem} wizardEnv={wizardEnv} />;
} else {
return <WizardStep4 selectedRepo={selectedItem} wizardEnv={wizardEnv} />;
}
};
return (
<div className={classes.container}>
<WizardStepBar
setStep={(x) => changeStepHandler(x)}
step={step}
nextStepHandler={() => nextStepHandler()}
previousStepHandler={() => previousStepHandler()}
/>
<div className={classes.selectRepo}>
<Select
onChange={(item) => onChangeSelect(item)}
isLoading={repoListIsLoading}
isDisabled={repoListIsLoading}
options={options}
isSearchable
value={selectedItem}
placeholder='Select your repository...'
theme={(theme) => ({
...theme,
borderRadius: 5,
colors: {
...theme.colors,
primary25: '#c3b6fa',
primary: '#6d4aff',
},
})}
/>
</div>
{wizardStep(step)}
</div>
);
}
export default SetupWizard;

View file

@ -1,208 +0,0 @@
//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>
</>
);
}

View file

@ -0,0 +1,171 @@
import { IconExternalLink } from '@tabler/icons-react';
import Link from 'next/link';
import { useEffect, useState } from 'react';
import { toast, ToastOptions } from 'react-toastify';
import 'react-toastify/dist/ReactToastify.css';
import classes from '../UserSettings.module.css';
//Components
import Switch from '~/Components/UI/Switch/Switch';
import { useLoader } from '~/contexts/LoaderContext';
import { Optional } from '~/types';
import AppriseMode from './AppriseMode/AppriseMode';
import AppriseURLs from './AppriseURLs/AppriseURLs';
type AppriseAlertDataForm = {
appriseAlert: boolean;
};
export default function AppriseAlertSettings() {
const toastOptions: ToastOptions = {
position: 'top-right',
autoClose: 5000,
hideProgressBar: false,
closeOnClick: true,
pauseOnHover: true,
draggable: true,
progress: undefined,
};
const { start, stop } = useLoader();
////State
const [isSendingTestNotification, setIsSendingTestNotification] = useState(false);
const [isSwitchDisabled, setIsSwitchDisabled] = useState(true);
const [isAlertEnabled, setIsAlertEnabled] = useState<Optional<boolean>>(undefined);
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/v1/notif/apprise/alert', {
method: 'GET',
headers: {
'Content-type': 'application/json',
},
});
const data: Optional<AppriseAlertDataForm> = await response.json();
setIsAlertEnabled(data?.appriseAlert ?? false);
setIsSwitchDisabled(false);
} catch (error) {
setIsSwitchDisabled(true);
setIsAlertEnabled(false);
toast.error('Fetching Apprise alert setting failed', toastOptions);
}
};
getAppriseAlert();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
////Functions
//Switch to enable/disable Apprise notifications
const onChangeSwitchHandler = async (data: AppriseAlertDataForm) => {
start();
setIsSwitchDisabled(true);
await fetch('/api/v1/notif/apprise/alert', {
method: 'PUT',
headers: {
'Content-type': 'application/json',
},
body: JSON.stringify(data),
})
.then((response) => {
if (response.ok && typeof data.appriseAlert === 'boolean') {
setIsAlertEnabled(data.appriseAlert);
toast.success(
data.appriseAlert ? 'Apprise notifications enabled' : 'Apprise notifications disabled',
toastOptions
);
} else {
toast.error('Update Apprise failed', toastOptions);
}
})
.catch(() => {
toast.error('Update Apprise failed', toastOptions);
})
.finally(() => {
stop();
setIsSwitchDisabled(false);
});
};
//Send Apprise test notification to services
const onSendTestAppriseHandler = async () => {
start();
setIsSendingTestNotification(true);
try {
const response = await fetch('/api/v1/notif/apprise/test', {
method: 'POST',
});
const result = await response.json();
if (!response.ok) {
toast.error(result.message, toastOptions);
} else {
setInfo(true);
setTimeout(() => {
setInfo(false);
}, 4000);
}
} catch (error) {
toast.error('Sending test notification failed', toastOptions);
} finally {
stop();
setIsSendingTestNotification(false);
}
};
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}>
<Switch
loading={isAlertEnabled === undefined}
checked={isAlertEnabled}
disabled={isSwitchDisabled}
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 })}
/>
{isAlertEnabled && (
<>
<AppriseURLs />
<AppriseMode />
<button
disabled={isSendingTestNotification}
style={{ marginTop: '20px' }}
className='defaultButton'
onClick={() => onSendTestAppriseHandler()}
>
Send a test notification
</button>
{info && (
<span style={{ marginLeft: '10px', color: '#119300' }}>
Notification successfully sent.
</span>
)}
</>
)}
</div>
</div>
</div>
</>
);
}

View file

@ -1,44 +1,51 @@
//Lib
import { useEffect } from 'react';
import classes from '../../UserSettings.module.css';
import { useState } from 'react';
import { SpinnerCircularFixed } from 'spinners-react';
import { useEffect, useState } from 'react';
import { useForm } from 'react-hook-form';
import { AppriseModeDTO, AppriseModeEnum, Optional } from '~/types';
import classes from '../../UserSettings.module.css';
//Components
import Error from '../../../../Components/UI/Error/Error';
import Error from '~/Components/UI/Error/Error';
import { useLoader } from '~/contexts/LoaderContext';
import { useFormStatus } from '~/hooks';
type AppriseModeDataForm = {
appriseMode: string;
appriseStatelessURL: string;
};
export default function AppriseMode() {
//Var
const {
register,
handleSubmit,
formState: { errors },
} = useForm({ mode: 'onBlur' });
} = useForm<AppriseModeDataForm>({ mode: 'onChange' });
////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();
const { error, setIsLoading, handleSuccess, handleError, clearError } = useFormStatus();
const { start, stop } = useLoader();
const [displayStatelessURL, setDisplayStatelessURL] = useState<boolean>(false);
const [appriseMode, setAppriseMode] = useState<Optional<AppriseModeEnum>>(
AppriseModeEnum.STATELESS
);
const [appriseStatelessURL, setAppriseStatelessURL] = useState<Optional<string>>();
////LifeCycle
//Component did mount
useEffect(() => {
//Initial fetch to get Apprise Mode enabled
const getAppriseMode = async () => {
try {
const response = await fetch('/api/account/getAppriseMode', {
const response = await fetch('/api/v1/notif/apprise/mode', {
method: 'GET',
headers: {
'Content-type': 'application/json',
},
});
const { appriseStatelessURL, appriseMode } = await response.json();
const data: AppriseModeDTO = await response.json();
const { appriseStatelessURL, appriseMode } = data;
setAppriseMode(appriseMode);
if (appriseMode == 'stateless') {
if (appriseMode == AppriseModeEnum.STATELESS) {
setAppriseStatelessURL(appriseStatelessURL);
setDisplayStatelessURL(true);
}
@ -50,15 +57,13 @@ export default function AppriseMode() {
}, []);
////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
const modeFormSubmitHandler = async (data: AppriseModeDataForm) => {
clearError();
setIsLoading(true);
start();
try {
const response = await fetch('/api/account/updateAppriseMode', {
const response = await fetch('/api/v1/notif/apprise/mode', {
method: 'PUT',
headers: {
'Content-type': 'application/json',
@ -68,20 +73,15 @@ export default function AppriseMode() {
const result = await response.json();
if (!response.ok) {
setFormIsLoading(false);
setError(result.message);
setTimeout(() => setError(), 4000);
handleError(result.message);
} else {
setFormIsLoading(false);
setModeFormIsSaved(true);
setTimeout(() => setModeFormIsSaved(false), 3000);
handleSuccess();
}
} catch (error) {
setFormIsLoading(false);
setError('Change mode failed. Contact your administrator.');
setTimeout(() => {
setError();
}, 4000);
handleError('The Apprise mode change has failed');
} finally {
stop();
setIsLoading(false);
}
};
@ -90,23 +90,9 @@ export default function AppriseMode() {
{/* 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)}>
<form className={classes.bwForm} onChange={handleSubmit(modeFormSubmitHandler)}>
<div className='radio-group'>
<label style={{ marginRight: '50px' }}>
<div style={{ display: 'flex' }}>
@ -116,7 +102,7 @@ export default function AppriseMode() {
value='package'
onClick={() => {
setDisplayStatelessURL(false);
setAppriseMode('package');
setAppriseMode(AppriseModeEnum.PACKAGE);
}}
checked={appriseMode == 'package' ? true : false}
/>
@ -131,7 +117,7 @@ export default function AppriseMode() {
type='radio'
onClick={() => {
setDisplayStatelessURL(true);
setAppriseMode('stateless');
setAppriseMode(AppriseModeEnum.STATELESS);
}}
checked={appriseMode == 'stateless' ? true : false}
/>

View file

@ -1,65 +1,67 @@
//Lib
import { useEffect } from 'react';
import classes from '../../UserSettings.module.css';
import { useState } from 'react';
import { SpinnerCircularFixed } from 'spinners-react';
import { useEffect, useState } from 'react';
import { useForm } from 'react-hook-form';
import { AppriseServicesDTO, Optional } from '~/types';
import classes from '../../UserSettings.module.css';
//Components
import Error from '../../../../Components/UI/Error/Error';
import Error from '~/Components/UI/Error/Error';
import { useLoader } from '~/contexts/LoaderContext';
import { useFormStatus } from '~/hooks';
type AppriseURLsDataForm = {
appriseURLs: string;
};
export default function AppriseURLs() {
//Var
const {
register,
handleSubmit,
formState: { errors },
} = useForm({ mode: 'onBlur' });
} = useForm<AppriseURLsDataForm>({ mode: 'onBlur' });
////State
const [formIsLoading, setFormIsLoading] = useState(false);
const [urlsFormIsSaved, setUrlsFormIsSaved] = useState(false);
const [appriseServicesList, setAppriseServicesList] = useState();
const [error, setError] = useState();
const { isSaved, error, handleSuccess, handleError, clearError } = useFormStatus();
const { start, stop } = useLoader();
const [appriseServicesList, setAppriseServicesList] = useState<Optional<string>>();
const [fetchError, setFetchError] = useState<Optional<boolean>>();
////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', {
const response = await fetch('/api/v1/notif/apprise/services', {
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());
const data: AppriseServicesDTO = await response.json();
const servicesText = data.appriseServices?.join('\n');
setAppriseServicesList(servicesText);
setFetchError(false);
} catch (error) {
console.log('Fetching Apprise services list failed.');
setFetchError(true);
handleError('Fetching Apprise services list failed.');
}
};
getAppriseServices();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
////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
const urlsFormSubmitHandler = async (data: AppriseURLsDataForm) => {
clearError();
start();
if (fetchError) {
handleError('Cannot update Apprise services. Failed to fetch the initial list.');
stop();
return;
}
try {
const response = await fetch('/api/account/updateAppriseServices', {
const response = await fetch('/api/v1/notif/apprise/services', {
method: 'PUT',
headers: {
'Content-type': 'application/json',
@ -69,20 +71,14 @@ export default function AppriseURLs() {
const result = await response.json();
if (!response.ok) {
setFormIsLoading(false);
setError(result.message);
setTimeout(() => setError(), 4000);
handleError(result.message);
} else {
setFormIsLoading(false);
setUrlsFormIsSaved(true);
setTimeout(() => setUrlsFormIsSaved(false), 3000);
handleSuccess();
}
} catch (error) {
setFormIsLoading(false);
setError('Failed to update your services. Contact your administrator.');
setTimeout(() => {
setError();
}, 4000);
handleError('Failed to update your Apprise services.');
} finally {
stop();
}
};
@ -91,18 +87,8 @@ export default function AppriseURLs() {
{/* 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 && (
{isSaved && (
<div className={classes.formIsSavedMessage}>
Apprise configuration has been saved.
</div>
@ -115,7 +101,6 @@ export default function AppriseURLs() {
>
<textarea
style={{ height: '100px' }}
type='text'
placeholder={
'matrixs://{user}:{password}@{matrixhost}\ndiscord://{WebhookID}/{WebhookToken}\nmmosts://user@hostname/authkey'
}
@ -151,6 +136,7 @@ export default function AppriseURLs() {
</a>{' '}
to send a notification to any service. Only one URL per line.
</div>
{error && <Error message={error} />}
</>
);
}

View file

@ -1,191 +0,0 @@
//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>
</>
);
}

View file

@ -0,0 +1,169 @@
import { IconExternalLink } from '@tabler/icons-react';
import Link from 'next/link';
import { useEffect, useState } from 'react';
import { toast, ToastOptions } from 'react-toastify';
import 'react-toastify/dist/ReactToastify.css';
import { useLoader } from '~/contexts/LoaderContext';
import { EmailAlertDTO, Optional } from '~/types';
import classes from '../UserSettings.module.css';
//Components
import Error from '~/Components/UI/Error/Error';
import Switch from '~/Components/UI/Switch/Switch';
import { useFormStatus } from '~/hooks';
export default function EmailAlertSettings() {
const toastOptions: ToastOptions = {
position: 'top-right',
autoClose: 5000,
hideProgressBar: false,
closeOnClick: true,
pauseOnHover: true,
draggable: true,
progress: undefined,
//Callback > re-enabled button after notification.
onClose: () => setIsSwitchDisabled(false),
};
const { error, handleError, clearError } = useFormStatus();
const { start, stop } = useLoader();
////State
const [isSendingTestNotification, setIsSendingTestNotification] = useState(false);
const [isSwitchDisabled, setIsSwitchDisabled] = useState(true);
const [isAlertEnabled, setIsAlertEnabled] = useState<Optional<boolean>>(undefined);
const [info, setInfo] = useState(false);
////LifeCycle
//Component did mount
useEffect(() => {
const dataFetch = async () => {
try {
const response = await fetch('/api/v1/notif/email/alert', {
method: 'GET',
headers: {
'Content-type': 'application/json',
},
});
const data: Optional<EmailAlertDTO> = await response.json();
setIsAlertEnabled(data?.emailAlert ?? false);
setIsSwitchDisabled(false);
} catch (error) {
setIsSwitchDisabled(true);
setIsAlertEnabled(false);
handleError('Fetching email alert setting failed');
}
};
dataFetch();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
////Functions
//Switch to enable/disable Email notifications
const onChangeSwitchHandler = async (data: EmailAlertDTO) => {
clearError();
start();
setIsSwitchDisabled(true);
await fetch('/api/v1/notif/email/alert', {
method: 'PUT',
headers: {
'Content-type': 'application/json',
},
body: JSON.stringify(data),
})
.then((response) => {
if (response.ok && typeof data.emailAlert === 'boolean') {
setIsAlertEnabled(data.emailAlert);
toast.success(
data.emailAlert ? 'Email notification enabled !' : 'Email notification disabled !',
toastOptions
);
} else {
handleError('Update email alert setting failed.');
}
})
.catch(() => {
handleError('Update email alert setting failed.');
})
.finally(() => {
stop();
setIsSwitchDisabled(false);
});
};
//Send a test notification by email
const onSendTestMailHandler = async () => {
clearError();
start();
setIsSendingTestNotification(true);
try {
const response = await fetch('/api/v1/notif/email/test', {
method: 'POST',
headers: {
'Content-type': 'application/json',
},
});
const result = await response.json();
if (!response.ok) {
setIsSendingTestNotification(false);
handleError(result.message);
} else {
setIsSendingTestNotification(false);
setInfo(true);
setTimeout(() => {
setInfo(false);
}, 4000);
}
} catch (error) {
setIsSendingTestNotification(false);
handleError('Send notification failed');
} finally {
stop();
}
};
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}>
<Switch
loading={isAlertEnabled === undefined}
checked={isAlertEnabled}
disabled={isSwitchDisabled}
switchName='Alert me by email'
switchDescription='You will receive an alert every 24H if you have a down status.'
onChange={(e) => onChangeSwitchHandler({ emailAlert: e })}
/>
<button
className='defaultButton'
disabled={isSendingTestNotification}
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>
</>
);
}

View file

@ -1,18 +1,18 @@
//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';
import { toast, ToastOptions } from 'react-toastify';
import 'react-toastify/dist/ReactToastify.css';
import classes from '../UserSettings.module.css';
//Components
import Error from '../../../Components/UI/Error/Error';
import Info from '../../../Components/UI/Info/Info';
import Error from '~/Components/UI/Error/Error';
import Info from '~/Components/UI/Info/Info';
import { useLoader } from '~/contexts/LoaderContext';
import { useFormStatus } from '~/hooks';
import { EmailSettingDTO } from '~/types/api/setting.types';
export default function EmailSettings(props) {
//Var
const toastOptions = {
export default function EmailSettings(props: EmailSettingDTO) {
const toastOptions: ToastOptions = {
position: 'top-right',
autoClose: 8000,
hideProgressBar: false,
@ -26,24 +26,23 @@ export default function EmailSettings(props) {
register,
handleSubmit,
reset,
formState: { errors, isSubmitting, isValid },
} = useForm({ mode: 'onChange' });
formState: { errors, isSubmitting },
} = useForm<EmailSettingDTO>({ mode: 'onChange' });
const { isLoading, error, setIsLoading, handleError, clearError } = useFormStatus();
const { start, stop } = useLoader();
////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.
const formSubmitHandler = async (data: EmailSettingDTO) => {
start();
clearError();
setIsLoading(true);
//POST API to send the new mail address
try {
const response = await fetch('/api/account/updateEmail', {
const response = await fetch('/api/v1/account/email', {
method: 'PUT',
headers: {
'Content-type': 'application/json',
@ -53,10 +52,8 @@ export default function EmailSettings(props) {
const result = await response.json();
if (!response.ok) {
setIsLoading(false);
reset();
setError(result.message);
setTimeout(() => setError(), 4000);
handleError(result.message);
} else {
reset();
setIsLoading(false);
@ -65,9 +62,10 @@ export default function EmailSettings(props) {
}
} catch (error) {
reset();
handleError('Updating your email failed.');
} finally {
stop();
setIsLoading(false);
setError("Can't update your email. Contact your administrator.");
setTimeout(() => setError(), 4000);
}
};
return (
@ -109,13 +107,9 @@ export default function EmailSettings(props) {
</p>
<button
className={classes.AccountSettingsButton}
disabled={!isValid || isSubmitting}
disabled={isSubmitting || isLoading}
>
{isLoading ? (
<SpinnerDotted size={20} thickness={150} speed={100} color='#fff' />
) : (
'Update your email'
)}
Update your email
</button>
</form>
)}

View file

@ -1,23 +1,26 @@
//Lib
import { toast } from 'react-toastify';
import 'react-toastify/dist/ReactToastify.css';
import classes from '../UserSettings.module.css';
import { IconExternalLink, IconTrash } from '@tabler/icons-react';
import { fromUnixTime } from 'date-fns';
import Link from 'next/link';
import { useEffect, useState } from 'react';
import { useForm } from 'react-hook-form';
import { SpinnerDotted } from 'spinners-react';
import { v4 as uuidv4 } from 'uuid';
import timestampConverter from '../../../helpers/functions/timestampConverter';
import { IconTrash, IconExternalLink } from '@tabler/icons-react';
import Link from 'next/link';
import { toast, ToastOptions } from 'react-toastify';
import 'react-toastify/dist/ReactToastify.css';
import { useFormStatus } from '~/hooks';
import { IntegrationTokenType, Optional, TokenPermissionEnum, TokenPermissionsType } from '~/types';
import classes from '../UserSettings.module.css';
//Components
import Error from '../../../Components/UI/Error/Error';
import CopyButton from '../../../Components/UI/CopyButton/CopyButton';
import Info from '../../../Components/UI/Info/Info';
import CopyButton from '~/Components/UI/CopyButton/CopyButton';
import Error from '~/Components/UI/Error/Error';
import Info from '~/Components/UI/Info/Info';
import { useLoader } from '~/contexts/LoaderContext';
type IntegrationsDataForm = {
tokenName: string;
};
export default function Integrations() {
//Var
const toastOptions = {
const toastOptions: ToastOptions = {
position: 'top-right',
autoClose: 5000,
hideProgressBar: false,
@ -32,16 +35,28 @@ export default function Integrations() {
handleSubmit,
reset,
formState: { errors, isSubmitting, isValid },
} = useForm({ mode: 'onChange' });
} = useForm<IntegrationsDataForm>({ mode: 'onChange' });
const { start, stop } = useLoader();
const { error, handleError, clearError, setIsLoading, isLoading } = useFormStatus();
const renderPermissionBadges = (permissions: TokenPermissionsType) => {
return Object.entries(permissions)
.filter(([, hasPermission]) => hasPermission)
.map(([key]) => (
<div key={key} className={classes.permissionBadge}>
{key.charAt(0).toUpperCase() + key.slice(1)}
</div>
));
};
////State
const [isLoading, setIsLoading] = useState(false);
const [isDeleteLoading, setIsDeleteLoading] = useState(false);
const [tokenList, setTokenList] = useState([]);
const [error, setError] = useState();
const [lastGeneratedToken, setLastGeneratedToken] = useState();
const [deletingToken, setDeletingToken] = useState(null);
const [permissions, setPermissions] = useState({
const [tokenList, setTokenList] = useState<Array<IntegrationTokenType>>();
const [lastGeneratedToken, setLastGeneratedToken] =
useState<Optional<{ name: string; value: string }>>();
const [deletingToken, setDeletingToken] = useState<Optional<IntegrationTokenType>>(undefined);
const [permissions, setPermissions] = useState<TokenPermissionsType>({
create: false,
read: false,
update: false,
@ -49,30 +64,34 @@ export default function Integrations() {
});
const fetchTokenList = async () => {
start();
try {
const response = await fetch('/api/account/tokenManager', {
const response = await fetch('/api/v1/integration/token-manager', {
method: 'GET',
headers: {
'Content-type': 'application/json',
},
});
const tokensArray = await response.json();
setTokenList(tokensArray);
const data: Array<IntegrationTokenType> = await response.json();
setTokenList(data);
} catch (error) {
console.log('Fetching token list failed.');
handleError('Fetching token list failed.');
} finally {
stop();
}
};
////LifeCycle
useEffect(() => {
fetchTokenList();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
// Permissions handler
const hasNoPermissionSelected = () => {
return !Object.values(permissions).some((value) => value);
};
const togglePermission = (permissionType) => {
const togglePermission = (permissionType: TokenPermissionEnum) => {
const updatedPermissions = {
...permissions,
[permissionType]: !permissions[permissionType],
@ -88,60 +107,48 @@ export default function Integrations() {
});
};
//Form submit Handler for ADD a new token
const formSubmitHandler = async (data) => {
//Remove old error
setError();
//Loading button on submit to avoid multiple send.
//Form submit handler to ADD a new token
const formSubmitHandler = async (data: IntegrationsDataForm) => {
start();
clearError();
setIsLoading(true);
//Generate a UUIDv4
const token = uuidv4();
setLastGeneratedToken({ name: data.tokenName, value: token });
// Post API to send the new token integration
try {
const response = await fetch('/api/account/tokenManager', {
const response = await fetch('/api/v1/integration/token-manager', {
method: 'POST',
headers: {
'Content-type': 'application/json',
},
body: JSON.stringify({
name: data.tokenName,
token: token,
creation: Math.floor(Date.now() / 1000),
expiration: null,
permissions: permissions,
}),
});
const result = await response.json();
setLastGeneratedToken({ name: data.tokenName, value: result.token });
if (!response.ok) {
setIsLoading(false);
reset();
resetPermissions();
toast.error(result.message, toastOptions);
setTimeout(() => setError(), 4000);
} else {
reset();
resetPermissions();
fetchTokenList();
setIsLoading(false);
toast.success('🔑 Token generated !', toastOptions);
}
} catch (error) {
reset();
resetPermissions();
toast.error('Failed to generate a new token', toastOptions);
} finally {
setIsLoading(false);
toast.error("Can't generate your token. Contact your administrator.", toastOptions);
setTimeout(() => setError(), 4000);
resetPermissions();
reset();
stop();
}
};
//Delete token
const deleteTokenHandler = async (tokenName) => {
const deleteTokenHandler = async (tokenName: string) => {
setIsDeleteLoading(true);
try {
const response = await fetch('/api/account/tokenManager', {
const response = await fetch('/api/v1/integration/token-manager', {
method: 'DELETE',
headers: {
'Content-type': 'application/json',
@ -154,7 +161,6 @@ export default function Integrations() {
if (!response.ok) {
toast.error(result.message, toastOptions);
setTimeout(() => setError(), 4000);
setIsDeleteLoading(false);
} else {
fetchTokenList();
@ -163,11 +169,10 @@ export default function Integrations() {
}
} catch (error) {
setIsDeleteLoading(false);
toast.error("Can't delete your token. Contact your administrator.", toastOptions);
setTimeout(() => setError(), 4000);
toast.error('Failed to delete the token', toastOptions);
} finally {
setIsDeleteLoading(false);
setDeletingToken(null);
setDeletingToken(undefined);
}
};
@ -205,25 +210,25 @@ export default function Integrations() {
<div className={classes.permissionsWrapper}>
<div
className={`${classes.permissionBadge} ${permissions.create ? classes.highlight : ''}`}
onClick={() => togglePermission('create')}
onClick={() => togglePermission(TokenPermissionEnum.CREATE)}
>
Create
</div>
<div
className={`${classes.permissionBadge} ${permissions.read ? classes.highlight : ''}`}
onClick={() => togglePermission('read')}
onClick={() => togglePermission(TokenPermissionEnum.READ)}
>
Read
</div>
<div
className={`${classes.permissionBadge} ${permissions.update ? classes.highlight : ''}`}
onClick={() => togglePermission('update')}
onClick={() => togglePermission(TokenPermissionEnum.UPDATE)}
>
Update
</div>
<div
className={`${classes.permissionBadge} ${permissions.delete ? classes.highlight : ''}`}
onClick={() => togglePermission('delete')}
onClick={() => togglePermission(TokenPermissionEnum.DELETE)}
>
Delete
</div>
@ -234,11 +239,7 @@ export default function Integrations() {
className={classes.AccountSettingsButton}
disabled={!isValid || isSubmitting || hasNoPermissionSelected()}
>
{isLoading ? (
<SpinnerDotted size={15} thickness={150} speed={100} color='#fff' />
) : (
'Generate'
)}
Generate
</button>
</form>
{errors.tokenName && errors.tokenName.type === 'maxLength' && (
@ -272,25 +273,19 @@ export default function Integrations() {
>
<div className={classes.tokenCardHeader}>{token.name}</div>
<div className={classes.tokenCardBody}>
<p>
<div className={classes.tokenInfo}>
<strong>Created at:</strong>
{timestampConverter(token.creation)}
</p>
<p>
{fromUnixTime(token.creation).toLocaleString()}
</div>
<div className={classes.tokenInfo}>
<strong>Permission:</strong>
<div className={classes.permissionBadges}>
{Object.keys(token.permissions).map((permission) =>
token.permissions[permission] ? (
<div key={permission} className={classes.permissionBadge}>
{permission.charAt(0).toUpperCase() + permission.slice(1)}
</div>
) : null
)}
{renderPermissionBadges(token.permissions)}
</div>
</p>
</div>
{lastGeneratedToken && lastGeneratedToken.name === token.name && (
<>
<p>
<div className={classes.tokenInfo}>
<strong>Token:</strong>
<CopyButton
size={22}
@ -299,10 +294,11 @@ export default function Integrations() {
>
<span>{lastGeneratedToken.value}</span>
</CopyButton>
</p>
<Info color='#3498db'>
This token will not be shown again. Please save it.
</Info>
</div>
<Info
color='#3498db'
message='This token will not be shown again. Please save it.'
/>
</>
)}
{deletingToken && deletingToken.name === token.name && (
@ -313,14 +309,11 @@ export default function Integrations() {
disabled={isDeleteLoading}
>
Confirm
{isDeleteLoading && (
<SpinnerDotted size={15} thickness={150} speed={100} color='#fff' />
)}{' '}
</button>
{!isDeleteLoading && (
<button
className={classes.cancelButton}
onClick={() => setDeletingToken(null)}
onClick={() => setDeletingToken(undefined)}
>
Cancel
</button>

View file

@ -1,17 +1,15 @@
//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';
import { toast, ToastOptions } from 'react-toastify';
import 'react-toastify/dist/ReactToastify.css';
import { useFormStatus } from '~/hooks';
import { PasswordSettingDTO } from '~/types';
import classes from '../UserSettings.module.css';
//Components
import Error from '../../../Components/UI/Error/Error';
import { useLoader } from '~/contexts/LoaderContext';
export default function PasswordSettings(props) {
//Var
const toastOptions = {
export default function PasswordSettings() {
const toastOptions: ToastOptions = {
position: 'top-right',
autoClose: 5000,
hideProgressBar: false,
@ -25,24 +23,19 @@ export default function PasswordSettings(props) {
register,
handleSubmit,
reset,
formState: { errors, isSubmitting, isValid },
} = useForm({ mode: 'onChange' });
formState: { isSubmitting },
} = useForm<PasswordSettingDTO>({ mode: 'onChange' });
const { start, stop } = useLoader();
////State
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState();
const { isLoading, setIsLoading } = useFormStatus();
////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.
const formSubmitHandler = async (data: PasswordSettingDTO) => {
start();
setIsLoading(true);
//POST API to send the new and old password
try {
const response = await fetch('/api/account/updatePassword', {
const response = await fetch('/api/v1/account/password', {
method: 'PUT',
headers: {
'Content-type': 'application/json',
@ -52,20 +45,16 @@ export default function PasswordSettings(props) {
const result = await response.json();
if (!response.ok) {
setIsLoading(false);
reset();
setError(result.message);
setTimeout(() => setError(), 4000);
toast.error(result.message, toastOptions);
} else {
reset();
setIsLoading(false);
toast.success('🔑 Password edited !', toastOptions);
}
} catch (error) {
toast.error('Failed to update password. Please try again.', toastOptions);
} finally {
stop();
reset();
setIsLoading(false);
setError("Can't update your password. Contact your administrator.");
setTimeout(() => setError(), 4000);
}
};
return (
@ -78,7 +67,6 @@ export default function PasswordSettings(props) {
<div className={classes.setting}>
<div className={classes.bwFormWrapper}>
<form onSubmit={handleSubmit(formSubmitHandler)} className={classes.bwForm}>
{error && <Error message={error} />}
<p>
<input
type='password'
@ -87,9 +75,6 @@ export default function PasswordSettings(props) {
required: true,
})}
/>
{errors.oldPassword && errors.oldPassword.type === 'required' && (
<small className={classes.errorMessage}>This field is required.</small>
)}
</p>
<p>
<input
@ -99,16 +84,12 @@ export default function PasswordSettings(props) {
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
className={classes.AccountSettingsButton}
disabled={isLoading || isSubmitting}
>
Update your password
</button>
</form>
</div>

View file

@ -1,92 +0,0 @@
//Lib
import 'react-toastify/dist/ReactToastify.css';
import classes from './UserSettings.module.css';
import { useState, useEffect } 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';
import Integrations from './Integrations/Integrations';
export default function UserSettings(props) {
//States
const [tab, setTab] = useState('General');
const [wizardEnv, setWizardEnv] = useState({});
//ComponentDidMount
useEffect(() => {
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();
}, []);
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>
{wizardEnv.DISABLE_INTEGRATIONS !== 'true' && (
<button
className={tab === 'Integrations' ? classes.tabListButtonActive : classes.tabListButton}
onClick={() => setTab('Integrations')}
>
Integrations
</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 />
</>
)}
{tab === 'Integrations' && (
<>
<Integrations />
</>
)}
</div>
);
}

View file

@ -117,7 +117,7 @@
align-content: baseline;
}
.tokenCardBody p {
.tokenInfo {
display: flex;
align-items: center;
gap: 5px;
@ -387,7 +387,7 @@
.headerFormAppriseUrls {
font-weight: 500;
color: #494b7a;
margin: 40px 0px 10px 0px;
margin-bottom: 10px;
display: flex;
padding-right: 5px;
}

View file

@ -0,0 +1,100 @@
import 'react-toastify/dist/ReactToastify.css';
import classes from './UserSettings.module.css';
import { useState, useEffect } from 'react';
import { Session } from 'next-auth';
import { Optional, WizardEnvType, SessionStatus } from '~/types';
// 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';
import Integrations from './Integrations/Integrations';
type UserSettingsProps = {
status: SessionStatus;
data: Session;
};
export default function UserSettings({ data }: UserSettingsProps) {
const [tab, setTab] = useState<'General' | 'Notifications' | 'Integrations'>('General');
const [wizardEnv, setWizardEnv] = useState<Optional<WizardEnvType>>(undefined);
// Fetch wizard environment on mount
useEffect(() => {
const fetchWizardEnv = async () => {
try {
const response = await fetch('/api/v1/account/wizard-env');
const data: WizardEnvType = await response.json();
setWizardEnv(data);
} catch (error) {
console.error('Failed to fetch wizard environment:', error);
}
};
fetchWizardEnv();
}, []);
// If Integrations tab is selected but disabled, fallback to General
useEffect(() => {
if (tab === 'Integrations' && wizardEnv?.DISABLE_INTEGRATIONS === 'true') {
setTab('General');
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [wizardEnv?.DISABLE_INTEGRATIONS]);
return (
<div className={classes.containerSettings}>
<h1 style={{ color: '#494b7a', textAlign: 'left', marginLeft: '30px' }}>Account</h1>
{wizardEnv != undefined && (
<>
<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>
{wizardEnv.DISABLE_INTEGRATIONS !== 'true' && (
<button
className={
tab === 'Integrations' ? classes.tabListButtonActive : classes.tabListButton
}
onClick={() => setTab('Integrations')}
>
Integrations
</button>
)}
</div>
{tab === 'General' && (
<>
<PasswordSettings />
<EmailSettings email={data.user?.email ?? undefined} />
<UsernameSettings username={data.user?.name ?? undefined} />
</>
)}
{tab === 'Notifications' && (
<>
<EmailAlertSettings />
<AppriseAlertSettings />
</>
)}
{tab === 'Integrations' && wizardEnv.DISABLE_INTEGRATIONS !== 'true' && <Integrations />}
</>
)}
</div>
);
}

View file

@ -1,18 +1,17 @@
//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';
import { toast, ToastOptions } from 'react-toastify';
import 'react-toastify/dist/ReactToastify.css';
import { useFormStatus } from '~/hooks';
import { UsernameSettingDTO } from '~/types';
import classes from '../UserSettings.module.css';
//Components
import Error from '../../../Components/UI/Error/Error';
import Info from '../../../Components/UI/Info/Info';
import Info from '~/Components/UI/Info/Info';
import { useLoader } from '~/contexts/LoaderContext';
export default function UsernameSettings(props) {
//Var
const toastOptions = {
export default function UsernameSettings(props: UsernameSettingDTO) {
const toastOptions: ToastOptions = {
position: 'top-right',
autoClose: 8000,
hideProgressBar: false,
@ -26,24 +25,22 @@ export default function UsernameSettings(props) {
register,
handleSubmit,
reset,
formState: { errors, isSubmitting, isValid },
} = useForm({ mode: 'onChange' });
formState: { errors, isSubmitting },
} = useForm<UsernameSettingDTO>({ mode: 'onChange' });
const { start, stop } = useLoader();
const { isLoading, setIsLoading } = useFormStatus();
////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.
const formSubmitHandler = async (data: UsernameSettingDTO) => {
start();
setIsLoading(true);
//POST API to update the username
try {
const response = await fetch('/api/account/updateUsername', {
const response = await fetch('/api/v1/account/username', {
method: 'PUT',
headers: {
'Content-type': 'application/json',
@ -53,21 +50,17 @@ export default function UsernameSettings(props) {
const result = await response.json();
if (!response.ok) {
setIsLoading(false);
reset();
setError(result.message);
setTimeout(() => setError(), 4000);
toast.error(result.message, toastOptions);
} else {
reset();
setIsLoading(false);
setInfo(true);
toast.success('Username edited !', toastOptions);
}
} catch (error) {
toast.error('Failed to update username. Please try again.', toastOptions);
} finally {
reset();
stop();
setIsLoading(false);
setError("Can't update your username. Contact your administrator.");
setTimeout(() => setError(), 4000);
}
};
return (
@ -84,30 +77,29 @@ export default function UsernameSettings(props) {
//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.' />
<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.',
value: /^[a-z]{1,40}$/,
message: 'Only a-z characters are allowed',
},
maxLength: {
value: 10,
message: '15 characters max.',
value: 40,
message: '40 characters max.',
},
minLength: {
value: 5,
message: '5 characters min.',
value: 1,
message: '1 characters min.',
},
})}
/>
@ -117,13 +109,9 @@ export default function UsernameSettings(props) {
</p>
<button
className={classes.AccountSettingsButton}
disabled={!isValid || isSubmitting}
disabled={isLoading || isSubmitting}
>
{isLoading ? (
<SpinnerDotted size={20} thickness={150} speed={100} color='#fff' />
) : (
'Update your username'
)}
Update your username
</button>
</form>
)}

View file

@ -6,23 +6,29 @@ FROM node:22-bookworm-slim as base
# build stage
FROM base AS deps
RUN corepack enable && corepack prepare pnpm@9 --activate
WORKDIR /app
COPY package.json package-lock.json ./
COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./
RUN npm ci --omit=dev
RUN pnpm install --frozen-lockfile --prod
FROM base AS builder
RUN corepack enable && corepack prepare pnpm@9 --activate
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./
RUN pnpm install --frozen-lockfile
COPY . .
RUN sed -i "s/images:/output: 'standalone',images:/" next.config.js
RUN sed -i "s/images:/output: 'standalone',images:/" next.config.ts
RUN npm run build
RUN pnpm run build
# run stage
FROM base AS runner
@ -35,7 +41,7 @@ ENV HOSTNAME=
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 && \
supervisor curl jq jc borgbackup/bookworm-backports openssh-server && \
apt-get clean && rm -rf /var/lib/apt/lists/*
RUN groupadd -g ${GID} borgwarehouse && useradd -m -u ${UID} -g ${GID} borgwarehouse
@ -50,7 +56,6 @@ 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

View file

@ -1,16 +1,19 @@
<div align="center">
[![TypeScript][typescript.js]][typescript-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)
[![Docker Pulls](https://img.shields.io/docker/pulls/borgwarehouse/borgwarehouse?label=borgwarehouse&style=for-the-badge&logo=docker)](https://hub.docker.com/r/borgwarehouse/borgwarehouse)
</div>
<h3 align="center">BorgWarehouse</h3>
<img src="public/borgwarehouse-logo-violet.svg" alt="BorgWarehouse" style="margin: 30px 0">
<p align="center">
A fast and modern WebUI for a BorgBackup's central repository server.
@ -20,17 +23,17 @@
<div align="center">
<a href="https://borgwarehouse.com">
<img src="medias/borgwarehouse-og.png" alt="presentation">
<img src="medias/borgwarehouse-og.jpg" alt="presentation">
</a>
</div>
## ⭐ 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 ?
@ -41,13 +44,14 @@ Today, if you want to have a large server on which you centralize backups of Bor
With BorgWarehouse, you have an interface that allows you to do all this simply and quickly :
- **add** repositories
- **edit** existing repositories
- **delete** repositories
- be **alerted** if there are no recent backups
- **monitor** the volume of data
- **flexibly manage quotas** for each repository
- ...
- **add** repositories
- **edit** existing repositories
- **delete** repositories
- be **alerted** if there are no recent backups
- **monitor** the volume of data
- **flexibly manage quotas** for each repository
- manage everything you want through the **REST API**
- ...
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.
@ -71,13 +75,22 @@ Check the online documentation [just here](https://borgwarehouse.com/docs/admin-
## ❤️ 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>
<a href="https://github.com/fphammerle"><img src="https://avatars.githubusercontent.com/fphammerle" style="width:50px; border-radius:50%;"/></a>
<a href="https://github.com/MacH59-cos"><img src="https://avatars.githubusercontent.com/MacH59-cos" style="width:50px; border-radius:50%;"/></a>
<a href="https://github.com/shrippen"><img src="https://avatars.githubusercontent.com/shrippen" style="width:50px; border-radius:50%;"/></a>
<a href="https://github.com/daschmidt1994"><img src="https://avatars.githubusercontent.com/daschmidt1994" 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>
<a href="https://github.com/Drallibor"><img src="https://avatars.githubusercontent.com/Drallibor" style="width:25px; border-radius:50%;"/></a>
<a href="https://github.com/shad-lp"><img src="https://avatars.githubusercontent.com/shad-lp" style="width:25px; border-radius:50%;"/></a>
<a href="https://github.com/Magneticdud"><img src="https://avatars.githubusercontent.com/Magneticdud" style="width:25px; border-radius:50%;"/></a>
[typescript.js]: https://img.shields.io/badge/TypeScript-3178C6?style=for-the-badge&logo=typescript&logoColor=white
[typescript-url]: https://www.typescriptlang.org/
[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

View file

@ -0,0 +1,25 @@
import { createContext, useContext } from 'react';
import NProgress from 'nprogress';
type LoaderContextType = {
start: () => void;
stop: () => void;
};
const LoaderContext = createContext<LoaderContextType>({
start: () => {},
stop: () => {},
});
export const LoaderProvider = ({ children }: { children: React.ReactNode }) => {
const start = () => NProgress.start();
const stop = () => NProgress.done();
return (
<LoaderContext.Provider value={{ start, stop }}>
{children}
</LoaderContext.Provider>
);
};
export const useLoader = () => useContext(LoaderContext);

View file

@ -20,8 +20,6 @@ services:
- ${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

View file

@ -1,40 +0,0 @@
# 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")
}

View file

@ -1,24 +1,21 @@
[supervisord]
nodaemon=true
logfile=/home/borgwarehouse/logs/supervisord.log
logfile=/dev/stdout
logfile_maxbytes=0
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
stdout_logfile=/dev/stdout
stdout_logfile_maxbytes=0
stderr_logfile=/dev/stderr
stderr_logfile_maxbytes=0
redirect_stderr=false
[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
stdout_logfile=/dev/stdout
stdout_logfile_maxbytes=0
stderr_logfile=/dev/stderr
stderr_logfile_maxbytes=0
redirect_stderr=false

16
eslint.config.mjs Normal file
View file

@ -0,0 +1,16 @@
import { defineConfig, globalIgnores } from 'eslint/config';
import nextVitals from 'eslint-config-next/core-web-vitals';
const eslintConfig = defineConfig([
...nextVitals,
// Override default ignores of eslint-config-next.
globalIgnores([
// Default ignores of eslint-config-next:
'.next/**',
'out/**',
'build/**',
'next-env.d.ts',
]),
]);
export default eslintConfig;

View file

@ -0,0 +1,12 @@
const ApiResponse = {
success: vi.fn(),
badRequest: vi.fn(),
unauthorized: vi.fn(),
forbidden: vi.fn(),
notFound: vi.fn(),
methodNotAllowed: vi.fn(),
validationError: vi.fn(),
serverError: vi.fn(),
};
export default ApiResponse;

View file

@ -0,0 +1,71 @@
import { NextApiResponse } from 'next';
const getErrorMessage = (error: unknown): any => {
if (error instanceof Error) {
const shellError = error as any;
// Handle shell errors
if ('code' in shellError || 'stderr' in shellError || 'stdout' in shellError) {
return {
code: shellError.code ?? null,
cmd: shellError.cmd ?? null,
stderr: shellError.stderr ?? null,
stdout: shellError.stdout ?? null,
};
}
return error.message;
}
if (typeof error === 'object' && error !== null && 'code' in error) {
const err = error as { code?: string };
if (err.code === 'ENOENT') {
return 'No such file or directory';
}
}
return 'API error, contact the administrator';
};
export default class ApiResponse {
static success<T>(res: NextApiResponse, message = 'Success', data?: T) {
res.status(200).json({ status: 200, message, data });
}
static badRequest(res: NextApiResponse, message = 'Bad Request') {
res.status(400).json({ status: 400, message });
}
static unauthorized(res: NextApiResponse, message = 'Unauthorized') {
res.status(401).json({ status: 401, message });
}
static forbidden(res: NextApiResponse, message = 'Forbidden') {
res.status(403).json({ status: 403, message });
}
static notFound(res: NextApiResponse, message = 'Not Found') {
res.status(404).json({ status: 404, message });
}
static methodNotAllowed(res: NextApiResponse, message = 'Method Not Allowed') {
res.status(405).json({ status: 405, message });
}
static validationError(res: NextApiResponse, message = 'Validation Error') {
res.status(422).json({ status: 422, message });
}
static conflict(res: NextApiResponse, message = 'Conflict') {
res.status(409).json({ status: 409, message });
}
static serverError(
res: NextApiResponse,
error: unknown,
fallbackMessage = 'API error, contact the administrator'
) {
const message = getErrorMessage(error) || fallbackMessage;
res.status(500).json({ status: 500, message });
}
}

View file

@ -1,11 +0,0 @@
// This function is used to hash user passwords and to verify them with the bcryptjs library
//Lib
import { hash, compare } from 'bcryptjs';
export async function hashPassword(password) {
return await hash(password, 12);
}
export async function verifyPassword(password, hashedPassword) {
return await compare(password, hashedPassword);
}

View file

@ -0,0 +1,4 @@
import lanCommandOption from './lanCommandOption';
import isSshPubKeyDuplicate from './isSshPubKeyDuplicate';
export { lanCommandOption, isSshPubKeyDuplicate };

View file

@ -0,0 +1,70 @@
import { describe, it, expect } from 'vitest';
import isSshPubKeyDuplicate from './isSshPubKeyDuplicate';
import { Optional, Repository } from '~/types';
describe('isSshPubKeyDuplicate', () => {
it('should return true if the SSH public key is duplicated', () => {
const pubKey = 'ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEArandomkey user@hostname';
const repoList: Array<Optional<Repository>> = [
{ sshPublicKey: 'ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEArandomkey other@host' } as Repository,
];
expect(isSshPubKeyDuplicate(pubKey, repoList)).toBe(true);
});
it('should return false if the SSH public key is not duplicated', () => {
const pubKey = 'ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEAdifferentkey user@hostname';
const repoList: Array<Optional<Repository>> = [
{ sshPublicKey: 'ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEArandomkey other@host' } as Repository,
];
expect(isSshPubKeyDuplicate(pubKey, repoList)).toBe(false);
});
it('should throw an error if pubKey is missing', () => {
const repoList: Array<Optional<Repository>> = [
{ sshPublicKey: 'ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEArandomkey other@host' } as Repository,
];
expect(() => isSshPubKeyDuplicate('', repoList)).toThrow(
'Missing or invalid parameters for duplicate SSH public key check.'
);
});
it('should throw an error if repoList is missing', () => {
const pubKey = 'ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEArandomkey user@hostname';
expect(() => isSshPubKeyDuplicate(pubKey, null as any)).toThrow(
'Missing or invalid parameters for duplicate SSH public key check.'
);
});
it('should return false if repoList is empty', () => {
const pubKey = 'ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEArandomkey user@hostname';
const repoList: Array<Optional<Repository>> = [];
expect(isSshPubKeyDuplicate(pubKey, repoList)).toBe(false);
});
it('should handle repositories with undefined sshPublicKey', () => {
const pubKey = 'ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEArandomkey user@hostname';
const repoList: Array<Optional<Repository>> = [
// @ts-expect-error
{ sshPublicKey: undefined } as Repository,
{ sshPublicKey: 'ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEArandomkey other@host' } as Repository,
];
expect(isSshPubKeyDuplicate(pubKey, repoList)).toBe(true);
});
it('should handle repositories with null sshPublicKey', () => {
const pubKey = 'ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEArandomkey user@hostname';
const repoList: Array<Optional<Repository>> = [
// @ts-expect-error
{ sshPublicKey: null } as Repository,
{ sshPublicKey: 'ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEAdifferentkey other@host' } as Repository,
];
expect(isSshPubKeyDuplicate(pubKey, repoList)).toBe(false);
});
});

View file

@ -1,3 +1,5 @@
import { Optional, Repository } from '~/types';
/**
* Checks if the given SSH public key is duplicated in the provided repository list by removing the comment part.
*
@ -6,7 +8,10 @@
* @returns {boolean} - Returns true if the SSH public key is duplicated, otherwise false.
* @throws {Error} - Throws an error if required parameters are missing or invalid.
*/
export default function isSshPubKeyDuplicate(pubKey, repoList) {
export default function isSshPubKeyDuplicate(
pubKey: string,
repoList: Array<Optional<Repository>>
): boolean {
if (!pubKey || !repoList || !Array.isArray(repoList)) {
throw new Error('Missing or invalid parameters for duplicate SSH public key check.');
}
@ -16,7 +21,7 @@ export default function isSshPubKeyDuplicate(pubKey, repoList) {
// Check if the normalized key is already in the repository list
return repoList.some((repo) => {
const repoSshKeyWithoutComment = repo.sshPublicKey.split(' ').slice(0, 2).join(' ');
const repoSshKeyWithoutComment = repo?.sshPublicKey?.split(' ').slice(0, 2).join(' ');
return repoSshKeyWithoutComment === pubKeyWithoutComment;
});
}

View file

@ -1,13 +0,0 @@
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.HIDE_SSH_PORT === 'true' ? '' : ':' + wizardEnv.SSH_SERVER_PORT_LAN;
} else {
FQDN = wizardEnv.FQDN;
SSH_SERVER_PORT = wizardEnv.HIDE_SSH_PORT === 'true' ? '' : ':' + wizardEnv.SSH_SERVER_PORT;
}
return { FQDN, SSH_SERVER_PORT };
}

View file

@ -0,0 +1,75 @@
import { describe, it, expect } from 'vitest';
import lanCommandOption from './lanCommandOption';
import { WizardEnvType } from '~/types';
describe('lanCommandOption', () => {
it('should return undefined values when wizardEnv is not provided', () => {
const result = lanCommandOption();
expect(result).toEqual({ FQDN: undefined, SSH_SERVER_PORT: undefined });
});
it('should return FQDN and SSH_SERVER_PORT from wizardEnv when lanCommand is false', () => {
const wizardEnv: Partial<WizardEnvType> = {
FQDN: 'example.com',
FQDN_LAN: 'lan.example.com',
SSH_SERVER_PORT: '22',
SSH_SERVER_PORT_LAN: '2222',
HIDE_SSH_PORT: 'false',
};
const result = lanCommandOption(wizardEnv, false);
expect(result).toEqual({ FQDN: 'example.com', SSH_SERVER_PORT: ':22' });
});
it('should return FQDN_LAN and SSH_SERVER_PORT_LAN from wizardEnv when lanCommand is true', () => {
const wizardEnv: Partial<WizardEnvType> = {
FQDN: 'example.com',
FQDN_LAN: 'lan.example.com',
SSH_SERVER_PORT: '22',
SSH_SERVER_PORT_LAN: '2222',
HIDE_SSH_PORT: 'false',
};
const result = lanCommandOption(wizardEnv, true);
expect(result).toEqual({ FQDN: 'lan.example.com', SSH_SERVER_PORT: ':2222' });
});
it('should return undefined for SSH_SERVER_PORT when HIDE_SSH_PORT is true', () => {
const wizardEnv: Partial<WizardEnvType> = {
FQDN: 'example.com',
FQDN_LAN: 'lan.example.com',
SSH_SERVER_PORT: '22',
SSH_SERVER_PORT_LAN: '2222',
HIDE_SSH_PORT: 'true',
};
const result = lanCommandOption(wizardEnv, false);
expect(result).toEqual({ FQDN: 'example.com', SSH_SERVER_PORT: undefined });
});
it('should fallback to FQDN and should leave ssh server port to undefined for some usages', () => {
const wizardEnv: Partial<WizardEnvType> = {
FQDN: 'example.com',
FQDN_LAN: undefined,
SSH_SERVER_PORT: '22',
SSH_SERVER_PORT_LAN: undefined,
HIDE_SSH_PORT: 'false',
};
const result = lanCommandOption(wizardEnv, true);
expect(result).toEqual({ FQDN: 'example.com', SSH_SERVER_PORT: undefined });
});
it('should handle missing FQDN and SSH_SERVER_PORT gracefully', () => {
const wizardEnv: Partial<WizardEnvType> = {
FQDN: undefined,
FQDN_LAN: 'lan.example.com',
SSH_SERVER_PORT: undefined,
SSH_SERVER_PORT_LAN: '2222',
HIDE_SSH_PORT: 'false',
};
const result = lanCommandOption(wizardEnv, false);
expect(result).toEqual({ FQDN: undefined, SSH_SERVER_PORT: undefined });
});
});

View file

@ -0,0 +1,24 @@
import { Optional, WizardEnvType } from '~/types';
export default function lanCommandOption(
wizardEnv?: Partial<WizardEnvType>,
lanCommand?: boolean
): { FQDN: Optional<string>; SSH_SERVER_PORT: Optional<string> } {
if (!wizardEnv) {
return { FQDN: undefined, SSH_SERVER_PORT: undefined };
}
const { FQDN, FQDN_LAN, SSH_SERVER_PORT, SSH_SERVER_PORT_LAN, HIDE_SSH_PORT } = wizardEnv;
const isPortHidden = HIDE_SSH_PORT === 'true';
const selectedFQDN = lanCommand && FQDN_LAN ? FQDN_LAN : FQDN;
const selectedPort = lanCommand ? SSH_SERVER_PORT_LAN : SSH_SERVER_PORT;
const formattedPort = !isPortHidden && selectedPort ? `:${selectedPort}` : undefined;
return {
FQDN: selectedFQDN,
SSH_SERVER_PORT: formattedPort,
};
}

View file

@ -1,18 +0,0 @@
//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;
}

View file

@ -1,36 +0,0 @@
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);
}
}

View file

@ -0,0 +1,52 @@
import { describe, it, expect } from 'vitest';
import repositoryNameCheck from './repositoryNameCheck';
describe('repositoryNameCheck', () => {
it('should return true for a valid 8-character hexadecimal string', () => {
expect(repositoryNameCheck('a1b2c3d4')).toBe(true);
});
it('should return false for a string shorter than 8 characters', () => {
expect(repositoryNameCheck('a1b2c3')).toBe(false);
});
it('should return false for a string longer than 8 characters', () => {
expect(repositoryNameCheck('a1b2c3d4e5')).toBe(false);
});
it('should return false for a string with non-hexadecimal characters', () => {
expect(repositoryNameCheck('a1b2c3g4')).toBe(false);
});
it('should return false for an empty string', () => {
expect(repositoryNameCheck('')).toBe(false);
});
it('should return false for a string with special characters', () => {
expect(repositoryNameCheck('a1b2c3d@')).toBe(false);
});
it('should return false for a string with uppercase hexadecimal characters', () => {
expect(repositoryNameCheck('A1B2C3D4')).toBe(false);
});
it('should return false for a string with spaces', () => {
expect(repositoryNameCheck('a1b2 c3d4')).toBe(false);
});
it('should return false for a non string name', () => {
expect(repositoryNameCheck(12345678)).toBe(false);
});
it('should return false for null', () => {
expect(repositoryNameCheck(null)).toBe(false);
});
it('should return false for undefined', () => {
expect(repositoryNameCheck(undefined)).toBe(false);
});
it('should return false for boolean', () => {
expect(repositoryNameCheck(true)).toBe(false);
});
});

View file

@ -0,0 +1,9 @@
// BorgWarehouse repository name is an 8-character hexadecimal string
export default function repositoryNameCheck(name: unknown): boolean {
if (typeof name !== 'string') {
return false;
}
const repositoryNameRegex = /^[a-f0-9]{8}$/;
return repositoryNameRegex.test(name) ? true : false;
}

View file

@ -1,12 +0,0 @@
// This function is used to parse the date and time into a human readable format from the timestamp.
export default function timestampConverter(UNIX_timestamp) {
const a = new Date(UNIX_timestamp * 1000);
const year = a.getFullYear();
const month = a.getMonth() + 1;
const date = a.getDate();
const hour = a.getHours();
const min = (a.getMinutes() < 10 ? '0' : '') + a.getMinutes();
//const sec = a.getSeconds();
const time = year + '/' + month + '/' + date + ' ' + hour + ':' + min;
return time;
}

View file

@ -1,34 +0,0 @@
import { promises as fs } from 'fs';
import path from 'path';
export default async function tokenController(API_KEY, FROM_IP) {
const jsonDirectory = path.join(process.cwd(), 'config');
const timestamp = new Date().toISOString();
try {
if (process.env.DISABLE_INTEGRATIONS === 'true') {
console.log(`API auth failed from : ${FROM_IP} [${timestamp}]`);
return null;
}
const usersList = await fs.readFile(jsonDirectory + '/users.json', 'utf8');
const users = JSON.parse(usersList);
const user = users.find(
(user) => Array.isArray(user.tokens) && user.tokens.some((token) => token.token === API_KEY)
);
if (user) {
const token = user.tokens.find((token) => token.token === API_KEY);
if (token && token.permissions && typeof token.permissions === 'object') {
console.log(
`API auth success with the token '${token.name}' of user '${user.username}' from : ${FROM_IP} [${timestamp}]`
);
return token.permissions;
}
}
console.log(`API auth failed from : ${FROM_IP} [${timestamp}]`);
return null;
} catch (error) {
throw new Error('Error with tokenController');
}
}

View file

@ -32,7 +32,7 @@ authorized_keys="${home}/.ssh/authorized_keys"
# Check args
if [ "$1" == "" ] || [ "$2" == "" ] || ! [[ "$2" =~ ^[0-9]+$ ]] || [ "$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]"
echo -n "This shell takes 3 arguments : SSH Public Key, Quota in Go [e.g. : 10], Append only mode [true|false]" >&2
exit 1
fi
@ -41,25 +41,25 @@ fi
pattern='(ssh-ed25519 AAAAC3NzaC1lZDI1NTE5|sk-ssh-ed25519@openssh.com AAAAGnNrLXNzaC1lZDI1NTE5QG9wZW5zc2guY29t|ssh-rsa AAAAB3NzaC1yc2)[0-9A-Za-z+/]+[=]{0,3}(\s.*)?'
if [[ ! "$1" =~ $pattern ]]
then
echo -n "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)" >&2
exit 2
fi
## Check if authorized_keys exists
if [ ! -f "${authorized_keys}" ];then
echo -n "${authorized_keys} must be present"
echo -n "${authorized_keys} must be present" >&2
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"
echo -n "SSH pub key already present in authorized_keys" >&2
exit 3
fi
# Check if borgbackup is installed
if ! [ -x "$(command -v borg)" ]; then
echo -n "You must install borgbackup package."
echo -n "You must install borgbackup package." >&2
exit 4
fi
@ -77,7 +77,7 @@ else
fi
## 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"
restricted_authkeys="command=\"cd ${pool};borg serve${appendOnlyMode} --restrict-to-repository ${pool}/${repositoryName} --storage-quota $2G\",restrict $1"
echo "$restricted_authkeys" | tee -a "${authorized_keys}" >/dev/null
## Return the repositoryName

View file

@ -1,5 +1,7 @@
#!/usr/bin/env bash
### DEPRECATED ### NodeJS will handle this in the future.
# Shell created by Raven for BorgWarehouse.
# 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.
@ -21,7 +23,7 @@ authorized_keys="${home}/.ssh/authorized_keys"
# Check arg
if [[ $# -ne 1 || $1 = "" ]]; then
echo -n "You must provide a repositoryName in argument."
echo -n "You must provide a repositoryName in argument." >&2
exit 1
fi
@ -29,7 +31,7 @@ fi
# If we receive another pattern there is necessarily a problem.
repositoryName=$1
if ! [[ "$repositoryName" =~ ^[a-f0-9]{8}$ ]]; then
echo "Invalid repository name. Must be an 8-character hex string."
echo "Invalid repository name. Must be an 8-character hex string." >&2
exit 2
fi

View file

@ -1,5 +1,7 @@
#!/usr/bin/env bash
### DEPRECATED ### NodeJS will handle this in the future.
# 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 :

View file

@ -1,5 +1,7 @@
#!/usr/bin/env bash
### DEPRECATED ### NodeJS will handle this in the future.
# Shell created by Raven for BorgWarehouse.
# Get the size of all repositories in a JSON output.
# stdout will be an array like :
@ -27,7 +29,7 @@ fi
# Get the size of each repository and format as JSON
cd "${home}"/repos
output=$(du -s -- * 2>/dev/null | awk '{print "{\"size\":" $1 ",\"name\":\"" $2 "\"}"}' | jq -s '.')
output=$(du -s -L -- * 2>/dev/null | awk '{print "{\"size\":" $1 ",\"name\":\"" $2 "\"}"}' | jq -s '.')
if [ -z "$output" ]; then
output="[]"
fi

View file

@ -1,5 +1,7 @@
#!/usr/bin/env bash
### DEPRECATED ### NodeJS will handle this in the future.
# Shell created by Raven for BorgWarehouse.
# 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.
@ -17,7 +19,7 @@ fi
# 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]]"
echo -n "This shell takes 4 args: [repositoryName] [new SSH pub key] [quota] [Append only mode [true|false]]" >&2
exit 1
fi
@ -26,7 +28,7 @@ fi
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 -n "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)" >&2
exit 2
fi
@ -34,13 +36,13 @@ fi
# If we receive another pattern there is necessarily a problem.
repositoryName=$1
if ! [[ "$repositoryName" =~ ^[a-f0-9]{8}$ ]]; then
echo "Invalid repository name. Must be an 8-character hex string."
echo "Invalid repository name. Must be an 8-character hex string." >&2
exit 3
fi
# 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"
echo -n "No line containing $repositoryName found in authorized_keys" >&2
exit 4
fi
@ -64,7 +66,7 @@ while IFS= read -r line; do
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."
echo -n "This SSH pub key is already present in authorized_keys on a different line." >&2
exit 5
fi

View file

@ -1,5 +1,7 @@
export default function emailTest(mailTo, username, aliasList) {
const aliasTemplate = (x) => {
import path from 'path';
export default function emailTest(mailTo: string, username: string, aliasList: string[]) {
const aliasTemplate = (x: string[]) => {
let str = '';
for (const alias of x) {
str = str + '<li>' + alias + '</li>';
@ -126,7 +128,7 @@ export default function emailTest(mailTo, username, aliasList) {
`,
attachments: [
{
path: 'helpers/templates/attachments/alert-icon.png',
path: path.join(process.cwd(), 'helpers/templates/attachments/alert-icon.png'),
cid: 'alert-icon',
},
],

View file

@ -1,4 +1,6 @@
export default function emailTest(mailTo, username) {
import path from 'path';
export default function emailTest(mailTo: string, username: string) {
const template = {
from: 'BorgWarehouse' + '<' + process.env.MAIL_SMTP_FROM + '>',
to: mailTo,
@ -94,7 +96,7 @@ export default function emailTest(mailTo, username) {
`,
attachments: [
{
path: 'helpers/templates/attachments/valid-icon.png',
path: path.join(process.cwd(), 'helpers/templates/attachments/valid-icon.png'),
cid: 'valid-icon',
},
],

1
hooks/index.ts Normal file
View file

@ -0,0 +1 @@
export * from './useFormStatus';

32
hooks/useFormStatus.ts Normal file
View file

@ -0,0 +1,32 @@
import { useState } from 'react';
import { Optional } from '~/types';
export function useFormStatus() {
const [isLoading, setIsLoading] = useState(false);
const [isSaved, setIsSaved] = useState(false);
const [error, setError] = useState<Optional<string>>(undefined);
const handleSuccess = () => {
setIsLoading(false);
setIsSaved(true);
setTimeout(() => setIsSaved(false), 3000);
};
const handleError = (message: string) => {
setIsLoading(false);
setError(message);
setTimeout(() => setError(undefined), 4000);
};
const clearError = () => setError(undefined);
return {
isLoading,
isSaved,
error,
setIsLoading,
handleSuccess,
handleError,
clearError,
};
}

BIN
medias/borgwarehouse-og.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 246 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 557 KiB

6
next-env.d.ts vendored Normal file
View file

@ -0,0 +1,6 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />
import "./.next/dev/types/routes.d.ts";
// NOTE: This file should not be edited
// see https://nextjs.org/docs/pages/api-reference/config/typescript for more information.

View file

@ -1,7 +1,6 @@
/** @type {import('next').NextConfig} */
import type { NextConfig } from 'next';
module.exports = {
// nextConfig
const nextConfig: NextConfig = {
images: {
unoptimized: true,
},
@ -21,3 +20,5 @@ module.exports = {
];
},
};
export default nextConfig;

6660
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -1,38 +1,52 @@
{
"name": "borgwarehouse",
"version": "2.4.2",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint",
"setup": "npm install && npm run setup:hooks",
"setup:hooks": "npx husky install",
"format": "prettier --write \"{Components,Containers,helpers,pages,styles}/**/*.{js,jsx,ts,tsx,json,css,scss,md}\""
},
"dependencies": {
"@tabler/icons-react": "^3.24.0",
"bcryptjs": "^2.4.3",
"chart.js": "^4.4.7",
"next": "^15.0.4",
"next-auth": "^4.24.10",
"nodemailer": "^6.9.16",
"react": "^18.3.1",
"react-chartjs-2": "^5.2.0",
"react-dom": "^18.3.1",
"react-hook-form": "^7.53.2",
"react-select": "^5.8.3",
"react-toastify": "^10.0.6",
"spinners-react": "^1.0.10",
"swr": "^2.2.5",
"uuid": "^11.0.3"
},
"devDependencies": {
"@commitlint/cli": "^19.6.0",
"@commitlint/config-conventional": "^19.6.0",
"eslint-config-next": "^15.0.4",
"husky": "^9.1.7",
"prettier": "^3.4.2"
}
"name": "borgwarehouse",
"version": "3.1.2",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "pnpm exec eslint",
"test": "vitest",
"setup": "pnpm install && pnpm run setup:hooks",
"setup:hooks": "pnpm exec husky install",
"format": "prettier --write \"{Components,Containers,helpers,pages,styles}/**/*.{js,jsx,ts,tsx,json,css,scss,md}\""
},
"dependencies": {
"@tabler/icons-react": "^3.37.1",
"async-mutex": "^0.5.0",
"bcryptjs": "^3.0.3",
"chart.js": "^4.5.1",
"date-fns": "^4.1.0",
"lowdb": "^7.0.1",
"next": "^16.1.6",
"next-auth": "^4.24.13",
"nodemailer": "^8.0.1",
"nprogress": "^0.2.0",
"react": "^19.2.4",
"react-chartjs-2": "^5.3.1",
"react-dom": "^19.2.4",
"react-hook-form": "^7.71.2",
"react-select": "^5.10.2",
"react-toastify": "^11.0.5",
"swr": "^2.4.1",
"use-media": "^1.5.0",
"uuid": "^13.0.0"
},
"devDependencies": {
"@commitlint/cli": "^20.4.2",
"@commitlint/config-conventional": "^20.4.2",
"@types/node": "^25.3.3",
"@types/nodemailer": "^7.0.11",
"@types/nprogress": "^0.2.3",
"@types/react": "^19.2.14",
"@types/supertest": "^7.2.0",
"eslint": "^9.39.3",
"eslint-config-next": "^16.1.6",
"husky": "^9.1.7",
"node-mocks-http": "^1.17.2",
"prettier": "^3.8.1",
"typescript": "^5.9.3",
"vitest": "^4.0.18"
}
}

View file

@ -1,11 +1,9 @@
//Lib
import Head from 'next/head';
import { useSession } from 'next-auth/react';
import { useRouter } from 'next/router';
import Image from 'next/image';
export default function Error404() {
//Var
const { status } = useSession();
const router = useRouter();

View file

@ -1,25 +0,0 @@
//Lib
import '../styles/default.css';
import Head from 'next/head';
import { ToastContainer } from 'react-toastify';
import 'react-toastify/dist/ReactToastify.css';
import { SessionProvider } from 'next-auth/react';
//Components
import Layout from '../Components/UI/Layout/Layout';
export default function MyApp({ Component, pageProps }) {
return (
<SessionProvider session={pageProps.session}>
<Layout>
<Head>
<meta name='viewport' content='width=device-width, initial-scale=1'></meta>
<link rel='shortcut icon' href='/favicon.ico' />
<title>BorgWarehouse</title>
</Head>
<ToastContainer stacked />
<Component {...pageProps} />
</Layout>
</SessionProvider>
);
}

Some files were not shown because too many files have changed in this diff Show more