Release/3.3.1 (#1438)

* refactor(app-store): improve apps payload to include only necessary fields

* chore: downgrade to pnpm v9

* chore(deps): bump the minor-patch group across 1 directory with 7 updates

Bumps the minor-patch group with 7 updates in the / directory:

| Package | From | To |
| --- | --- | --- |
| [next](https://github.com/vercel/next.js) | `14.2.2` | `14.2.3` |
| [react](https://github.com/facebook/react/tree/HEAD/packages/react) | `18.2.0` | `18.3.1` |
| [@types/react](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/react) | `18.2.79` | `18.3.1` |
| [react-dom](https://github.com/facebook/react/tree/HEAD/packages/react-dom) | `18.2.0` | `18.3.1` |
| [@types/react-dom](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/react-dom) | `18.2.25` | `18.3.0` |
| [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) | `20.12.7` | `20.12.8` |
| [eslint-config-next](https://github.com/vercel/next.js/tree/HEAD/packages/eslint-config-next) | `14.2.2` | `14.2.3` |



Updates `next` from 14.2.2 to 14.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/v14.2.2...v14.2.3)

Updates `react` from 18.2.0 to 18.3.1
- [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/v18.3.1/packages/react)

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

Updates `react-dom` from 18.2.0 to 18.3.1
- [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/v18.3.1/packages/react-dom)

Updates `@types/react-dom` from 18.2.25 to 18.3.0
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/react-dom)

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

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

Updates `@types/react-dom` from 18.2.25 to 18.3.0
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/react-dom)

Updates `eslint-config-next` from 14.2.2 to 14.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/commits/v14.2.3/packages/eslint-config-next)

---
updated-dependencies:
- dependency-name: next
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: minor-patch
- dependency-name: react
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: minor-patch
- dependency-name: "@types/react"
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: minor-patch
- dependency-name: react-dom
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: minor-patch
- dependency-name: "@types/react-dom"
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: minor-patch
- dependency-name: "@types/node"
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: minor-patch
- dependency-name: "@types/react"
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: minor-patch
- dependency-name: "@types/react-dom"
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: minor-patch
- dependency-name: eslint-config-next
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: minor-patch
...

Signed-off-by: dependabot[bot] <support@github.com>

* feat: add tipi minimum version requirement when installing and updating

* feat(dashboard): add warning if internal ip is public

fix(dashboard): fix eslint error

* New Crowdin updates (#1387)

* New translations en.json (French)

* New translations en.json (Turkish)

* New translations en.json (Romanian)

* New translations en.json (Spanish)

* New translations en.json (Afrikaans)

* New translations en.json (Arabic)

* New translations en.json (Catalan)

* New translations en.json (Czech)

* New translations en.json (Danish)

* New translations en.json (German)

* New translations en.json (Greek)

* New translations en.json (Finnish)

* New translations en.json (Hebrew)

* New translations en.json (Hungarian)

* New translations en.json (Italian)

* New translations en.json (Japanese)

* New translations en.json (Korean)

* New translations en.json (Dutch)

* New translations en.json (Norwegian)

* New translations en.json (Polish)

* New translations en.json (Portuguese)

* New translations en.json (Russian)

* New translations en.json (Serbian (Cyrillic))

* New translations en.json (Swedish)

* New translations en.json (Ukrainian)

* New translations en.json (Chinese Simplified)

* New translations en.json (Chinese Traditional)

* New translations en.json (English)

* New translations en.json (Vietnamese)

* New translations en.json (Portuguese, Brazilian)

* New translations en.json (Russian)

* New translations en.json (Vietnamese)

* New translations en.json (Portuguese, Brazilian)

* New translations en.json (Portuguese, Brazilian)

* New translations en.json (French)

* New translations en.json (Turkish)

* New translations en.json (Romanian)

* New translations en.json (Spanish)

* New translations en.json (Afrikaans)

* New translations en.json (Arabic)

* New translations en.json (Catalan)

* New translations en.json (Czech)

* New translations en.json (Danish)

* New translations en.json (German)

* New translations en.json (Greek)

* New translations en.json (Finnish)

* New translations en.json (Hebrew)

* New translations en.json (Hungarian)

* New translations en.json (Italian)

* New translations en.json (Japanese)

* New translations en.json (Korean)

* New translations en.json (Dutch)

* New translations en.json (Norwegian)

* New translations en.json (Polish)

* New translations en.json (Portuguese)

* New translations en.json (Russian)

* New translations en.json (Serbian (Cyrillic))

* New translations en.json (Swedish)

* New translations en.json (Ukrainian)

* New translations en.json (Chinese Simplified)

* New translations en.json (Chinese Traditional)

* New translations en.json (English)

* New translations en.json (Vietnamese)

* New translations en.json (Portuguese, Brazilian)

* New translations en.json (Russian)

* New translations en.json (Portuguese, Brazilian)

* New translations en.json (Portuguese, Brazilian)

* New translations en.json (French)

* New translations en.json (Turkish)

* New translations en.json (Romanian)

* New translations en.json (Spanish)

* New translations en.json (Afrikaans)

* New translations en.json (Arabic)

* New translations en.json (Catalan)

* New translations en.json (Czech)

* New translations en.json (Danish)

* New translations en.json (German)

* New translations en.json (Greek)

* New translations en.json (Finnish)

* New translations en.json (Hebrew)

* New translations en.json (Hungarian)

* New translations en.json (Italian)

* New translations en.json (Japanese)

* New translations en.json (Korean)

* New translations en.json (Dutch)

* New translations en.json (Norwegian)

* New translations en.json (Polish)

* New translations en.json (Portuguese)

* New translations en.json (Russian)

* New translations en.json (Serbian (Cyrillic))

* New translations en.json (Swedish)

* New translations en.json (Ukrainian)

* New translations en.json (Chinese Simplified)

* New translations en.json (Chinese Traditional)

* New translations en.json (English)

* New translations en.json (Vietnamese)

* New translations en.json (Portuguese, Brazilian)

* chore(deps-dev): bump @testing-library/react from 14.3.1 to 15.0.6

Bumps [@testing-library/react](https://github.com/testing-library/react-testing-library) from 14.3.1 to 15.0.6.
- [Release notes](https://github.com/testing-library/react-testing-library/releases)
- [Changelog](https://github.com/testing-library/react-testing-library/blob/main/CHANGELOG.md)
- [Commits](https://github.com/testing-library/react-testing-library/compare/v14.3.1...v15.0.6)

---
updated-dependencies:
- dependency-name: "@testing-library/react"
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>

* chore(deps-dev): bump @testing-library/dom from 9.3.4 to 10.1.0

Bumps [@testing-library/dom](https://github.com/testing-library/dom-testing-library) from 9.3.4 to 10.1.0.
- [Release notes](https://github.com/testing-library/dom-testing-library/releases)
- [Changelog](https://github.com/testing-library/dom-testing-library/blob/main/CHANGELOG.md)
- [Commits](https://github.com/testing-library/dom-testing-library/compare/v9.3.4...v10.1.0)

---
updated-dependencies:
- dependency-name: "@testing-library/dom"
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>

* New Crowdin updates (#1396)

* New translations en.json (French)

* New translations en.json (Turkish)

* New translations en.json (Romanian)

* New translations en.json (Spanish)

* New translations en.json (Afrikaans)

* New translations en.json (Arabic)

* New translations en.json (Catalan)

* New translations en.json (Czech)

* New translations en.json (Danish)

* New translations en.json (German)

* New translations en.json (Greek)

* New translations en.json (Finnish)

* New translations en.json (Hebrew)

* New translations en.json (Hungarian)

* New translations en.json (Italian)

* New translations en.json (Japanese)

* New translations en.json (Korean)

* New translations en.json (Dutch)

* New translations en.json (Norwegian)

* New translations en.json (Polish)

* New translations en.json (Portuguese)

* New translations en.json (Russian)

* New translations en.json (Serbian (Cyrillic))

* New translations en.json (Swedish)

* New translations en.json (Ukrainian)

* New translations en.json (Chinese Simplified)

* New translations en.json (Chinese Traditional)

* New translations en.json (English)

* New translations en.json (Vietnamese)

* New translations en.json (Portuguese, Brazilian)

* New translations en.json (Russian)

* chore(deps): bump the minor-patch group across 1 directory with 24 updates (#1403)

Bumps the minor-patch group with 24 updates in the / directory:

| Package | From | To |
| --- | --- | --- |
| [@sentry/integrations](https://github.com/getsentry/sentry-javascript) | `7.113.0` | `7.114.0` |
| [@sentry/nextjs](https://github.com/getsentry/sentry-javascript) | `7.113.0` | `7.114.0` |
| [next-intl](https://github.com/amannn/next-intl) | `3.12.1` | `3.13.0` |
| [react-hook-form](https://github.com/react-hook-form/react-hook-form) | `7.51.3` | `7.51.4` |
| [sass](https://github.com/sass/dart-sass) | `1.76.0` | `1.77.0` |
| [semver](https://github.com/npm/node-semver) | `7.6.0` | `7.6.2` |
| [validator](https://github.com/validatorjs/validator.js) | `13.11.0` | `13.12.0` |
| [zod](https://github.com/colinhacks/zod) | `3.23.6` | `3.23.8` |
| [@playwright/test](https://github.com/microsoft/playwright) | `1.43.1` | `1.44.0` |
| [@testing-library/jest-dom](https://github.com/testing-library/jest-dom) | `6.4.3` | `6.4.5` |
| [@testing-library/react](https://github.com/testing-library/react-testing-library) | `15.0.6` | `15.0.7` |
| [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) | `20.12.8` | `20.12.11` |
| [@types/pg](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/pg) | `8.11.5` | `8.11.6` |
| [@vitest/coverage-v8](https://github.com/vitest-dev/vitest/tree/HEAD/packages/coverage-v8) | `1.5.3` | `1.6.0` |
| [dotenv-cli](https://github.com/entropitor/dotenv-cli) | `7.4.1` | `7.4.2` |
| [eslint-plugin-jest](https://github.com/jest-community/eslint-plugin-jest) | `28.4.0` | `28.5.0` |
| [knip](https://github.com/webpro/knip/tree/HEAD/packages/knip) | `5.12.0` | `5.13.0` |
| [msw](https://github.com/mswjs/msw) | `2.2.14` | `2.3.0` |
| [tsx](https://github.com/privatenumber/tsx) | `4.8.2` | `4.9.3` |
| [vitest](https://github.com/vitest-dev/vitest/tree/HEAD/packages/vitest) | `1.5.3` | `1.6.0` |
| [@sentry/types](https://github.com/getsentry/sentry-javascript) | `7.113.0` | `7.114.0` |
| [@sentry/node](https://github.com/getsentry/sentry-javascript) | `7.113.0` | `7.114.0` |
| [hono](https://github.com/honojs/hono) | `4.2.9` | `4.3.4` |
| [systeminformation](https://github.com/sebhildebrandt/systeminformation) | `5.22.7` | `5.22.8` |



Updates `@sentry/integrations` from 7.113.0 to 7.114.0
- [Release notes](https://github.com/getsentry/sentry-javascript/releases)
- [Changelog](https://github.com/getsentry/sentry-javascript/blob/7.114.0/CHANGELOG.md)
- [Commits](https://github.com/getsentry/sentry-javascript/compare/7.113.0...7.114.0)

Updates `@sentry/nextjs` from 7.113.0 to 7.114.0
- [Release notes](https://github.com/getsentry/sentry-javascript/releases)
- [Changelog](https://github.com/getsentry/sentry-javascript/blob/7.114.0/CHANGELOG.md)
- [Commits](https://github.com/getsentry/sentry-javascript/compare/7.113.0...7.114.0)

Updates `next-intl` from 3.12.1 to 3.13.0
- [Changelog](https://github.com/amannn/next-intl/blob/main/CHANGELOG.md)
- [Commits](https://github.com/amannn/next-intl/compare/v3.12.1...v3.13.0)

Updates `react-hook-form` from 7.51.3 to 7.51.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.51.3...v7.51.4)

Updates `sass` from 1.76.0 to 1.77.0
- [Release notes](https://github.com/sass/dart-sass/releases)
- [Changelog](https://github.com/sass/dart-sass/blob/main/CHANGELOG.md)
- [Commits](https://github.com/sass/dart-sass/compare/1.76.0...1.77.0)

Updates `semver` from 7.6.0 to 7.6.2
- [Release notes](https://github.com/npm/node-semver/releases)
- [Changelog](https://github.com/npm/node-semver/blob/main/CHANGELOG.md)
- [Commits](https://github.com/npm/node-semver/compare/v7.6.0...v7.6.2)

Updates `validator` from 13.11.0 to 13.12.0
- [Release notes](https://github.com/validatorjs/validator.js/releases)
- [Changelog](https://github.com/validatorjs/validator.js/blob/master/CHANGELOG.md)
- [Commits](https://github.com/validatorjs/validator.js/compare/13.11.0...13.12.0)

Updates `zod` from 3.23.6 to 3.23.8
- [Release notes](https://github.com/colinhacks/zod/releases)
- [Changelog](https://github.com/colinhacks/zod/blob/master/CHANGELOG.md)
- [Commits](https://github.com/colinhacks/zod/compare/v3.23.6...v3.23.8)

Updates `@playwright/test` from 1.43.1 to 1.44.0
- [Release notes](https://github.com/microsoft/playwright/releases)
- [Commits](https://github.com/microsoft/playwright/compare/v1.43.1...v1.44.0)

Updates `@testing-library/jest-dom` from 6.4.3 to 6.4.5
- [Release notes](https://github.com/testing-library/jest-dom/releases)
- [Changelog](https://github.com/testing-library/jest-dom/blob/main/CHANGELOG.md)
- [Commits](https://github.com/testing-library/jest-dom/compare/v6.4.3...v6.4.5)

Updates `@testing-library/react` from 15.0.6 to 15.0.7
- [Release notes](https://github.com/testing-library/react-testing-library/releases)
- [Changelog](https://github.com/testing-library/react-testing-library/blob/main/CHANGELOG.md)
- [Commits](https://github.com/testing-library/react-testing-library/compare/v15.0.6...v15.0.7)

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

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

Updates `@vitest/coverage-v8` from 1.5.3 to 1.6.0
- [Release notes](https://github.com/vitest-dev/vitest/releases)
- [Commits](https://github.com/vitest-dev/vitest/commits/v1.6.0/packages/coverage-v8)

Updates `dotenv-cli` from 7.4.1 to 7.4.2
- [Release notes](https://github.com/entropitor/dotenv-cli/releases)
- [Commits](https://github.com/entropitor/dotenv-cli/compare/v7.4.1...v7.4.2)

Updates `eslint-plugin-jest` from 28.4.0 to 28.5.0
- [Release notes](https://github.com/jest-community/eslint-plugin-jest/releases)
- [Changelog](https://github.com/jest-community/eslint-plugin-jest/blob/main/CHANGELOG.md)
- [Commits](https://github.com/jest-community/eslint-plugin-jest/compare/v28.4.0...v28.5.0)

Updates `knip` from 5.12.0 to 5.13.0
- [Release notes](https://github.com/webpro/knip/releases)
- [Changelog](https://github.com/webpro/knip/blob/main/packages/knip/.release-it.json)
- [Commits](https://github.com/webpro/knip/commits/5.13.0/packages/knip)

Updates `msw` from 2.2.14 to 2.3.0
- [Release notes](https://github.com/mswjs/msw/releases)
- [Changelog](https://github.com/mswjs/msw/blob/main/CHANGELOG.md)
- [Commits](https://github.com/mswjs/msw/compare/v2.2.14...v2.3.0)

Updates `tsx` from 4.8.2 to 4.9.3
- [Release notes](https://github.com/privatenumber/tsx/releases)
- [Changelog](https://github.com/privatenumber/tsx/blob/master/release.config.cjs)
- [Commits](https://github.com/privatenumber/tsx/compare/v4.8.2...v4.9.3)

Updates `vitest` from 1.5.3 to 1.6.0
- [Release notes](https://github.com/vitest-dev/vitest/releases)
- [Commits](https://github.com/vitest-dev/vitest/commits/v1.6.0/packages/vitest)

Updates `@sentry/types` from 7.113.0 to 7.114.0
- [Release notes](https://github.com/getsentry/sentry-javascript/releases)
- [Changelog](https://github.com/getsentry/sentry-javascript/blob/7.114.0/CHANGELOG.md)
- [Commits](https://github.com/getsentry/sentry-javascript/compare/7.113.0...7.114.0)

Updates `@sentry/node` from 7.113.0 to 7.114.0
- [Release notes](https://github.com/getsentry/sentry-javascript/releases)
- [Changelog](https://github.com/getsentry/sentry-javascript/blob/7.114.0/CHANGELOG.md)
- [Commits](https://github.com/getsentry/sentry-javascript/compare/7.113.0...7.114.0)

Updates `hono` from 4.2.9 to 4.3.4
- [Release notes](https://github.com/honojs/hono/releases)
- [Commits](https://github.com/honojs/hono/compare/v4.2.9...v4.3.4)

Updates `systeminformation` from 5.22.7 to 5.22.8
- [Changelog](https://github.com/sebhildebrandt/systeminformation/blob/master/CHANGELOG.md)
- [Commits](https://github.com/sebhildebrandt/systeminformation/compare/v5.22.7...v5.22.8)

---
updated-dependencies:
- dependency-name: "@sentry/integrations"
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: minor-patch
- dependency-name: "@sentry/nextjs"
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: minor-patch
- dependency-name: next-intl
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: minor-patch
- dependency-name: react-hook-form
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: minor-patch
- dependency-name: sass
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: minor-patch
- dependency-name: semver
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: minor-patch
- dependency-name: validator
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: minor-patch
- dependency-name: zod
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: minor-patch
- dependency-name: "@playwright/test"
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: minor-patch
- dependency-name: "@testing-library/jest-dom"
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: minor-patch
- dependency-name: "@testing-library/react"
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: minor-patch
- dependency-name: "@types/node"
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: minor-patch
- dependency-name: "@types/pg"
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: minor-patch
- dependency-name: "@vitest/coverage-v8"
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: minor-patch
- dependency-name: dotenv-cli
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: minor-patch
- dependency-name: eslint-plugin-jest
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: minor-patch
- dependency-name: knip
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: minor-patch
- dependency-name: msw
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: minor-patch
- dependency-name: tsx
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: minor-patch
- dependency-name: vitest
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: minor-patch
- dependency-name: "@sentry/types"
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: minor-patch
- dependency-name: "@sentry/node"
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: minor-patch
- dependency-name: hono
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: minor-patch
- dependency-name: systeminformation
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: minor-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* Add password reset request expiry check and automatic session invalidation

* chore(deps): bump the minor-patch group across 1 directory with 7 updates (#1405)

* Update auth.service.ts

Move request expiry time to a constant so it can be changed easier

* chore: bump sentry dependencies

* chore(deps): bump the minor-patch group across 1 directory with 7 updates (#1405)

* chore: bump sentry dependencies

* chore(auth.service): small async improvements

* fix: missing sentry/next in worker

* New Crowdin updates (#1400)

* New translations en.json (Spanish)

* New translations en.json (Spanish)

* New translations en.json (French)

* chore(deps): bump pnpm/action-setup from 3.0.0 to 4.0.0

Bumps [pnpm/action-setup](https://github.com/pnpm/action-setup) from 3.0.0 to 4.0.0.
- [Release notes](https://github.com/pnpm/action-setup/releases)
- [Commits](https://github.com/pnpm/action-setup/compare/v3.0.0...v4.0.0)

---
updated-dependencies:
- dependency-name: pnpm/action-setup
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>

* fix(app-actions): missing open buttons with legacy app setup

* test: enable app folder in tests

* test: migrate to vitest

* feat/section-app-install-form (#1414)

* done

* fix: wrong conditions

---------

Co-authored-by: Nicolas Meienberger <github@thisprops.com>

* refactor(docker-compose.json): fallback to classic docker compose in case of failing to parse file

* chore(app.executors): add more logging

* chore(watcher): pull repo only in non-dev mode

* chore: cleanup eslint config and add no-floating-promise rule

* chore: cleanup eslint configs in packages

* chore(deps): bump the minor-patch group across 1 directory with 13 updates

Bumps the minor-patch group with 3 updates in the / directory: [@hookform/resolvers](https://github.com/react-hook-form/resolvers), [sharp](https://github.com/lovell/sharp) and [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node).


Updates `@hookform/resolvers` from 3.4.0 to 3.4.2
- [Release notes](https://github.com/react-hook-form/resolvers/releases)
- [Commits](https://github.com/react-hook-form/resolvers/compare/v3.4.0...v3.4.2)

Updates `@sentry/nextjs` from 8.2.1 to 8.3.0
- [Release notes](https://github.com/getsentry/sentry-javascript/releases)
- [Changelog](https://github.com/getsentry/sentry-javascript/blob/develop/CHANGELOG.md)
- [Commits](https://github.com/getsentry/sentry-javascript/compare/8.2.1...8.3.0)

Updates `@tabler/icons-react` from 3.4.0 to 3.5.0
- [Release notes](https://github.com/tabler/tabler-icons/releases)
- [Commits](https://github.com/tabler/tabler-icons/commits/v3.5.0/packages/icons-react)

Updates `bullmq` from 5.7.9 to 5.7.11
- [Release notes](https://github.com/taskforcesh/bullmq/releases)
- [Commits](https://github.com/taskforcesh/bullmq/compare/v5.7.9...v5.7.11)

Updates `react-hook-form` from 7.51.4 to 7.51.5
- [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.51.4...v7.51.5)

Updates `sharp` from 0.33.3 to 0.33.4
- [Release notes](https://github.com/lovell/sharp/releases)
- [Changelog](https://github.com/lovell/sharp/blob/main/docs/changelog.md)
- [Commits](https://github.com/lovell/sharp/compare/v0.33.3...v0.33.4)

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

Updates `@vitejs/plugin-react` from 4.2.1 to 4.3.0
- [Release notes](https://github.com/vitejs/vite-plugin-react/releases)
- [Changelog](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite-plugin-react/commits/v4.3.0/packages/plugin-react)

Updates `ts-jest` from 29.1.2 to 29.1.3
- [Release notes](https://github.com/kulshekhar/ts-jest/releases)
- [Changelog](https://github.com/kulshekhar/ts-jest/blob/main/CHANGELOG.md)
- [Commits](https://github.com/kulshekhar/ts-jest/compare/v29.1.2...v29.1.3)

Updates `tsx` from 4.10.4 to 4.11.0
- [Release notes](https://github.com/privatenumber/tsx/releases)
- [Changelog](https://github.com/privatenumber/tsx/blob/master/release.config.cjs)
- [Commits](https://github.com/privatenumber/tsx/compare/v4.10.4...v4.11.0)

Updates `@sentry/types` from 8.2.1 to 8.3.0
- [Release notes](https://github.com/getsentry/sentry-javascript/releases)
- [Changelog](https://github.com/getsentry/sentry-javascript/blob/develop/CHANGELOG.md)
- [Commits](https://github.com/getsentry/sentry-javascript/compare/8.2.1...8.3.0)

Updates `@sentry/node` from 8.2.1 to 8.3.0
- [Release notes](https://github.com/getsentry/sentry-javascript/releases)
- [Changelog](https://github.com/getsentry/sentry-javascript/blob/develop/CHANGELOG.md)
- [Commits](https://github.com/getsentry/sentry-javascript/compare/8.2.1...8.3.0)

Updates `hono` from 4.3.7 to 4.3.9
- [Release notes](https://github.com/honojs/hono/releases)
- [Commits](https://github.com/honojs/hono/compare/v4.3.7...v4.3.9)

---
updated-dependencies:
- dependency-name: "@hookform/resolvers"
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: minor-patch
- dependency-name: "@sentry/nextjs"
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: minor-patch
- dependency-name: "@tabler/icons-react"
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: minor-patch
- dependency-name: bullmq
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: minor-patch
- dependency-name: react-hook-form
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: minor-patch
- dependency-name: sharp
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: minor-patch
- dependency-name: "@types/node"
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: minor-patch
- dependency-name: "@vitejs/plugin-react"
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: minor-patch
- dependency-name: ts-jest
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: minor-patch
- dependency-name: tsx
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: minor-patch
- dependency-name: "@sentry/types"
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: minor-patch
- dependency-name: "@sentry/node"
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: minor-patch
- dependency-name: hono
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: minor-patch
...

Signed-off-by: dependabot[bot] <support@github.com>

* Update regex in install script (#1426)

* docs: update README.md [skip ci]

* docs: update .all-contributorsrc [skip ci]

* chore(deps): bump the minor-patch group across 1 directory with 10 updates (#1434)

Bumps the minor-patch group with 10 updates in the / directory:

| Package | From | To |
| --- | --- | --- |
| [@sentry/nextjs](https://github.com/getsentry/sentry-javascript) | `8.3.0` | `8.4.0` |
| [bullmq](https://github.com/taskforcesh/bullmq) | `5.7.11` | `5.7.12` |
| [next-intl](https://github.com/amannn/next-intl) | `3.14.0` | `3.14.1` |
| [@babel/core](https://github.com/babel/babel/tree/HEAD/packages/babel-core) | `7.24.5` | `7.24.6` |
| [@playwright/test](https://github.com/microsoft/playwright) | `1.44.0` | `1.44.1` |
| [@types/react](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/react) | `18.3.2` | `18.3.3` |
| [@sentry/types](https://github.com/getsentry/sentry-javascript) | `8.3.0` | `8.4.0` |
| [@sentry/node](https://github.com/getsentry/sentry-javascript) | `8.3.0` | `8.4.0` |
| [hono](https://github.com/honojs/hono) | `4.3.9` | `4.3.11` |
| [@sentry/esbuild-plugin](https://github.com/getsentry/sentry-javascript-bundler-plugins) | `2.16.1` | `2.17.0` |



Updates `@sentry/nextjs` from 8.3.0 to 8.4.0
- [Release notes](https://github.com/getsentry/sentry-javascript/releases)
- [Changelog](https://github.com/getsentry/sentry-javascript/blob/develop/CHANGELOG.md)
- [Commits](https://github.com/getsentry/sentry-javascript/compare/8.3.0...8.4.0)

Updates `bullmq` from 5.7.11 to 5.7.12
- [Release notes](https://github.com/taskforcesh/bullmq/releases)
- [Commits](https://github.com/taskforcesh/bullmq/compare/v5.7.11...v5.7.12)

Updates `next-intl` from 3.14.0 to 3.14.1
- [Changelog](https://github.com/amannn/next-intl/blob/main/CHANGELOG.md)
- [Commits](https://github.com/amannn/next-intl/compare/v3.14.0...v3.14.1)

Updates `@babel/core` from 7.24.5 to 7.24.6
- [Release notes](https://github.com/babel/babel/releases)
- [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md)
- [Commits](https://github.com/babel/babel/commits/v7.24.6/packages/babel-core)

Updates `@playwright/test` from 1.44.0 to 1.44.1
- [Release notes](https://github.com/microsoft/playwright/releases)
- [Commits](https://github.com/microsoft/playwright/compare/v1.44.0...v1.44.1)

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

Updates `@sentry/types` from 8.3.0 to 8.4.0
- [Release notes](https://github.com/getsentry/sentry-javascript/releases)
- [Changelog](https://github.com/getsentry/sentry-javascript/blob/develop/CHANGELOG.md)
- [Commits](https://github.com/getsentry/sentry-javascript/compare/8.3.0...8.4.0)

Updates `@sentry/node` from 8.3.0 to 8.4.0
- [Release notes](https://github.com/getsentry/sentry-javascript/releases)
- [Changelog](https://github.com/getsentry/sentry-javascript/blob/develop/CHANGELOG.md)
- [Commits](https://github.com/getsentry/sentry-javascript/compare/8.3.0...8.4.0)

Updates `hono` from 4.3.9 to 4.3.11
- [Release notes](https://github.com/honojs/hono/releases)
- [Commits](https://github.com/honojs/hono/compare/v4.3.9...v4.3.11)

Updates `@sentry/esbuild-plugin` from 2.16.1 to 2.17.0
- [Release notes](https://github.com/getsentry/sentry-javascript-bundler-plugins/releases)
- [Changelog](https://github.com/getsentry/sentry-javascript-bundler-plugins/blob/main/CHANGELOG.md)
- [Commits](https://github.com/getsentry/sentry-javascript-bundler-plugins/compare/2.16.1...2.17.0)

---
updated-dependencies:
- dependency-name: "@sentry/nextjs"
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: minor-patch
- dependency-name: bullmq
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: minor-patch
- dependency-name: next-intl
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: minor-patch
- dependency-name: "@babel/core"
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: minor-patch
- dependency-name: "@playwright/test"
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: minor-patch
- dependency-name: "@types/react"
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: minor-patch
- dependency-name: "@sentry/types"
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: minor-patch
- dependency-name: "@sentry/node"
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: minor-patch
- dependency-name: hono
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: minor-patch
- dependency-name: "@sentry/esbuild-plugin"
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: minor-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* chore(deps-dev): bump @typescript-eslint/eslint-plugin (#1432)

* ci: local e2e

* ci(e2e): add workflow dispatch

* ci(e2e): replace usages of old e2e workflow (#1436)

* chore: various cleanups and deprecations fixes (#1437)

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Stavros <steveiliop56@gmail.com>
Co-authored-by: hex-developer <77530549+hex-developer@users.noreply.github.com>
Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com>
This commit is contained in:
Nicolas Meienberger 2024-05-26 16:18:57 +02:00 committed by GitHub
parent 0c718ee1fe
commit 06f36881c3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
47 changed files with 1119 additions and 1744 deletions

View File

@ -466,6 +466,15 @@
"code",
"doc"
]
},
{
"login": "hex-developer",
"name": "hex-developer",
"avatar_url": "https://avatars.githubusercontent.com/u/77530549?v=4",
"profile": "https://github.com/hex-developer",
"contributions": [
"code"
]
}
],
"contributorsPerLine": 7,

View File

@ -16,3 +16,4 @@
!src/**
!tests/**
!start.*.sh
!scripts/**

View File

@ -1,18 +1,12 @@
module.exports = {
plugins: ['@typescript-eslint', 'import', 'react', 'jest', 'jsx-a11y', 'testing-library', 'jest-dom', 'jsonc', 'drizzle'],
plugins: ['@typescript-eslint', 'import', 'react', 'jsx-a11y'],
extends: [
'plugin:@typescript-eslint/recommended',
'plugin:react-hooks/recommended',
'next/core-web-vitals',
'next',
'airbnb',
'airbnb-typescript',
'eslint:recommended',
'plugin:import/typescript',
'plugin:react/recommended',
'plugin:jsx-a11y/recommended',
'plugin:jsonc/recommended-with-json',
'plugin:jsonc/prettier',
'plugin:drizzle/recommended',
'prettier',
],
parser: '@typescript-eslint/parser',
@ -23,6 +17,7 @@ module.exports = {
tsconfigRootDir: __dirname,
},
rules: {
'@typescript-eslint/no-floating-promises': 1,
'no-restricted-exports': 0,
'no-redeclare': 0, // already handled by @typescript-eslint/no-redeclare
'react/display-name': 0,
@ -70,10 +65,13 @@ module.exports = {
overrides: [
{
files: ['*.test.ts', '*.test.tsx'],
plugins: ['jest', 'jest-dom', 'testing-library'],
extends: ['plugin:jest-dom/recommended', 'plugin:testing-library/react'],
},
{
files: ['**/*.json', '*.json5', '*.jsonc'],
plugins: ['jsonc'],
extends: ['plugin:jsonc/recommended-with-json', 'plugin:jsonc/prettier'],
parser: 'jsonc-eslint-parser',
rules: {
// Disable all @typescript-eslint rules as they don't apply here
@ -84,7 +82,7 @@ module.exports = {
'@typescript-eslint/return-await': 'off',
// jsonc rules
'jsonc/sort-keys': 2,
'jsonc/key-name-casing': 0,
'jsonc/key-name-casing': 1,
},
},
],
@ -92,7 +90,4 @@ module.exports = {
JSX: true,
NodeJS: true,
},
env: {
'jest/globals': true,
},
};

View File

@ -1,5 +1,4 @@
name: E2E Tests
on:
workflow_call:
inputs:
@ -7,110 +6,90 @@ on:
required: true
type: string
description: 'Version to test (e.g. v1.6.0-beta.1)'
outputs:
page_url:
description: 'URL of the deployed report'
value: ${{ jobs.report-deployment.outputs.page_url }}
workflow_dispatch:
inputs:
version:
required: true
type: string
description: 'Version to test (e.g. v1.6.0-beta.1)'
jobs:
deploy:
timeout-minutes: 15
build-images:
if: ${{ !inputs.version }}
runs-on: ubuntu-latest
outputs:
droplet_id: ${{ steps.create-droplet.outputs.droplet_id }}
droplet_ip: ${{ steps.get-droplet-ip.outputs.droplet_ip }}
postgres_password: ${{ steps.get-postgres-password.outputs.postgres_password }}
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Install SSH key
uses: shimataro/ssh-key-action@v2
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to GitHub Container Registry
uses: docker/login-action@v3
with:
key: ${{ secrets.SSH_KEY }}
known_hosts: unnecessary
name: id_rsa
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Get sha of last commit
id: get-sha
run: echo "sha=$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT
- name: Install doctl
uses: digitalocean/action-doctl@v2
- name: Build and push images
uses: docker/build-push-action@v5
with:
token: ${{ secrets.DIGITALOCEAN_ACCESS_TOKEN }}
context: .
build-args: |
SENTRY_AUTH_TOKEN=${{ secrets.SENTRY_AUTH_TOKEN }}
TIPI_VERSION=e2e
platforms: linux/amd64
push: true
tags: ghcr.io/runtipi/runtipi:e2e
cache-from: type=registry,ref=ghcr.io/runtipi/runtipi:buildcache
cache-to: type=registry,ref=ghcr.io/runtipi/runtipi:buildcache,mode=max
- name: Create new Droplet
id: create-droplet
- name: Create cli folder
run: mkdir -p bin
- name: Download CLI form release on runtipi/cli repo
run: |
droplet_id=$(doctl compute droplet create runtipi-${{ steps.get-sha.outputs.sha }} \
--image ubuntu-20-04-x64 \
--size s-2vcpu-2gb \
--format ID \
--no-header \
--ssh-keys ${{ secrets.SSH_KEY_FINGERPRINT }})
echo "droplet_id=$droplet_id" >> $GITHUB_OUTPUT
REPO="runtipi/cli"
VERSION="nightly"
- name: Wait for Droplet to become active
run: |
while ! doctl compute droplet get ${{ steps.create-droplet.outputs.droplet_id }} --format Status --no-header | grep -q "active"; do sleep 5; done
ASSETS_URL=$(curl -s \
-H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \
-H "Accept: application/vnd.github.v3+json" \
"https://api.github.com/repos/$REPO/releases/tags/$VERSION" \
| jq -r '.assets[] | select(.name | test("runtipi-cli.+")) | .browser_download_url')
- name: Get Droplet IP address
id: get-droplet-ip
run: |
droplet_ip=$(doctl compute droplet get ${{ steps.create-droplet.outputs.droplet_id }} --format PublicIPv4 --no-header)
echo "droplet_ip=$droplet_ip" >> $GITHUB_OUTPUT
echo "Assets URL: $ASSETS_URL"
- name: Wait for SSH to be ready on Droplet
run: |
while ! ssh -o StrictHostKeyChecking=no -i ~/.ssh/id_rsa root@${{ steps.get-droplet-ip.outputs.droplet_ip }} "echo 'SSH is ready'"; do sleep 5; done
for url in $ASSETS_URL; do
echo "Downloading from $url"
curl -L -o "bin/${url##*/}" -H "Accept: application/octet-stream" -H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" "$url"
done
- name: Create docker group on Droplet
uses: fifsky/ssh-action@master
- name: Print files
run: tree bin
- uses: pyTooling/Actions/releaser@r0
with:
command: |
groupadd docker
usermod -aG docker root
host: ${{ steps.get-droplet-ip.outputs.droplet_ip }}
user: root
key: ${{ secrets.SSH_KEY }}
- name: Wait 90 seconds for Docker to be ready on Droplet
run: sleep 90
- name: Deploy app to Droplet
uses: fifsky/ssh-action@master
with:
command: |
echo 'Downloading install script from GitHub'
curl -s https://raw.githubusercontent.com/runtipi/runtipi/${{ inputs.version }}/scripts/install.sh > install.sh
chmod +x install.sh
echo 'Running install script'
./install.sh --version ${{ inputs.version }} --asset runtipi-cli-linux-x86_64.tar.gz
echo 'App deployed'
host: ${{ steps.get-droplet-ip.outputs.droplet_ip }}
user: root # TODO: use non-root user
key: ${{ secrets.SSH_KEY }}
- name: Get POSTGRES_PASSWORD from .env file
id: get-postgres-password
run: |
postgres_password=$(ssh -o StrictHostKeyChecking=no -i ~/.ssh/id_rsa root@${{ steps.get-droplet-ip.outputs.droplet_ip }} "cat ./runtipi/.env | grep POSTGRES_PASSWORD | cut -d '=' -f2")
echo "postgres_password=$postgres_password" >> $GITHUB_OUTPUT
token: ${{ secrets.GITHUB_TOKEN }}
tag: e2e
rm: true
files: bin/runtipi-cli-*
e2e:
timeout-minutes: 60
runs-on: ubuntu-latest
needs: [deploy]
needs: [build-images]
steps:
- uses: actions/checkout@v4
- name: Checkout
uses: actions/checkout@v4
- name: Run install script
if: ${{ inputs.version }}
run: |
curl -s https://raw.githubusercontent.com/runtipi/runtipi/${{ inputs.version }}/scripts/install.sh > install.sh
chmod +x install.sh
./install.sh --version ${{ inputs.version }} --asset runtipi-cli-linux-x86_64.tar.gz
- name: Run install script
if: ${{ !inputs.version }}
run: |
./scripts/install.sh --version e2e
cd ..
- uses: pnpm/action-setup@v4.0.0
name: Install pnpm
@ -128,34 +107,26 @@ jobs:
name: Setup pnpm cache
with:
path: ${{ steps.pnpm-cache.outputs.pnpm_cache_dir }}
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-pnpm-store-
- name: Create .env.e2e file with Droplet IP
run: |
echo "SERVER_IP=$(hostname -I | awk '{print $1}')" > .env.e2e
echo "POSTGRES_PASSWORD=$(grep POSTGRES_PASSWORD runtipi/.env | cut -d '=' -f2)" >> .env.e2e
echo "BASE_PATH=./runtipi" >> .env.e2e
- name: Install dependencies
run: pnpm install
- uses: actions/setup-node@v4
with:
node-version: 20
- name: Create .env.e2e file with Droplet IP
run: |
echo "SERVER_IP=${{ needs.deploy.outputs.droplet_ip }}" > .env.e2e
echo "POSTGRES_PASSWORD=${{ needs.deploy.outputs.postgres_password }}" >> .env.e2e
echo "REMOTE=true" >> .env.e2e
echo "${{ secrets.SSH_KEY }}" > .temp-ssh-key
# Base 64 encode SSH key to avoid issues with newlines
echo "SSH_PRIVATE_KEY=$(base64 -w 0 .temp-ssh-key)" >> .env.e2e
- name: Install dependencies
run: pnpm install
- name: Install Playwright Browsers
run: npx playwright install --with-deps
- name: Create state/settings.json
run: |
mkdir -p state
echo '{}' > state/settings.json
- name: Run Playwright tests
id: run-e2e
run: npm run test:e2e
@ -197,16 +168,3 @@ jobs:
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v4
teardown:
runs-on: ubuntu-latest
if: always()
needs: [e2e, deploy]
steps:
- name: Install doctl
uses: digitalocean/action-doctl@v2
with:
token: ${{ secrets.DIGITALOCEAN_ACCESS_TOKEN }}
- name: Delete Droplet
run: doctl compute droplet delete ${{ needs.deploy.outputs.droplet_id }} --force

3
.gitignore vendored
View File

@ -70,3 +70,6 @@ temp
# Sentry Config File
.sentryclirc
public/js/*
!public/js/.gitkeep

3
.prettierignore Normal file
View File

@ -0,0 +1,3 @@
packages
coverage
.github

View File

@ -34,12 +34,13 @@ RUN pnpm fetch
COPY ./pnpm-workspace.yaml ./
COPY ./package*.json ./
COPY ./packages/shared ./packages/shared
COPY ./scripts ./scripts
COPY ./public ./public
RUN pnpm install -r --prefer-offline
COPY ./src ./src
COPY ./tsconfig.json ./tsconfig.json
COPY ./next.config.mjs ./next.config.mjs
COPY ./public ./public
COPY ./tests ./tests
# Sentry

View File

@ -32,13 +32,14 @@ RUN pnpm fetch --ignore-scripts
COPY ./package*.json ./
COPY ./packages/worker/package.json ./packages/worker/package.json
COPY ./packages/shared/package.json ./packages/shared/package.json
COPY ./scripts ./scripts
COPY ./public ./public
RUN pnpm install -r --prefer-offline
COPY ./packages ./packages
COPY ./tsconfig.json ./tsconfig.json
COPY ./next.config.mjs ./next.config.mjs
COPY ./public ./public
# Sentry
COPY ./sentry.client.config.ts ./sentry.client.config.ts

View File

@ -1,9 +1,7 @@
# Tipi — A personal homeserver for everyone
<!-- ALL-CONTRIBUTORS-BADGE:START - Do not remove or modify this section -->
[![All Contributors](https://img.shields.io/badge/all_contributors-49-orange.svg?style=flat-square)](#contributors-)
[![All Contributors](https://img.shields.io/badge/all_contributors-50-orange.svg?style=flat-square)](#contributors-)
<!-- ALL-CONTRIBUTORS-BADGE:END -->
[![License](https://img.shields.io/github/license/runtipi/runtipi)](https://github.com/runtipi/runtipi/blob/master/LICENSE)
@ -140,6 +138,9 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d
<td align="center" valign="top" width="14.28%"><a href="https://thibaultclaude.be"><img src="https://avatars.githubusercontent.com/u/23203061?v=4?s=100" width="100px;" alt="Thibault Claude"/><br /><sub><b>Thibault Claude</b></sub></a><br /><a href="https://github.com/runtipi/runtipi/commits?author=thclaude" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/DansNewLegs"><img src="https://avatars.githubusercontent.com/u/152246049?v=4?s=100" width="100px;" alt="Joshua Banks"/><br /><sub><b>Joshua Banks</b></sub></a><br /><a href="https://github.com/runtipi/runtipi/commits?author=DansNewLegs" title="Code">💻</a> <a href="https://github.com/runtipi/runtipi/commits?author=DansNewLegs" title="Documentation">📖</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/hex-developer"><img src="https://avatars.githubusercontent.com/u/77530549?v=4?s=100" width="100px;" alt="hex-developer"/><br /><sub><b>hex-developer</b></sub></a><br /><a href="https://github.com/runtipi/runtipi/commits?author=hex-developer" title="Code">💻</a></td>
</tr>
</tbody>
</table>

View File

@ -14,8 +14,8 @@ test.beforeEach(async ({ page, context, isMobile }) => {
await page.getByRole('link', { name: 'App store' }).click();
}
await page.getByPlaceholder('Search').fill('hello');
await page.getByRole('link', { name: 'Hello World' }).click();
await page.getByPlaceholder('Search').fill('nginx');
await page.getByRole('link', { name: 'Nginx' }).click();
});
test('user can install and uninstall app', async ({ page, context }) => {
@ -23,7 +23,7 @@ test('user can install and uninstall app', async ({ page, context }) => {
// Install app
await page.getByRole('button', { name: 'Install' }).click();
await expect(page.getByText('Install Hello World')).toBeVisible();
await expect(page.getByText('Install Nginx')).toBeVisible();
await page.getByRole('button', { name: 'Install' }).click();
@ -33,16 +33,16 @@ test('user can install and uninstall app', async ({ page, context }) => {
await page.getByTestId('app-details').getByRole('button', { name: 'Open' }).press('ArrowDown');
const [newPage] = await Promise.all([
context.waitForEvent('page'),
await page.getByRole('menuitem', { name: `${process.env.SERVER_IP}:8000` }).click(),
await page.getByRole('menuitem', { name: `${process.env.SERVER_IP}:8754` }).click(),
]);
await newPage.waitForLoadState();
await expect(newPage.getByText('Hello World')).toBeVisible();
await expect(newPage.getByText('Welcome to nginx!')).toBeVisible();
await newPage.close();
// Stop app
await page.getByRole('button', { name: 'Stop' }).click();
await expect(page.getByText('Stop Hello World')).toBeVisible();
await expect(page.getByText('Stop Nginx')).toBeVisible();
await page.getByRole('button', { name: 'Stop' }).click();
@ -50,7 +50,7 @@ test('user can install and uninstall app', async ({ page, context }) => {
// Uninstall app
await page.getByRole('button', { name: 'Remove' }).click();
await expect(page.getByText('Uninstall Hello World ?')).toBeVisible();
await expect(page.getByText('Uninstall Nginx ?')).toBeVisible();
await page.getByRole('button', { name: 'Uninstall' }).click();

View File

@ -10,12 +10,15 @@ export const createTestUser = async () => {
await db.insert(userTable).values({ password, username: testUser.email, operator: true });
};
export const loginUser = async (page: Page, context: BrowserContext) => {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
export const loginUser = async (page: Page, _: BrowserContext) => {
await page.addLocatorHandler(page.getByText('Insecure configuration'), async () => {
await page.getByRole('button', { name: 'Close' }).click();
});
// Create user in database
await createTestUser();
await context.addCookies([{ name: 'hide-insecure-instance', value: 'true', path: '/', domain: process.env.SERVER_IP }]);
// Login flow
await page.goto('/login');

View File

@ -2,3 +2,5 @@ export const testUser = {
email: 'tester@test.com',
password: 'password',
};
export const BASE_PATH = process.env.BASE_PATH || '.';

View File

@ -2,52 +2,34 @@ import { promises } from 'fs';
import { z } from 'zod';
import { settingsSchema } from '@runtipi/shared';
import { pathExists } from '@runtipi/shared/node';
import { execRemoteCommand } from './write-remote-file';
import path from 'path';
import { BASE_PATH } from './constants';
export const setSettings = async (settings: z.infer<typeof settingsSchema>) => {
if (process.env.REMOTE === 'true') {
await execRemoteCommand(`mkdir -p ./runtipi/state`);
await execRemoteCommand(`echo '${JSON.stringify(settings)}' > ./runtipi/state/settings.json`);
} else {
// Create state folder if it doesn't exist
await promises.mkdir('./state', { recursive: true });
await promises.writeFile('./state/settings.json', JSON.stringify(settings));
}
await promises.mkdir(path.join(BASE_PATH, 'state'), { recursive: true });
await promises.writeFile(path.join(BASE_PATH, 'state', 'settings.json'), JSON.stringify(settings));
};
export const setPassowrdChangeRequest = async () => {
if (process.env.REMOTE === 'true') {
// Write date in ms to file
await execRemoteCommand('touch ./runtipi/state/password-change-request && echo $(date +%s) >> ./runtipi/state/password-change-request');
} else {
await promises.writeFile('./state/password-change-request', `${new Date().getTime() / 1000}`);
}
await promises.writeFile(path.join(BASE_PATH, 'state', 'password-change-request'), `${new Date().getTime() / 1000}`);
};
export const unsetPasswordChangeRequest = async () => {
if (process.env.REMOTE === 'true') {
await execRemoteCommand('rm ./runtipi/state/password-change-request');
} else if (await pathExists('./state/password-change-request')) {
await promises.unlink('./state/password-change-request');
const requestPath = path.join(BASE_PATH, 'state', 'password-change-request');
if (await pathExists(requestPath)) {
await promises.unlink(requestPath);
}
};
export const setWelcomeSeen = async (seen: boolean) => {
if (seen && process.env.REMOTE === 'true') {
return execRemoteCommand('touch ./runtipi/state/seen-welcome');
const seenPath = path.join(BASE_PATH, 'state', 'seen-welcome');
if (seen && !(await pathExists(seenPath))) {
return promises.writeFile(seenPath, '');
}
if (!seen && process.env.REMOTE === 'true') {
return execRemoteCommand('rm ./runtipi/state/seen-welcome');
}
if (seen && !(await pathExists('./state/seen-welcome'))) {
return promises.writeFile('./state/seen-welcome', '');
}
if (!seen && (await pathExists('./state/seen-welcome'))) {
return promises.unlink('./state/seen-welcome');
if (!seen && (await pathExists(seenPath))) {
return promises.unlink(seenPath);
}
return Promise.resolve();

View File

@ -1,32 +0,0 @@
import { Client } from 'ssh2';
export const execRemoteCommand = async (command: string): Promise<void> => {
return new Promise((resolve) => {
const conn = new Client();
conn.connect({
host: process.env.SERVER_IP,
port: 22,
username: 'root',
privateKey: atob(process.env.SSH_PRIVATE_KEY as string),
});
conn.on('ready', () => {
conn.exec(command, (err, stream) => {
if (err) throw err;
stream
.on('close', () => {
conn.end();
resolve();
})
.on('data', (data: Buffer) => {
console.log('STDOUT:', data.toString());
})
.stderr.on('data', (data: Buffer) => {
console.log('STDERR:', data.toString());
});
});
});
});
};

View File

@ -1,53 +0,0 @@
import nextJest from 'next/jest';
const createJestConfig = nextJest({
// Provide the path to your Next.js app to load next.config.js and .env files in your test environment
dir: './',
});
const customClientConfig = {
testEnvironment: 'jest-environment-jsdom',
setupFilesAfterEnv: ['<rootDir>/tests/client/jest.setup.tsx'],
testMatch: [
'<rootDir>/src/client/**/*.{spec,test}.{ts,tsx}',
'<rootDir>/src/app/**/*.{spec,test}.{ts,tsx}',
'!<rootDir>/src/server/**/*.{spec,test}.{ts,tsx}',
],
};
const customServerConfig = {
testEnvironment: 'node',
testMatch: ['<rootDir>/src/server/**/*.test.ts'],
setupFilesAfterEnv: ['<rootDir>/tests/server/jest.setup.ts'],
globals: {
fetch,
},
};
export default async () => {
const clientConfig = await createJestConfig(customClientConfig)();
const serverConfig = await createJestConfig(customServerConfig)();
return {
randomize: true,
verbose: true,
collectCoverage: true,
collectCoverageFrom: [
'src/server/**/*.{ts,tsx}',
'src/client/**/*.{ts,tsx}',
'!src/**/mocks/**/*.{ts,tsx}',
'!**/*.{spec,test}.{ts,tsx}',
'!**/index.{ts,tsx}',
],
projects: [
{
displayName: 'client',
...clientConfig,
},
{
displayName: 'server',
...serverConfig,
},
],
};
};

View File

@ -9,30 +9,23 @@
"test:ui": "dotenv -e .env.test -- vitest --ui",
"test:e2e": "NODE_ENV=test dotenv -e .env -e .env.e2e -- playwright test",
"test:e2e:ui": "NODE_ENV=test dotenv -e .env -e .env.e2e -- playwright test --ui",
"test:client": "jest --colors --selectProjects client --",
"test:server": "jest --colors --selectProjects server --",
"test:vite": "dotenv -e .env.test -- vitest run --coverage",
"dev": "next dev --turbo",
"db:migrate": "NODE_ENV=development dotenv -e .env.local -- tsx ./src/server/run-migrations-dev.ts",
"lint": "next lint",
"lint:fix": "next lint --fix",
"build": "next build --experimental-build-mode compile",
"start": "NODE_ENV=production node server.js",
"start:dev-container": "./.devcontainer/filewatcher.sh && npm run start:dev",
"start:rc": "docker compose -f docker-compose.rc.yml --env-file .env up --build",
"start:dev": "docker compose -f docker-compose.dev.yml up --build",
"start:prod": "docker compose --env-file ./.env -f docker-compose.prod.yml up --build",
"start:pg": "docker run --name test-db -p 5433:5432 -d --rm -e POSTGRES_PASSWORD=postgres postgres:14",
"version": "echo $npm_package_version",
"release:rc": "./scripts/deploy/release-rc.sh",
"test:build": "docker buildx build --platform linux/amd64,linux/arm64 -t meienberger/runtipi:test .",
"test:build:arm64": "docker buildx build --platform linux/arm64 -t meienberger/runtipi:test .",
"test:build:arm7": "docker buildx build --platform linux/arm/v7 -t meienberger/runtipi:test .",
"test:build:amd64": "docker buildx build --platform linux/amd64 -t meienberger/runtipi:test .",
"tsc": "tsc"
"tsc": "tsc",
"gen:certs": "mkcert -key-file ./traefik/tls/key.pem -cert-file ./traefik/tls/cert.pem tipi.local \"*.tipi.local\"",
"prettier-check": "prettier --check .",
"postinstall": "./scripts/postinstall.sh"
},
"dependencies": {
"@hookform/resolvers": "^3.3.4",
"@hookform/resolvers": "^3.4.2",
"@otplib/core": "^12.0.1",
"@otplib/plugin-crypto": "^12.0.1",
"@otplib/plugin-thirty-two": "^12.0.1",
@ -45,13 +38,12 @@
"@radix-ui/react-tabs": "^1.0.4",
"@runtipi/postgres-migrations": "^5.3.0",
"@runtipi/shared": "workspace:^",
"@sentry/nextjs": "^8.0.0",
"@sentry/nextjs": "^8.4.0",
"@tabler/core": "1.0.0-beta20",
"@tabler/icons-react": "^3.2.0",
"argon2": "^0.40.1",
"bullmq": "^5.7.4",
"bullmq": "^5.7.12",
"clsx": "^2.1.0",
"connect-redis": "^7.1.1",
"drizzle-orm": "^0.30.9",
"fs-extra": "^11.2.0",
"geist": "^1.3.0",
@ -62,7 +54,7 @@
"minisearch": "^6.3.0",
"next": "14.2.3",
"next-client-cookies": "^1.1.1",
"next-intl": "^3.13.0",
"next-intl": "^3.14.1",
"next-safe-action": "^6.2.0",
"pg": "^8.11.5",
"qrcode.react": "^3.1.0",
@ -80,7 +72,7 @@
"remark-gfm": "^4.0.0",
"sass": "^1.77.1",
"semver": "^7.6.2",
"sharp": "0.33.3",
"sharp": "0.33.4",
"socket.io-client": "^4.7.5",
"uuid": "^9.0.1",
"validator": "^13.12.0",
@ -89,9 +81,9 @@
"zustand": "^4.5.2"
},
"devDependencies": {
"@babel/core": "^7.24.5",
"@babel/core": "^7.24.6",
"@faker-js/faker": "^8.4.1",
"@playwright/test": "^1.44.0",
"@playwright/test": "^1.44.1",
"@testing-library/dom": "^10.1.0",
"@testing-library/jest-dom": "^6.4.5",
"@testing-library/react": "^15.0.7",
@ -100,30 +92,24 @@
"@total-typescript/ts-reset": "^0.5.1",
"@types/eslint": "^8.56.10",
"@types/fs-extra": "^11.0.4",
"@types/jest": "^29.5.12",
"@types/jsonwebtoken": "^9.0.6",
"@types/lodash.merge": "^4.6.9",
"@types/node": "20.12.11",
"@types/node": "20.12.12",
"@types/pg": "^8.11.6",
"@types/react": "18.3.2",
"@types/react": "18.3.3",
"@types/react-dom": "18.3.0",
"@types/semver": "^7.5.8",
"@types/ssh2": "^1.15.0",
"@types/uuid": "^9.0.8",
"@types/validator": "^13.11.10",
"@typescript-eslint/eslint-plugin": "^6.21.0",
"@typescript-eslint/eslint-plugin": "^7.0.0",
"@typescript-eslint/parser": "^6.21.0",
"@vitejs/plugin-react": "^4.2.1",
"@vitest/coverage-v8": "^1.5.0",
"@vitest/ui": "^1.6.0",
"dotenv-cli": "^7.4.1",
"eslint": "8.57.0",
"eslint-config-airbnb": "^19.0.4",
"eslint-config-airbnb-typescript": "^17.1.0",
"eslint-config-next": "14.2.3",
"eslint-config-prettier": "^9.1.0",
"eslint-import-resolver-typescript": "^3.6.1",
"eslint-plugin-drizzle": "^0.2.3",
"eslint-plugin-import": "^2.29.1",
"eslint-plugin-jest": "^28.5.0",
"eslint-plugin-jest-dom": "^5.4.0",
@ -132,19 +118,14 @@
"eslint-plugin-react": "^7.34.1",
"eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-testing-library": "^6.2.2",
"eslint-plugin-vitest": "^0.5.4",
"jest": "^29.7.0",
"jest-environment-jsdom": "^29.7.0",
"jsdom": "^24.0.0",
"jsonc-eslint-parser": "^2.4.0",
"knip": "^5.9.4",
"memfs": "^4.8.2",
"msw": "^2.3.0",
"next-router-mock": "^0.9.13",
"prettier": "^3.2.5",
"ssh2": "^1.15.0",
"ts-jest": "^29.1.2",
"ts-node": "^10.9.2",
"tsx": "^4.10.2",
"typescript": "5.4.5",
"vite-tsconfig-paths": "^4.3.2",
"vitest": "^1.6.0",

View File

@ -1,7 +1,7 @@
module.exports = {
root: true,
plugins: ['@typescript-eslint', 'import'],
extends: ['plugin:@typescript-eslint/recommended', 'airbnb', 'airbnb-typescript', 'eslint:recommended', 'plugin:import/typescript', 'prettier'],
extends: ['plugin:@typescript-eslint/recommended', 'plugin:import/typescript', 'prettier'],
parser: '@typescript-eslint/parser',
parserOptions: {
ecmaVersion: 'latest',
@ -10,6 +10,7 @@ module.exports = {
tsconfigRootDir: __dirname,
},
rules: {
'@typescript-eslint/no-floating-promises': 1,
'import/prefer-default-export': 0,
'class-methods-use-this': 0,
'import/extensions': [
@ -26,7 +27,15 @@ module.exports = {
'import/no-extraneous-dependencies': [
'error',
{
devDependencies: ['build.js', '**/*.test.{ts,tsx}', '**/mocks/**', '**/__mocks__/**', '**/*.setup.{ts,js}', '**/*.config.{ts,js}', '**/tests/**'],
devDependencies: [
'build.js',
'**/*.test.{ts,tsx}',
'**/mocks/**',
'**/__mocks__/**',
'**/*.setup.{ts,js}',
'**/*.config.{ts,js}',
'**/tests/**',
],
},
],
'arrow-body-style': 0,

View File

@ -25,7 +25,8 @@
],
"scripts": {
"lint": "eslint --ext .ts src",
"tsc": "tsc --noEmit"
"tsc": "tsc --noEmit",
"prettier-check": "prettier --check ."
},
"keywords": [],
"author": "",
@ -37,7 +38,12 @@
"zod": "^3.23.8"
},
"devDependencies": {
"@sentry/types": "^8.0.0",
"@types/lodash.clonedeep": "^4.5.9"
"@sentry/types": "^8.4.0",
"@types/lodash.clonedeep": "^4.5.9",
"@typescript-eslint/eslint-plugin": "^7.0.0",
"@typescript-eslint/parser": "^6.21.0",
"eslint": "8.57.0",
"eslint-plugin-import": "^2.29.1",
"prettier": "^3.2.5"
}
}

View File

@ -1,7 +1,7 @@
module.exports = {
root: true,
plugins: ['@typescript-eslint', 'import'],
extends: ['plugin:@typescript-eslint/recommended', 'airbnb', 'airbnb-typescript', 'eslint:recommended', 'plugin:import/typescript', 'prettier'],
extends: ['plugin:@typescript-eslint/recommended', 'plugin:import/typescript', 'prettier'],
parser: '@typescript-eslint/parser',
parserOptions: {
ecmaVersion: 'latest',
@ -10,6 +10,7 @@ module.exports = {
tsconfigRootDir: __dirname,
},
rules: {
'@typescript-eslint/no-floating-promises': 1,
'import/prefer-default-export': 0,
'class-methods-use-this': 0,
'import/extensions': [
@ -26,7 +27,15 @@ module.exports = {
'import/no-extraneous-dependencies': [
'error',
{
devDependencies: ['build.js', '**/*.test.{ts,tsx}', '**/mocks/**', '**/__mocks__/**', '**/*.setup.{ts,js}', '**/*.config.{ts,js}', '**/tests/**'],
devDependencies: [
'build.js',
'**/*.test.{ts,tsx}',
'**/mocks/**',
'**/__mocks__/**',
'**/*.setup.{ts,js}',
'**/*.config.{ts,js}',
'**/tests/**',
],
},
],
'arrow-body-style': 0,

View File

@ -10,21 +10,26 @@
"tsc": "tsc",
"dev": "dotenv -e ../../.env nodemon",
"knip": "knip",
"lint": "eslint . --ext .ts"
"lint": "eslint . --ext .ts",
"prettier-check": "prettier --check ."
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"@faker-js/faker": "^8.4.1",
"@sentry/esbuild-plugin": "^2.16.1",
"@total-typescript/shoehorn": "^0.1.2",
"@sentry/esbuild-plugin": "^2.17.0",
"@types/web-push": "^3.6.3",
"@typescript-eslint/eslint-plugin": "^7.0.0",
"@typescript-eslint/parser": "^6.21.0",
"dotenv-cli": "^7.4.2",
"esbuild": "^0.19.4",
"eslint": "8.57.0",
"eslint-plugin-import": "^2.29.1",
"knip": "^5.15.1",
"memfs": "^4.8.2",
"nodemon": "^3.1.0",
"prettier": "^3.2.5",
"tsx": "^4.10.2",
"typescript": "^5.4.5",
"vite-tsconfig-paths": "^4.3.2",
@ -35,10 +40,10 @@
"@runtipi/postgres-migrations": "^5.3.0",
"@runtipi/shared": "workspace:^",
"@sentry/integrations": "^7.114.0",
"@sentry/node": "^8.0.0",
"bullmq": "^5.7.4",
"@sentry/node": "^8.4.0",
"bullmq": "^5.7.12",
"dotenv": "^16.4.5",
"hono": "^4.3.6",
"hono": "^4.3.11",
"ioredis": "^5.4.1",
"pg": "^8.11.5",
"socket.io": "^4.7.5",

View File

@ -138,7 +138,9 @@ const main = async () => {
// Start all apps
const appExecutor = new AppExecutors();
logger.info('Starting all apps...');
appExecutor.startAllApps();
// Fire and forget
void appExecutor.startAllApps();
const app = new Hono().basePath('/worker-api');
serve({ fetch: app.fetch, port: 5000 }, (info) => {
@ -157,4 +159,4 @@ const main = async () => {
}
};
main();
void main();

View File

@ -11,7 +11,9 @@ describe('app helpers', () => {
describe('Test: generateEnvFile()', () => {
it('should generate an env file', async () => {
// arrange
const appConfig = createAppConfig({ form_fields: [{ env_variable: 'TEST_FIELD', type: 'text', label: 'test', required: true }] });
const appConfig = createAppConfig({
form_fields: [{ env_variable: 'TEST_FIELD', type: 'text', label: 'test', required: true }],
});
const fakevalue = faker.string.alphanumeric(10);
// act
@ -23,7 +25,9 @@ describe('app helpers', () => {
expect(envmap.get('APP_PORT')).toBe(String(appConfig.port));
expect(envmap.get('APP_ID')).toBe(appConfig.id);
expect(envmap.get('ROOT_FOLDER_HOST')).toBe(process.env.ROOT_FOLDER_HOST);
expect(envmap.get('APP_DATA_DIR')).toBe(`${process.env.RUNTIPI_APP_DATA_PATH}/app-data/${appConfig.id}`);
expect(envmap.get('APP_DATA_DIR')).toBe(
`${process.env.RUNTIPI_APP_DATA_PATH}/app-data/${appConfig.id}`,
);
expect(envmap.get('APP_DOMAIN')).toBe(`localhost:${appConfig.port}`);
expect(envmap.get('APP_HOST')).toBe(`localhost`);
expect(envmap.get('APP_PROTOCOL')).toBe(`http`);
@ -35,12 +39,25 @@ describe('app helpers', () => {
await fs.promises.writeFile(`${DATA_DIR}/apps/${appConfig.id}/config.json`, '{}');
// act & assert
expect(generateEnvFile(appConfig.id, {})).rejects.toThrowError(`App ${appConfig.id} has invalid config.json file`);
await expect(generateEnvFile(appConfig.id, {})).rejects.toThrowError(
`App ${appConfig.id} has invalid config.json file`,
);
});
it('Should automatically generate value for random field', async () => {
// arrange
const appConfig = createAppConfig({ form_fields: [{ env_variable: 'RANDOM_FIELD', type: 'random', label: 'test', min: 32, max: 32, required: true }] });
const appConfig = createAppConfig({
form_fields: [
{
env_variable: 'RANDOM_FIELD',
type: 'random',
label: 'test',
min: 32,
max: 32,
required: true,
},
],
});
// act
await generateEnvFile(appConfig.id, {});
@ -53,10 +70,24 @@ describe('app helpers', () => {
it('Should not re-generate random field if it already exists', async () => {
// arrange
const appConfig = createAppConfig({ form_fields: [{ env_variable: 'RANDOM_FIELD', type: 'random', label: 'test', min: 32, max: 32, required: true }] });
const appConfig = createAppConfig({
form_fields: [
{
env_variable: 'RANDOM_FIELD',
type: 'random',
label: 'test',
min: 32,
max: 32,
required: true,
},
],
});
const randomField = faker.string.alphanumeric(32);
await fs.promises.mkdir(`${APP_DATA_DIR}/${appConfig.id}`, { recursive: true });
await fs.promises.writeFile(`${APP_DATA_DIR}/${appConfig.id}/app.env`, `RANDOM_FIELD=${randomField}`);
await fs.promises.writeFile(
`${APP_DATA_DIR}/${appConfig.id}/app.env`,
`RANDOM_FIELD=${randomField}`,
);
// act
await generateEnvFile(appConfig.id, {});
@ -68,7 +99,9 @@ describe('app helpers', () => {
it('Should throw an error if required field is not provided', async () => {
// arrange
const appConfig = createAppConfig({ form_fields: [{ env_variable: 'TEST_FIELD', type: 'text', label: 'test', required: true }] });
const appConfig = createAppConfig({
form_fields: [{ env_variable: 'TEST_FIELD', type: 'text', label: 'test', required: true }],
});
// act & assert
await expect(generateEnvFile(appConfig.id, {})).rejects.toThrowError();
@ -167,7 +200,10 @@ describe('app helpers', () => {
// act
await fs.promises.mkdir(`${APP_DATA_DIR}/${appConfig.id}`, { recursive: true });
await fs.promises.writeFile(`${APP_DATA_DIR}/${appConfig.id}/app.env`, `VAPID_PRIVATE_KEY=${vapidPrivateKey}\nVAPID_PUBLIC_KEY=${vapidPublicKey}`);
await fs.promises.writeFile(
`${APP_DATA_DIR}/${appConfig.id}/app.env`,
`VAPID_PRIVATE_KEY=${vapidPrivateKey}\nVAPID_PUBLIC_KEY=${vapidPublicKey}`,
);
await generateEnvFile(appConfig.id, {});
const envmap = await getAppEnvMap(appConfig.id);
@ -223,7 +259,9 @@ describe('app helpers', () => {
// assert
const appDataDir = `${APP_DATA_DIR}/${appConfig.id}`;
expect(await fs.promises.readFile(`${appDataDir}/data/subdir/subsubdir/test.txt`, 'utf8')).toBe('test');
expect(
await fs.promises.readFile(`${appDataDir}/data/subdir/subsubdir/test.txt`, 'utf8'),
).toBe('test');
expect(await fs.promises.readFile(`${appDataDir}/data/test.txt`, 'utf8')).toBe('test');
});

View File

@ -21,7 +21,7 @@ export class AppExecutors {
this.logger = logger;
}
private handleAppError = (
private handleAppError = async (
err: unknown,
appId: string,
event: Extract<SocketEvent, { type: 'app' }>['event'],
@ -31,12 +31,12 @@ export class AppExecutors {
});
if (err instanceof Error) {
SocketManager.emit({ type: 'app', event, data: { appId, error: err.message } });
await SocketManager.emit({ type: 'app', event, data: { appId, error: err.message } });
this.logger.error(`An error occurred: ${err.message}`);
return { success: false, message: err.message };
}
SocketManager.emit({ type: 'app', event, data: { appId, error: String(err) } });
await SocketManager.emit({ type: 'app', event, data: { appId, error: String(err) } });
return { success: false, message: `An error occurred: ${String(err)}` };
};
@ -105,7 +105,7 @@ export class AppExecutors {
await this.ensureAppDir(appId, form);
await generateEnvFile(appId, form);
SocketManager.emit({ type: 'app', event: 'generate_env_success', data: { appId } });
await SocketManager.emit({ type: 'app', event: 'generate_env_success', data: { appId } });
return { success: true, message: `App ${appId} env file regenerated successfully` };
} catch (err) {
return this.handleAppError(err, appId, 'generate_env_error');
@ -119,7 +119,7 @@ export class AppExecutors {
*/
public installApp = async (appId: string, form: AppEventForm) => {
try {
SocketManager.emit({ type: 'app', event: 'status_change', data: { appId } });
await SocketManager.emit({ type: 'app', event: 'status_change', data: { appId } });
if (process.getuid && process.getgid) {
this.logger.info(
@ -177,7 +177,7 @@ export class AppExecutors {
this.logger.info(`Docker-compose up for app ${appId} finished`);
SocketManager.emit({ type: 'app', event: 'install_success', data: { appId } });
await SocketManager.emit({ type: 'app', event: 'install_success', data: { appId } });
return { success: true, message: `App ${appId} installed successfully` };
} catch (err) {
@ -200,7 +200,7 @@ export class AppExecutors {
return { success: true, message: `App ${appId} is not an app. Skipping...` };
}
SocketManager.emit({ type: 'app', event: 'status_change', data: { appId } });
await SocketManager.emit({ type: 'app', event: 'status_change', data: { appId } });
this.logger.info(`Stopping app ${appId}`);
await this.ensureAppDir(appId, form);
@ -213,7 +213,7 @@ export class AppExecutors {
this.logger.info(`App ${appId} stopped`);
SocketManager.emit({ type: 'app', event: 'stop_success', data: { appId } });
await SocketManager.emit({ type: 'app', event: 'stop_success', data: { appId } });
const client = await getDbClient();
await client?.query('UPDATE app SET status = $1 WHERE id = $2', ['stopped', appId]);
@ -234,7 +234,7 @@ export class AppExecutors {
return { success: true, message: `App ${appId} is not an app. Skipping...` };
}
SocketManager.emit({ type: 'app', event: 'status_change', data: { appId } });
await SocketManager.emit({ type: 'app', event: 'status_change', data: { appId } });
this.logger.info(`Restarting app ${appId}`);
@ -262,7 +262,7 @@ export class AppExecutors {
this.logger.info(`App ${appId} restarted`);
SocketManager.emit({ type: 'app', event: 'restart_success', data: { appId } });
await SocketManager.emit({ type: 'app', event: 'restart_success', data: { appId } });
return { success: true, message: `App ${appId} restarted successfully` };
} catch (err) {
@ -272,7 +272,7 @@ export class AppExecutors {
public startApp = async (appId: string, form: AppEventForm, skipEnvGeneration = false) => {
try {
SocketManager.emit({ type: 'app', event: 'status_change', data: { appId } });
await SocketManager.emit({ type: 'app', event: 'status_change', data: { appId } });
this.logger.info(`Starting app ${appId}`);
@ -287,7 +287,7 @@ export class AppExecutors {
this.logger.info(`App ${appId} started`);
SocketManager.emit({ type: 'app', event: 'start_success', data: { appId } });
await SocketManager.emit({ type: 'app', event: 'start_success', data: { appId } });
const client = await getDbClient();
await client?.query('UPDATE app SET status = $1 WHERE id = $2', ['running', appId]);
@ -299,7 +299,7 @@ export class AppExecutors {
public uninstallApp = async (appId: string, form: AppEventForm) => {
try {
SocketManager.emit({ type: 'app', event: 'status_change', data: { appId } });
await SocketManager.emit({ type: 'app', event: 'status_change', data: { appId } });
const { appDirPath, appDataDirPath } = this.getAppPaths(appId);
this.logger.info(`Uninstalling app ${appId}`);
@ -331,7 +331,7 @@ export class AppExecutors {
this.logger.info(`App ${appId} uninstalled`);
SocketManager.emit({ type: 'app', event: 'uninstall_success', data: { appId } });
await SocketManager.emit({ type: 'app', event: 'uninstall_success', data: { appId } });
const client = await getDbClient();
await client?.query(`DELETE FROM app WHERE id = $1`, [appId]);
@ -343,7 +343,7 @@ export class AppExecutors {
public resetApp = async (appId: string, form: AppEventForm) => {
try {
SocketManager.emit({ type: 'app', event: 'status_change', data: { appId } });
await SocketManager.emit({ type: 'app', event: 'status_change', data: { appId } });
const { appDataDirPath } = this.getAppPaths(appId);
this.logger.info(`Resetting app ${appId}`);
@ -385,7 +385,7 @@ export class AppExecutors {
this.logger.info(`Running docker-compose up for app ${appId}`);
await compose(appId, 'up -d');
SocketManager.emit({ type: 'app', event: 'reset_success', data: { appId } });
await SocketManager.emit({ type: 'app', event: 'reset_success', data: { appId } });
const client = await getDbClient();
await client?.query(`UPDATE app SET status = $1 WHERE id = $2`, ['running', appId]);
@ -397,7 +397,7 @@ export class AppExecutors {
public updateApp = async (appId: string, form: AppEventForm) => {
try {
SocketManager.emit({ type: 'app', event: 'status_change', data: { appId } });
await SocketManager.emit({ type: 'app', event: 'status_change', data: { appId } });
const { appDirPath, repoPath } = this.getAppPaths(appId);
this.logger.info(`Updating app ${appId}`);
@ -423,7 +423,7 @@ export class AppExecutors {
await compose(appId, 'pull');
SocketManager.emit({ type: 'app', event: 'update_success', data: { appId } });
await SocketManager.emit({ type: 'app', event: 'update_success', data: { appId } });
return { success: true, message: `App ${appId} updated successfully` };
} catch (err) {

View File

@ -76,7 +76,7 @@ const runCommand = async (jobData: unknown) => {
/**
* Start the worker for the events queue
*/
export const startWorker = async () => {
export const startWorker = () => {
const repeatWorker = new Worker(
'repeat',
async (job) => {

File diff suppressed because it is too large Load Diff

0
public/js/.gitkeep Normal file
View File

View File

@ -45,8 +45,8 @@ while [ -n "${1-}" ]; do
shift
done
OS="$(cat /etc/[A-Za-z]*[_-][rv]e[lr]* | grep "^ID=" | cut -d= -f2 | uniq | tr '[:upper:]' '[:lower:]' | tr -d '"')"
SUB_OS="$(cat /etc/[A-Za-z]*[_-][rv]e[lr]* | grep "^ID_LIKE=" | cut -d= -f2 | uniq | tr '[:upper:]' '[:lower:]' | tr -d '"' || echo 'unknown')"
OS="$(cat $(ls -p /etc | grep -v / | grep "[A-Za-z]*[_-][rv]e[lr]" | awk '{print "/etc/" $1}') | grep "^ID=" | cut -d= -f2 | uniq | tr '[:upper:]' '[:lower:]' | tr -d '"')"
SUB_OS="$(cat $(ls -p /etc | grep -v / | grep "[A-Za-z]*[_-][rv]e[lr]" | awk '{print "/etc/" $1}') | grep "^ID_LIKE=" | cut -d= -f2 | uniq | tr '[:upper:]' '[:lower:]' | tr -d '"' || echo 'unknown')"
function install_generic() {
local dependency="${1}"

3
scripts/postinstall.sh Executable file
View File

@ -0,0 +1,3 @@
#!/bin/sh
cp ./node_modules/@tabler/core/dist/js/tabler.min.js ./public/js/tabler.min.js

View File

@ -61,7 +61,7 @@ export const InstallFormField = (props: IProps) => {
control={control}
name={field.env_variable}
defaultValue={field.default}
render={({ field: { onChange, value, ref, ...rest } }) => (
render={({ field: { onChange, value, ...rest } }) => (
<Select value={value as string} defaultValue={initialValue} onValueChange={onChange} {...rest}>
<SelectTrigger className="mb-3" error={error} label={label}>
<SelectValue placeholder={t('APP_INSTALL_FORM_CHOOSE_OPTION')} />

View File

@ -1,6 +1,5 @@
import { AppInfo } from '@runtipi/shared';
export type SortableColumns = keyof Pick<AppInfo, 'id'>;
export type SortDirection = 'asc' | 'desc';
export type AppTableData = Omit<AppInfo, 'description' | 'form_fields' | 'source' | 'status' | 'url_suffix' | 'version'>[];

View File

@ -45,7 +45,7 @@ export const Header: React.FC<IProps> = ({ isUpdateAvailable, authenticated = tr
return (
<header className="text-white navbar navbar-expand-md navbar-dark navbar-overlap d-print-none" data-bs-theme="dark">
<Script src="https://cdn.jsdelivr.net/npm/@tabler/core@latest/dist/js/tabler.min.js" async />
<Script src="/js/tabler.min.js" async />
<div className="container-xl">
<button className="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbar-menu">
<span className="navbar-toggler-icon" />

View File

@ -12,7 +12,6 @@ export const deleteLinkAction = action(z.number(), async (linkId: number) => {
const linksService = new CustomLinksServiceClass();
// eslint-disable-next-line drizzle/enforce-delete-with-where -- False positive
await linksService.delete(linkId, user.id);
return { success: true };
} catch (e) {

View File

@ -1,7 +1,6 @@
'use client';
import React, { ComponentProps } from 'react';
import { CookiesProvider } from 'next-client-cookies';
import React from 'react';
import { AbstractIntlMessages, NextIntlClientProvider } from 'next-intl';
import { QueryClient, QueryClientProvider } from 'react-query';
import { ThemeProvider } from './ThemeProvider';
@ -9,7 +8,6 @@ import { SocketProvider } from './SocketProvider/SocketProvider';
type Props = {
children: React.ReactNode;
cookies: ComponentProps<typeof CookiesProvider>['value'];
initialTheme?: string;
locale?: string;
messages: AbstractIntlMessages;
@ -17,18 +15,14 @@ type Props = {
const queryClient = new QueryClient({ defaultOptions: { queries: { refetchOnWindowFocus: false } } });
export const ClientProviders = ({ children, initialTheme, cookies, locale, messages }: Props) => {
export const ClientProviders = ({ children, initialTheme, locale, messages }: Props) => {
return (
<QueryClientProvider client={queryClient}>
<NextIntlClientProvider locale={locale} messages={messages} timeZone={Intl.DateTimeFormat().resolvedOptions().timeZone}>
<SocketProvider>
<CookiesProvider value={cookies}>
<ThemeProvider initialTheme={initialTheme}>{children}</ThemeProvider>
</CookiesProvider>
<ThemeProvider initialTheme={initialTheme}>{children}</ThemeProvider>
</SocketProvider>
</NextIntlClientProvider>
</QueryClientProvider>
);
};
export const ClientCookiesProvider: typeof CookiesProvider = (props) => <CookiesProvider {...props} />;

View File

@ -42,7 +42,7 @@ export const ThemeProvider = (props: Props) => {
useEffect(() => {
const autoTheme = getAutoTheme();
if (autoTheme === 'christmas' && allowAutoThemes && typeof window !== 'undefined') {
loadChristmasTheme();
void loadChristmasTheme();
}
}, [allowAutoThemes]);

View File

@ -11,6 +11,7 @@ import clsx from 'clsx';
import { Toaster } from 'react-hot-toast';
import { getCurrentLocale } from '../utils/getCurrentLocale';
import { ClientProviders } from './components/ClientProviders';
import { CookiesProvider } from 'next-client-cookies/server';
export const metadata: Metadata = {
title: 'Tipi',
@ -30,13 +31,15 @@ export default async function RootLayout({ children }: { children: React.ReactNo
return (
<html lang={locale} className={clsx(GeistSans.className, 'border-top-wide border-primary')}>
<ClientProviders messages={mergedMessages} locale={locale} initialTheme={theme?.value} cookies={cookies().getAll()}>
<body data-bs-theme={theme?.value}>
<input type="hidden" value={JSON.stringify(clientSettings)} id="client-settings" />
{children}
<Toaster />
</body>
</ClientProviders>
<CookiesProvider>
<ClientProviders messages={mergedMessages} locale={locale} initialTheme={theme?.value}>
<body data-bs-theme={theme?.value}>
<input type="hidden" value={JSON.stringify(clientSettings)} id="client-settings" />
{children}
<Toaster />
</body>
</ClientProviders>
</CookiesProvider>
</html>
);
}

View File

@ -33,7 +33,7 @@ DialogPortal.displayName = DialogPrimitive.Portal.displayName;
const DialogOverlay = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Overlay>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
>(({ className, children, ...props }, ref) => <DialogPrimitive.Overlay className={clsx('', className)} {...props} ref={ref} />);
>(({ className, ...props }, ref) => <DialogPrimitive.Overlay className={clsx('', className)} {...props} ref={ref} />);
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName;
const DialogContent = React.forwardRef<

View File

@ -12,8 +12,6 @@ type TriggerProps = {
const Select: React.FC<React.ComponentPropsWithoutRef<typeof SelectPrimitive.Root> & { label?: string; error?: string; className?: string }> = ({
children,
error,
className,
...props
}) => {
return <SelectPrimitive.Root {...props}>{children}</SelectPrimitive.Root>;

View File

@ -7,10 +7,10 @@ async function initMocks() {
server.listen();
} else {
const { worker } = await import('./browser');
worker.start();
await worker.start();
}
}
initMocks();
void initMocks();
export { initMocks };

View File

@ -1,4 +1,4 @@
export const THEMES = {
const THEMES = {
christmas: {
name: 'christmas',
month: 11,
@ -7,7 +7,7 @@ export const THEMES = {
},
};
export type Theme = keyof typeof THEMES | 'default';
type Theme = keyof typeof THEMES | 'default';
export const getAutoTheme = (): Theme => {
const date = new Date();

View File

@ -21,5 +21,3 @@ export const readFile = (path: string): string => {
export const readdirSync = (path: string): string[] => fs.readdirSync(path);
export const fileExists = (path: string): boolean => fs.existsSync(path);
export const unlinkFile = (path: string) => fs.promises.unlink(path);

View File

@ -85,7 +85,7 @@ export class EventDispatcher {
Logger.info(`Scheduling event ${JSON.stringify(event)} with cron expression ${cronExpression}`);
const jobid = this.generateJobId(event);
this.queue.add(jobid, eventSchema.parse(event), { repeat: { pattern: cronExpression } });
void this.queue.add(jobid, eventSchema.parse(event), { repeat: { pattern: cronExpression } });
}
public async close() {

View File

@ -40,13 +40,13 @@ describe('Test: getConfig', () => {
});
describe('Test: setConfig', () => {
it('It should be able set config', () => {
it('It should be able set config', async () => {
// arrange
const randomWord = faker.internet.url();
// act
const tipiConfig = new TipiConfigClass(0);
tipiConfig.setConfig('appsRepoUrl', randomWord);
await tipiConfig.setConfig('appsRepoUrl', randomWord);
const config = tipiConfig.getConfig();
// assert
@ -136,12 +136,12 @@ describe('Test: setSettings', () => {
expect(settingsJson.appsRepoUrl).toBe(fakeSettings.appsRepoUrl);
});
it('should not write settings to json file if there are invalid values', () => {
it('should not write settings to json file if there are invalid values', async () => {
// arrange
const fakeSettings = { appsRepoUrl: 10 };
// act
new TipiConfigClass(0).setSettings(fakeSettings as object);
await new TipiConfigClass(0).setSettings(fakeSettings as object);
const settingsJson = (readJsonFile('/data/state/settings.json') || {}) as { [key: string]: string };
// assert
@ -154,7 +154,7 @@ describe('Test: setSettings', () => {
let error;
const fakeSettings = { appsRepoUrl: faker.internet.url() };
const tipiConf = new TipiConfigClass(0);
tipiConf.setConfig('demoMode', true);
await tipiConf.setConfig('demoMode', true);
// act
try {

View File

@ -43,7 +43,7 @@ describe('Test: checkAppRequirements()', () => {
it('Should throw if architecture is not supported', async () => {
// arrange
TipiConfig.setConfig('architecture', 'arm64');
await TipiConfig.setConfig('architecture', 'arm64');
const appConfig = createAppConfig({ supported_architectures: ['arm'] });
// assert

View File

@ -101,13 +101,13 @@ export class AppServiceClass {
try {
await this.queries.updateApp(app.id, { status: 'starting' });
eventDispatcher
void eventDispatcher
.dispatchEventAsync({ type: 'app', command: 'start', appid: app.id, form: castAppConfig(app.config) })
.then(({ success }) => {
if (success) {
this.queries.updateApp(app.id, { status: 'running' });
this.queries.updateApp(app.id, { status: 'running' }).catch(Logger.error);
} else {
this.queries.updateApp(app.id, { status: 'stopped' });
this.queries.updateApp(app.id, { status: 'stopped' }).catch(Logger.error);
}
});
} catch (e) {
@ -135,7 +135,7 @@ export class AppServiceClass {
await this.queries.updateApp(appName, { status: 'starting' });
const eventDispatcher = new EventDispatcher('startApp');
eventDispatcher
void eventDispatcher
.dispatchEventAsync({
type: 'app',
command: 'start',
@ -144,13 +144,13 @@ export class AppServiceClass {
})
.then(({ success, stdout }) => {
if (success) {
this.queries.updateApp(appName, { status: 'running' });
this.queries.updateApp(appName, { status: 'running' }).catch(Logger.error);
} else {
this.queries.updateApp(appName, { status: 'stopped' });
Logger.error(`Failed to start app ${appName}: ${stdout}`);
this.queries.updateApp(appName, { status: 'stopped' }).catch(Logger.error);
}
eventDispatcher.close();
void eventDispatcher.close();
});
const updatedApp = await this.queries.getApp(appName);
@ -228,15 +228,15 @@ export class AppServiceClass {
// Run script
const eventDispatcher = new EventDispatcher('installApp');
eventDispatcher.dispatchEventAsync({ type: 'app', command: 'install', appid: id, form }).then(({ success, stdout }) => {
void eventDispatcher.dispatchEventAsync({ type: 'app', command: 'install', appid: id, form }).then(({ success, stdout }) => {
if (success) {
this.queries.updateApp(id, { status: 'running' });
this.queries.updateApp(id, { status: 'running' }).catch(Logger.error);
} else {
this.queries.deleteApp(id);
this.queries.deleteApp(id).catch(Logger.error);
Logger.error(`Failed to install app ${id}: ${stdout}`);
}
eventDispatcher.close();
void eventDispatcher.close();
});
}
};
@ -358,16 +358,18 @@ export class AppServiceClass {
await this.queries.updateApp(id, { status: 'stopping' });
const eventDispatcher = new EventDispatcher('stopApp');
eventDispatcher.dispatchEventAsync({ type: 'app', command: 'stop', appid: id, form: castAppConfig(app.config) }).then(({ success, stdout }) => {
if (success) {
this.queries.updateApp(id, { status: 'stopped' });
} else {
Logger.error(`Failed to stop app ${id}: ${stdout}`);
this.queries.updateApp(id, { status: 'running' });
}
void eventDispatcher
.dispatchEventAsync({ type: 'app', command: 'stop', appid: id, form: castAppConfig(app.config) })
.then(({ success, stdout }) => {
if (success) {
this.queries.updateApp(id, { status: 'stopped' }).catch(Logger.error);
} else {
Logger.error(`Failed to stop app ${id}: ${stdout}`);
this.queries.updateApp(id, { status: 'running' }).catch(Logger.error);
}
eventDispatcher.close();
});
void eventDispatcher.close();
});
const updatedApp = await this.queries.getApp(id);
return updatedApp;
@ -392,16 +394,16 @@ export class AppServiceClass {
await this.queries.updateApp(id, { status: 'uninstalling' });
const eventDispatcher = new EventDispatcher('uninstallApp');
eventDispatcher
void eventDispatcher
.dispatchEventAsync({ type: 'app', command: 'uninstall', appid: id, form: castAppConfig(app.config) })
.then(({ stdout, success }) => {
if (success) {
this.queries.deleteApp(id);
this.queries.deleteApp(id).catch(Logger.error);
} else {
this.queries.updateApp(id, { status: 'stopped' });
this.queries.updateApp(id, { status: 'stopped' }).catch(Logger.error);
Logger.error(`Failed to uninstall app ${id}: ${stdout}`);
}
eventDispatcher.close();
void eventDispatcher.close();
});
return { id, status: 'missing', config: {} };
@ -416,18 +418,20 @@ export class AppServiceClass {
public resetApp = async (id: string) => {
const app = await this.getApp(id);
this.queries.updateApp(id, { status: 'resetting' });
await this.queries.updateApp(id, { status: 'resetting' });
const eventDispatcher = new EventDispatcher('resetApp');
eventDispatcher.dispatchEventAsync({ type: 'app', command: 'reset', appid: id, form: castAppConfig(app.config) }).then(({ stdout, success }) => {
if (success) {
this.queries.updateApp(id, { status: 'running' });
} else {
this.queries.updateApp(id, { status: 'stopped' });
Logger.error(`Failed to reset app ${id}: ${stdout}`);
}
eventDispatcher.close();
});
void eventDispatcher
.dispatchEventAsync({ type: 'app', command: 'reset', appid: id, form: castAppConfig(app.config) })
.then(({ stdout, success }) => {
if (success) {
this.queries.updateApp(id, { status: 'running' }).catch(Logger.error);
} else {
this.queries.updateApp(id, { status: 'stopped' }).catch(Logger.error);
Logger.error(`Failed to reset app ${id}: ${stdout}`);
}
void eventDispatcher.close();
});
};
/**
@ -447,16 +451,16 @@ export class AppServiceClass {
await this.queries.updateApp(id, { status: 'restarting' });
const eventDispatcher = new EventDispatcher('restartApp');
eventDispatcher
void eventDispatcher
.dispatchEventAsync({ type: 'app', command: 'restart', appid: id, form: castAppConfig(app.config) })
.then(({ success, stdout }) => {
if (!success) {
Logger.error(`Failed to restart app ${id}: ${stdout}`);
}
this.queries.updateApp(id, { status: 'running' });
this.queries.updateApp(id, { status: 'running' }).catch(Logger.error);
eventDispatcher.close();
void eventDispatcher.close();
});
const updatedApp = await this.queries.getApp(id);
@ -508,7 +512,7 @@ export class AppServiceClass {
await this.queries.updateApp(id, { status: 'updating' });
const eventDispatcher = new EventDispatcher('updateApp');
eventDispatcher
void eventDispatcher
.dispatchEventAsync({
type: 'app',
command: 'update',
@ -519,18 +523,18 @@ export class AppServiceClass {
if (success) {
const appInfo = getAppInfo(app.id, app.status);
this.queries.updateApp(id, { version: appInfo?.tipi_version });
this.queries.updateApp(id, { version: appInfo?.tipi_version }).catch(Logger.error);
if (appStatusBeforeUpdate === 'running') {
this.startApp(id);
this.startApp(id).catch(Logger.error);
} else {
this.queries.updateApp(id, { status: appStatusBeforeUpdate });
this.queries.updateApp(id, { status: appStatusBeforeUpdate }).catch(Logger.error);
}
} else {
this.queries.updateApp(id, { status: 'stopped' });
this.queries.updateApp(id, { status: 'stopped' }).catch(Logger.error);
Logger.error(`Failed to update app ${id}: ${stdout}`);
}
eventDispatcher.close();
void eventDispatcher.close();
});
const updatedApp = await this.getApp(id);

View File

@ -48,9 +48,7 @@ const createDatabase = async (testsuite: string): Promise<TestDatabase> => {
* @param {TestDatabase} database - database to clear
*/
const clearDatabase = async (database: TestDatabase) => {
// eslint-disable-next-line drizzle/enforce-delete-with-where -- we want to clear the whole table
await database.db.delete(schema.userTable);
// eslint-disable-next-line drizzle/enforce-delete-with-where -- we want to clear the whole table
await database.db.delete(schema.appTable);
};

View File

@ -4,9 +4,9 @@ import { TipiConfig } from '../../core/TipiConfig';
import { encrypt, decrypt } from '../encryption';
describe('Test: encrypt', () => {
it('should encrypt the provided data', () => {
it('should encrypt the provided data', async () => {
// arrange
TipiConfig.setConfig('jwtSecret', faker.lorem.word());
await TipiConfig.setConfig('jwtSecret', faker.lorem.word());
const data = faker.lorem.word();
const salt = faker.lorem.word();
@ -17,9 +17,9 @@ describe('Test: encrypt', () => {
expect(encryptedData).not.toEqual(data);
});
it('should decrypt the provided data', () => {
it('should decrypt the provided data', async () => {
// arrange
TipiConfig.setConfig('jwtSecret', faker.lorem.word());
await TipiConfig.setConfig('jwtSecret', faker.lorem.word());
const data = faker.lorem.word();
const salt = faker.lorem.word();
@ -31,24 +31,24 @@ describe('Test: encrypt', () => {
expect(decryptedData).toEqual(data);
});
it('should throw an error if jwtSecret has changed', () => {
it('should throw an error if jwtSecret has changed', async () => {
// arrange
TipiConfig.setConfig('jwtSecret', faker.lorem.word());
await TipiConfig.setConfig('jwtSecret', faker.lorem.word());
const data = faker.lorem.word();
const salt = faker.lorem.word();
// act
const encryptedData = encrypt(data, salt);
TipiConfig.setConfig('jwtSecret', faker.lorem.word());
await TipiConfig.setConfig('jwtSecret', faker.lorem.word());
const decrypting = () => decrypt(encryptedData, salt);
// assert
expect(decrypting).toThrow();
});
it('should throw an error if salt has changed', () => {
it('should throw an error if salt has changed', async () => {
// arrange
TipiConfig.setConfig('jwtSecret', faker.lorem.word());
await TipiConfig.setConfig('jwtSecret', faker.lorem.word());
const data = faker.lorem.word();
const salt = faker.lorem.word();

View File

@ -12,14 +12,7 @@ export const isInstanceInsecure = () => {
const myHeaders = headers();
const ip = myHeaders.get('x-forwarded-host')?.split(':')[0] || '';
if (ipaddrjs.isValid(ip)) {
const range = ipaddrjs.parse(ip!).range();
if (range !== 'private' && range !== 'carrierGradeNat' && range !== 'loopback') {
return true;
}
} else if (ip !== 'localhost' && myHeaders.get('x-forwarded-proto') === 'http') {
return true;
}
return false;
const isPublicIp = ipaddrjs.isValid(ip) && !['private', 'carrierGradeNat', 'loopback'].includes(ipaddrjs.parse(ip).range());
const isHttpProtocol = ip !== 'localhost' && myHeaders.get('x-forwarded-proto') === 'http';
return isPublicIp || isHttpProtocol;
};