From 48c1e2607f56db5551d52c25816b0db851a8b95e Mon Sep 17 00:00:00 2001
From: Nicolas Meienberger <47644445+meienberger@users.noreply.github.com>
Date: Fri, 4 Nov 2022 07:49:40 +0000
Subject: [PATCH] Release/0.7.2 (#249)
* feat: move from cookie base auth to jwt auth
test: mock redis
* test: auth.service & auth.resolver
test: auth.resolver
* test: session middleware
* chore: bump version
fix: merge conflicts
* docs: update readme & trace start script
* fix: start script unbound variables [skip ci]
* fix: kill watcher function [skip ci]
* fix: register store token
* fix: don't delete token immediately after refresh. keep it for 6 sec to account for delays
---
.gitignore | 1 +
README.md | 67 ++++-
docker-compose.dev.yml | 13 +-
docker-compose.rc.yml | 13 +-
docker-compose.yml | 13 +-
package.json | 2 +-
packages/dashboard/jest.config.js | 1 -
packages/dashboard/package.json | 2 +-
.../src/components/Layout/Layout.tsx | 10 +-
.../src/components/Layout/SideMenu.tsx | 7 +-
packages/dashboard/src/core/apollo/client.ts | 2 +-
.../src/core/apollo/links/authLink.ts | 14 +
.../src/core/apollo/links/httpLink.ts | 1 -
.../dashboard/src/core/apollo/links/index.ts | 2 +
packages/dashboard/src/generated/graphql.tsx | 65 +++--
.../src/graphql/mutations/login.graphql | 4 +-
.../src/graphql/mutations/register.graphql | 4 +-
.../src/graphql/queries/refreshToken.graphql | 5 +
.../src/modules/Auth/containers/Login.tsx | 13 +-
.../modules/Auth/containers/Onboarding.tsx | 10 +-
packages/system-api/__mocks__/redis.ts | 24 ++
packages/system-api/package.json | 11 +-
packages/system-api/src/config/TipiCache.ts | 62 ++++-
.../system-api/src/core/config/TipiConfig.ts | 3 +
.../__tests__/sessionMiddleware.test.ts | 75 ++++++
.../src/core/middlewares/sessionMiddleware.ts | 39 ++-
packages/system-api/src/declarations.d.ts | 9 +
packages/system-api/src/helpers/helpers.ts | 2 +-
.../apps/__tests__/apps.service.test.ts | 2 +-
.../src/modules/apps/apps.service.ts | 13 +-
.../auth/__tests__/auth.resolver.test.ts | 81 +++++-
.../auth/__tests__/auth.service.test.ts | 168 +++++++++++-
.../src/modules/auth/auth.resolver.ts | 42 +--
.../src/modules/auth/auth.service.ts | 48 +++-
.../system-api/src/modules/auth/auth.types.ts | 8 +-
.../system/__tests__/system.resolver.test.ts | 3 +-
.../system/__tests__/system.service.test.ts | 1 +
.../src/modules/system/system.service.ts | 5 +-
packages/system-api/src/server.ts | 32 +--
packages/system-api/src/test/gcall.ts | 5 +-
.../src/test/mutations/login.graphql | 5 +-
.../src/test/mutations/register.graphql | 5 +-
packages/system-api/src/test/queries/index.ts | 2 +
.../src/test/queries/refreshToken.graphql | 6 +
packages/system-api/src/types.ts | 7 -
pnpm-lock.yaml | 245 ++++++++----------
scripts/common.sh | 8 +-
scripts/start-dev.sh | 7 +-
scripts/start.sh | 22 +-
scripts/stop.sh | 3 +-
scripts/watcher.sh | 5 -
51 files changed, 856 insertions(+), 341 deletions(-)
create mode 100644 packages/dashboard/src/core/apollo/links/authLink.ts
create mode 100644 packages/dashboard/src/graphql/queries/refreshToken.graphql
create mode 100644 packages/system-api/__mocks__/redis.ts
create mode 100644 packages/system-api/src/core/middlewares/__tests__/sessionMiddleware.test.ts
create mode 100644 packages/system-api/src/declarations.d.ts
create mode 100644 packages/system-api/src/test/queries/refreshToken.graphql
diff --git a/.gitignore b/.gitignore
index 4e012e49..960959a0 100644
--- a/.gitignore
+++ b/.gitignore
@@ -11,6 +11,7 @@ github.secrets
node_modules/
app-data/*
data/postgres
+data/redis
!app-data/.gitkeep
repos/*
!repos/.gitkeep
diff --git a/README.md b/README.md
index e77ad9c6..21e78903 100644
--- a/README.md
+++ b/README.md
@@ -1,7 +1,11 @@
-# ⛺️ Tipi — A personal homeserver for everyone
+# Tipi — A personal homeserver for everyone
+
+
[](#contributors-)
+
+
[](https://github.com/meienberger/runtipi/blob/master/LICENSE)
[](https://github.com/meienberger/runtipi/releases)

@@ -9,11 +13,14 @@
[](https://hub.docker.com/r/meienberger/runtipi/)

[](https://codecov.io/gh/meienberger/runtipi)
+
#### Join the discussion
+
[](https://discord.gg/Bu9qEPnHsc)
[](https://matrix.to/#/#runtipi:matrix.org)

+
> ⚠️ Tipi is still at an early stage of development and issues are to be expected. Feel free to open an issue or pull request if you find a bug.
Tipi is a personal homeserver orchestrator. It is running docker containers under the hood and provides a simple web interface to manage them. Every service comes with an opinionated configuration in order to remove the need for manual configuration and network setup.
@@ -21,13 +28,14 @@ Tipi is a personal homeserver orchestrator. It is running docker containers unde
Check our demo instance : **[demo.runtipi.com](https://demo.runtipi.com)** / username: **user@runtipi.com** / password: **runtipi**
## Apps available
+
- [Adguard Home](https://github.com/AdguardTeam/AdGuardHome) - Adguard Home DNS adblocker
- [Booksonic](https://github.com/popeen) - A server for streaming your audiobooks
- [BookStack](https://www.bookstackapp.com/) - BookStack is a self-hosted platform for organising and storing information.
- [Calibre-Web](https://github.com/janeczku/calibre-web) - Web Ebook Reader
-- [Code-Server](https://github.com/coder/code-server) - Web VS Code
+- [Code-Server](https://github.com/coder/code-server) - Web VS Code
- [Filebrowser](https://github.com/filebrowser/filebrowser) - Web File Browser
-- [Firefly III](https://github.com/firefly-iii/firefly-iii) - A personal finances manager
+- [Firefly III](https://github.com/firefly-iii/firefly-iii) - A personal finances manager
- [FreshRSS](https://github.com/FreshRSS/FreshRSS) - A free, self-hostable RSS aggregator
- [Ghost](https://github.com/TryGhost/Ghost) - Ghost - Turn your audience into a business
- [Gitea](https://github.com/go-gitea/gitea) - Gitea - A painless self-hosted Git service
@@ -84,9 +92,11 @@ You can find and submit new apps inside of the [RunTipi Appstore](https://github
## 🛠 Installation
### Installation Requirements
+
Ubuntu 18.04 LTS or higher is recommended. However other major Linux distribution are supported but may lead to installation issues. Please file an issue if you encounter one.
### Step 1. Download Tipi
+
Run this in an empty directory where you want to install Tipi.
```bash
@@ -94,6 +104,7 @@ git clone https://github.com/meienberger/runtipi.git
```
### Step 2. Run Tipi
+
cd into the downloaded directory and run the start script.
```bash
@@ -115,45 +126,73 @@ sudo ./scripts/stop.sh
```
### Custom settings
-You can change the default settings by creating a `settings.json` file. The file should be located in the `state` directory. This file will make your changes persist across restarts. Example file with all possible values:
+
+You can change the default settings by creating a `settings.json` file. The file should be located in the `state` directory. This file will make your changes persist across restarts. Example file:
```json
{
- "dnsIp": "9.9.9.9", // DNS IP address
- "domain": "mydomain.com", // Domain name to link to the dashboard
- "port": 7000, // Change default http port 80
- "sslPort": 7001, // Change default ssl port 443
- "listenIp": "192.168.1.1", // Change default listen ip (advanced)
- "storagePath": "/mnt/usb", // Change default storage path of app data
+ "dnsIp": "9.9.9.9",
+ "domain": "mydomain.com"
}
-
```
-## Linking a domain to your dashboard
+Available settings:
+
+- `dnsIp` - The IP address of the DNS server to use. Default: `9.9.9.9`
+- `domain` - The domain name to use for the dashboard. Default: `localhost`
+- `port` - The port to use for the dashboard. Default: `80`
+- `sslPort` - The port to use for the dashboard with SSL. Default: `443`
+- `listenIp` - The IP address to listen on. Default: `automatically detected`
+- `storagePath` - The path to use for storing data. Default: `runtipi/app-data`
+
+### Linking a domain to your dashboard
+
If you want to link a domain to your dashboard, you can do so by providing the `--domain` option in the start script.
```bash
sudo ./scripts/start.sh --domain mydomain.com
```
-You can also specify it in the `settings.json` file as shown in the previous section.
+You can also specify it in the `settings.json` file as shown in the previous section to keep the setting saved across restarts.
A Let's Encrypt certificate will be generated and installed automatically. Make sure to have ports 80 and 443 open on your firewall and that your domain has an **A** record pointing to your server IP.
+Please note that this setting will only expose the dashboard. If you want to expose other apps, you need to configure them individually. You cannot use the `--domain` option to expose apps.
+
+This option will only work if you keep the default port 80 and 443 for the dashboard.
+
+### Uninstalling Tipi
+
+Make sure Tipi is completely stopped and then remove the `runtipi` directory.
+
+```bash
+sudo ./scripts/stop.sh
+cd ..
+sudo rm -rf runtipi
+```
+
+## 📚 Documentation
+
+You can find more documentation and tutorials / FAQ in the [Wiki](https://github.com/meienberger/runtipi/wiki).
+
## ❤️ Contributing
Tipi is made to be very easy to plug in new apps. We welcome and appreciate new contributions.
If you want to add a new app or feature, you can follow the [Contribution guide](https://github.com/meienberger/runtipi/wiki/Adding-your-own-app) for instructions on how to do so.
+We are looking for contributions of all kinds. If you know design, development, or have ideas for new features, please get in touch.
+
## 📜 License
+
[](https://github.com/meienberger/runtipi/blob/master/LICENSE)
Tipi is licensed under the GNU General Public License v3.0. TL;DR — You may copy, distribute and modify the software as long as you track changes/dates in source files. Any modifications to or software including (via compiler) GPL-licensed code must also be made available under the GPL along with build & install instructions.
-The bash scripts `app.sh` contained in the `scripts` folder contains some snippets from [Umbrel](https://github.com/getumbrel/umbrel)'s code. Therefore some parts of the code are licensed under the PolyForm Noncommercial License 1.0.0 license. You can for now consider the whole file under this license. We are actively working on re-writing those parts in order to make them available under the GPL license like the rest of our code.
+The bash script `app.sh` located in the `scripts` folder contains some snippets from [Umbrel](https://github.com/getumbrel/umbrel)'s code. Therefore some parts of the code are licensed under the PolyForm Noncommercial License 1.0.0 license. You can for now consider the whole file under this license. We are actively working on re-writing those parts in order to make them available under the GPL license like the rest of our code.
## 🗣 Community
+
- [Matrix](https://matrix.to/#/#runtipi:matrix.org)
- [Twitter](https://twitter.com/runtipi)
- [Telegram](https://t.me/+72-y10MnLBw2ZGI0)
diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml
index 14eb3953..9e8e7dc2 100644
--- a/docker-compose.dev.yml
+++ b/docker-compose.dev.yml
@@ -4,7 +4,7 @@ services:
reverse-proxy:
container_name: reverse-proxy
image: traefik:v2.8
- restart: always
+ restart: unless-stopped
ports:
- ${NGINX_PORT-80}:80
- ${NGINX_PORT_SSL-443}:443
@@ -21,7 +21,7 @@ services:
tipi-db:
container_name: tipi-db
image: postgres:14
- restart: on-failure
+ restart: unless-stopped
stop_grace_period: 1m
volumes:
- ./data/postgres:/var/lib/postgresql/data
@@ -39,6 +39,15 @@ services:
networks:
- tipi_main_network
+ tipi-redis:
+ container_name: tipi-redis
+ image: redis:alpine
+ restart: unless-stopped
+ volumes:
+ - ./data/redis:/data
+ networks:
+ - tipi_main_network
+
api:
build:
context: .
diff --git a/docker-compose.rc.yml b/docker-compose.rc.yml
index a2a5ae1d..c0ff8acb 100644
--- a/docker-compose.rc.yml
+++ b/docker-compose.rc.yml
@@ -4,7 +4,7 @@ services:
reverse-proxy:
container_name: reverse-proxy
image: traefik:v2.8
- restart: always
+ restart: unless-stopped
ports:
- ${NGINX_PORT-80}:80
- ${NGINX_PORT_SSL-443}:443
@@ -20,7 +20,7 @@ services:
tipi-db:
container_name: tipi-db
image: postgres:14
- restart: on-failure
+ restart: unless-stopped
stop_grace_period: 1m
volumes:
- ./data/postgres:/var/lib/postgresql/data
@@ -36,6 +36,15 @@ services:
networks:
- tipi_main_network
+ tipi-redis:
+ container_name: tipi-redis
+ image: redis:alpine
+ restart: unless-stopped
+ volumes:
+ - ./data/redis:/data
+ networks:
+ - tipi_main_network
+
api:
image: meienberger/runtipi:rc-${TIPI_VERSION}
command: /bin/sh -c "cd /api && npm run start"
diff --git a/docker-compose.yml b/docker-compose.yml
index 94a7077b..207db198 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -4,7 +4,7 @@ services:
reverse-proxy:
container_name: reverse-proxy
image: traefik:v2.8
- restart: always
+ restart: unless-stopped
ports:
- ${NGINX_PORT-80}:80
- ${NGINX_PORT_SSL-443}:443
@@ -19,7 +19,7 @@ services:
tipi-db:
container_name: tipi-db
image: postgres:14
- restart: on-failure
+ restart: unless-stopped
stop_grace_period: 1m
volumes:
- ${PWD}/data/postgres:/var/lib/postgresql/data
@@ -35,6 +35,15 @@ services:
networks:
- tipi_main_network
+ tipi-redis:
+ container_name: tipi-redis
+ image: redis:alpine
+ restart: unless-stopped
+ volumes:
+ - ./data/redis:/data
+ networks:
+ - tipi_main_network
+
api:
image: meienberger/runtipi:${TIPI_VERSION}
command: /bin/sh -c "cd /api && npm run start"
diff --git a/package.json b/package.json
index 1a5afc11..d9bfdccf 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "runtipi",
- "version": "0.7.1",
+ "version": "0.7.2",
"description": "A homeserver for everyone",
"scripts": {
"prepare": "husky install",
diff --git a/packages/dashboard/jest.config.js b/packages/dashboard/jest.config.js
index 8d9ce450..c77512f6 100644
--- a/packages/dashboard/jest.config.js
+++ b/packages/dashboard/jest.config.js
@@ -1,6 +1,5 @@
/** @type {import('ts-jest/dist/types').InitialOptionsTsJest} */
module.exports = {
- preset: 'ts-jest',
verbose: true,
// testEnvironment: 'node',
testMatch: ['**/__tests__/**/*.test.ts'],
diff --git a/packages/dashboard/package.json b/packages/dashboard/package.json
index db5812f2..48059b05 100644
--- a/packages/dashboard/package.json
+++ b/packages/dashboard/package.json
@@ -1,6 +1,6 @@
{
"name": "dashboard",
- "version": "0.7.1",
+ "version": "0.7.2",
"private": true,
"scripts": {
"test": "jest --colors",
diff --git a/packages/dashboard/src/components/Layout/Layout.tsx b/packages/dashboard/src/components/Layout/Layout.tsx
index 1ab52716..048cc8a6 100644
--- a/packages/dashboard/src/components/Layout/Layout.tsx
+++ b/packages/dashboard/src/components/Layout/Layout.tsx
@@ -1,11 +1,12 @@
import { Flex, useDisclosure, Spinner, Breadcrumb, BreadcrumbItem, useColorModeValue, Box } from '@chakra-ui/react';
import Head from 'next/head';
import Link from 'next/link';
-import React from 'react';
+import React, { useEffect } from 'react';
import { FiChevronRight } from 'react-icons/fi';
import Header from './Header';
import Menu from './SideMenu';
import MenuDrawer from './MenuDrawer';
+import { useRefreshTokenQuery } from '../../generated/graphql';
interface IProps {
loading?: boolean;
@@ -15,6 +16,13 @@ interface IProps {
const Layout: React.FC = ({ children, loading, breadcrumbs }) => {
const { isOpen, onClose, onOpen } = useDisclosure();
+ const { data } = useRefreshTokenQuery({ fetchPolicy: 'network-only' });
+
+ useEffect(() => {
+ if (data?.refreshToken?.token) {
+ localStorage.setItem('token', data.refreshToken.token);
+ }
+ }, [data]);
const menubg = useColorModeValue('#F1F3F4', '#202736');
const bg = useColorModeValue('white', '#1a202c');
diff --git a/packages/dashboard/src/components/Layout/SideMenu.tsx b/packages/dashboard/src/components/Layout/SideMenu.tsx
index 450c6aa4..617989c9 100644
--- a/packages/dashboard/src/components/Layout/SideMenu.tsx
+++ b/packages/dashboard/src/components/Layout/SideMenu.tsx
@@ -45,6 +45,11 @@ const SideMenu: React.FC = () => {
setColorMode(checked ? 'dark' : 'light');
};
+ const handleLogout = async () => {
+ localStorage.removeItem('token');
+ logout();
+ };
+
return (
@@ -64,7 +69,7 @@ const SideMenu: React.FC = () => {
Donate
- logout()} className="cursor-pointer hover:font-bold flex items-center mb-5">
+
Log out
diff --git a/packages/dashboard/src/core/apollo/client.ts b/packages/dashboard/src/core/apollo/client.ts
index d49d504e..72375b54 100644
--- a/packages/dashboard/src/core/apollo/client.ts
+++ b/packages/dashboard/src/core/apollo/client.ts
@@ -2,7 +2,7 @@ import { ApolloClient, from, InMemoryCache } from '@apollo/client';
import links from './links';
export const createApolloClient = async (url: string): Promise> => {
- const additiveLink = from([links.errorLink, links.httpLink(url)]);
+ const additiveLink = from([links.errorLink, links.authLink, links.httpLink(url)]);
return new ApolloClient({
link: additiveLink,
diff --git a/packages/dashboard/src/core/apollo/links/authLink.ts b/packages/dashboard/src/core/apollo/links/authLink.ts
new file mode 100644
index 00000000..893defd2
--- /dev/null
+++ b/packages/dashboard/src/core/apollo/links/authLink.ts
@@ -0,0 +1,14 @@
+import { setContext } from '@apollo/client/link/context';
+
+const authLink = setContext((_, { headers }) => {
+ const token = localStorage.getItem('token');
+
+ return {
+ headers: {
+ ...headers,
+ authorization: token ? `Bearer ${token}` : '',
+ },
+ };
+});
+
+export default authLink;
diff --git a/packages/dashboard/src/core/apollo/links/httpLink.ts b/packages/dashboard/src/core/apollo/links/httpLink.ts
index fd2fcb13..361266b9 100644
--- a/packages/dashboard/src/core/apollo/links/httpLink.ts
+++ b/packages/dashboard/src/core/apollo/links/httpLink.ts
@@ -3,7 +3,6 @@ import { HttpLink } from '@apollo/client';
const httpLink = (url: string) => {
return new HttpLink({
uri: `${url}/graphql`,
- credentials: 'include',
});
};
diff --git a/packages/dashboard/src/core/apollo/links/index.ts b/packages/dashboard/src/core/apollo/links/index.ts
index 8ff028b3..4c22132a 100644
--- a/packages/dashboard/src/core/apollo/links/index.ts
+++ b/packages/dashboard/src/core/apollo/links/index.ts
@@ -1,9 +1,11 @@
import errorLink from './errorLink';
import httpLink from './httpLink';
+import authLink from './authLink';
const links = {
errorLink,
httpLink,
+ authLink,
};
export default links;
diff --git a/packages/dashboard/src/generated/graphql.tsx b/packages/dashboard/src/generated/graphql.tsx
index dea4a73a..bf85be6b 100644
--- a/packages/dashboard/src/generated/graphql.tsx
+++ b/packages/dashboard/src/generated/graphql.tsx
@@ -134,9 +134,9 @@ export type ListAppsResonse = {
export type Mutation = {
__typename?: 'Mutation';
installApp: App;
- login: UserResponse;
+ login: TokenResponse;
logout: Scalars['Boolean'];
- register: UserResponse;
+ register: TokenResponse;
restart: Scalars['Boolean'];
startApp: App;
stopApp: App;
@@ -185,6 +185,7 @@ export type Query = {
isConfigured: Scalars['Boolean'];
listAppsInfo: ListAppsResonse;
me?: Maybe;
+ refreshToken?: Maybe;
systemInfo?: Maybe;
version: VersionResponse;
};
@@ -200,6 +201,11 @@ export type SystemInfoResponse = {
memory: DiskMemory;
};
+export type TokenResponse = {
+ __typename?: 'TokenResponse';
+ token: Scalars['String'];
+};
+
export type UpdateInfo = {
__typename?: 'UpdateInfo';
current: Scalars['Float'];
@@ -215,11 +221,6 @@ export type User = {
username: Scalars['String'];
};
-export type UserResponse = {
- __typename?: 'UserResponse';
- user?: Maybe;
-};
-
export type UsernamePasswordInput = {
password: Scalars['String'];
username: Scalars['String'];
@@ -241,7 +242,7 @@ export type LoginMutationVariables = Exact<{
input: UsernamePasswordInput;
}>;
-export type LoginMutation = { __typename?: 'Mutation'; login: { __typename?: 'UserResponse'; user?: { __typename?: 'User'; id: string } | null } };
+export type LoginMutation = { __typename?: 'Mutation'; login: { __typename?: 'TokenResponse'; token: string } };
export type LogoutMutationVariables = Exact<{ [key: string]: never }>;
@@ -251,7 +252,7 @@ export type RegisterMutationVariables = Exact<{
input: UsernamePasswordInput;
}>;
-export type RegisterMutation = { __typename?: 'Mutation'; register: { __typename?: 'UserResponse'; user?: { __typename?: 'User'; id: string } | null } };
+export type RegisterMutation = { __typename?: 'Mutation'; register: { __typename?: 'TokenResponse'; token: string } };
export type RestartMutationVariables = Exact<{ [key: string]: never }>;
@@ -382,6 +383,10 @@ export type MeQueryVariables = Exact<{ [key: string]: never }>;
export type MeQuery = { __typename?: 'Query'; me?: { __typename?: 'User'; id: string } | null };
+export type RefreshTokenQueryVariables = Exact<{ [key: string]: never }>;
+
+export type RefreshTokenQuery = { __typename?: 'Query'; refreshToken?: { __typename?: 'TokenResponse'; token: string } | null };
+
export type SystemInfoQueryVariables = Exact<{ [key: string]: never }>;
export type SystemInfoQuery = {
@@ -436,9 +441,7 @@ export type InstallAppMutationOptions = Apollo.BaseMutationOptions;
export type MeLazyQueryHookResult = ReturnType;
export type MeQueryResult = Apollo.QueryResult;
+export const RefreshTokenDocument = gql`
+ query RefreshToken {
+ refreshToken {
+ token
+ }
+ }
+`;
+
+/**
+ * __useRefreshTokenQuery__
+ *
+ * To run a query within a React component, call `useRefreshTokenQuery` and pass it any options that fit your needs.
+ * When your component renders, `useRefreshTokenQuery` returns an object from Apollo Client that contains loading, error, and data properties
+ * you can use to render your UI.
+ *
+ * @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options;
+ *
+ * @example
+ * const { data, loading, error } = useRefreshTokenQuery({
+ * variables: {
+ * },
+ * });
+ */
+export function useRefreshTokenQuery(baseOptions?: Apollo.QueryHookOptions) {
+ const options = { ...defaultOptions, ...baseOptions };
+ return Apollo.useQuery(RefreshTokenDocument, options);
+}
+export function useRefreshTokenLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions) {
+ const options = { ...defaultOptions, ...baseOptions };
+ return Apollo.useLazyQuery(RefreshTokenDocument, options);
+}
+export type RefreshTokenQueryHookResult = ReturnType;
+export type RefreshTokenLazyQueryHookResult = ReturnType;
+export type RefreshTokenQueryResult = Apollo.QueryResult;
export const SystemInfoDocument = gql`
query SystemInfo {
systemInfo {
diff --git a/packages/dashboard/src/graphql/mutations/login.graphql b/packages/dashboard/src/graphql/mutations/login.graphql
index 77e7b3be..f82f52af 100644
--- a/packages/dashboard/src/graphql/mutations/login.graphql
+++ b/packages/dashboard/src/graphql/mutations/login.graphql
@@ -1,7 +1,5 @@
mutation Login($input: UsernamePasswordInput!) {
login(input: $input) {
- user {
- id
- }
+ token
}
}
diff --git a/packages/dashboard/src/graphql/mutations/register.graphql b/packages/dashboard/src/graphql/mutations/register.graphql
index 96e5b858..b8f829e3 100644
--- a/packages/dashboard/src/graphql/mutations/register.graphql
+++ b/packages/dashboard/src/graphql/mutations/register.graphql
@@ -1,7 +1,5 @@
mutation Register($input: UsernamePasswordInput!) {
register(input: $input) {
- user {
- id
- }
+ token
}
}
diff --git a/packages/dashboard/src/graphql/queries/refreshToken.graphql b/packages/dashboard/src/graphql/queries/refreshToken.graphql
new file mode 100644
index 00000000..e8e0f204
--- /dev/null
+++ b/packages/dashboard/src/graphql/queries/refreshToken.graphql
@@ -0,0 +1,5 @@
+query RefreshToken {
+ refreshToken {
+ token
+ }
+}
diff --git a/packages/dashboard/src/modules/Auth/containers/Login.tsx b/packages/dashboard/src/modules/Auth/containers/Login.tsx
index 6bf21fbe..2790dc37 100644
--- a/packages/dashboard/src/modules/Auth/containers/Login.tsx
+++ b/packages/dashboard/src/modules/Auth/containers/Login.tsx
@@ -1,3 +1,4 @@
+import { useApolloClient } from '@apollo/client';
import { useToast } from '@chakra-ui/react';
import React, { useState } from 'react';
import { useLoginMutation } from '../../../generated/graphql';
@@ -7,11 +8,13 @@ import LoginForm from '../components/LoginForm';
type FormValues = { email: string; password: string };
const Login: React.FC = () => {
- const [login] = useLoginMutation({ refetchQueries: ['Me'] });
+ const client = useApolloClient();
+ const [login] = useLoginMutation({});
const [loading, setLoading] = useState(false);
const toast = useToast();
const handleError = (error: unknown) => {
+ localStorage.removeItem('token');
if (error instanceof Error) {
toast({
title: 'Error',
@@ -26,7 +29,13 @@ const Login: React.FC = () => {
const handleLogin = async (values: FormValues) => {
try {
setLoading(true);
- await login({ variables: { input: { username: values.email, password: values.password } } });
+ const { data } = await login({ variables: { input: { username: values.email, password: values.password } } });
+
+ if (data?.login?.token) {
+ localStorage.setItem('token', data.login.token);
+ }
+
+ await client.refetchQueries({ include: ['Me'] });
} catch (error) {
handleError(error);
} finally {
diff --git a/packages/dashboard/src/modules/Auth/containers/Onboarding.tsx b/packages/dashboard/src/modules/Auth/containers/Onboarding.tsx
index 13f8e8aa..a6d62a3e 100644
--- a/packages/dashboard/src/modules/Auth/containers/Onboarding.tsx
+++ b/packages/dashboard/src/modules/Auth/containers/Onboarding.tsx
@@ -1,3 +1,4 @@
+import { useApolloClient } from '@apollo/client';
import { useToast } from '@chakra-ui/react';
import React, { useState } from 'react';
import { useRegisterMutation } from '../../../generated/graphql';
@@ -5,6 +6,7 @@ import AuthFormLayout from '../components/AuthFormLayout';
import RegisterForm from '../components/RegisterForm';
const Onboarding: React.FC = () => {
+ const client = useApolloClient();
const toast = useToast();
const [register] = useRegisterMutation({ refetchQueries: ['Me'] });
const [loading, setLoading] = useState(false);
@@ -24,7 +26,13 @@ const Onboarding: React.FC = () => {
const handleRegister = async (values: { email: string; password: string }) => {
try {
setLoading(true);
- await register({ variables: { input: { username: values.email, password: values.password } } });
+ const { data } = await register({ variables: { input: { username: values.email, password: values.password } } });
+
+ if (data?.register?.token) {
+ localStorage.setItem('token', data.register.token);
+ }
+
+ await client.refetchQueries({ include: ['Me'] });
} catch (error) {
handleError(error);
} finally {
diff --git a/packages/system-api/__mocks__/redis.ts b/packages/system-api/__mocks__/redis.ts
new file mode 100644
index 00000000..efd884fe
--- /dev/null
+++ b/packages/system-api/__mocks__/redis.ts
@@ -0,0 +1,24 @@
+module.exports = {
+ createClient: jest.fn(() => {
+ const values = new Map();
+ const expirations = new Map();
+ return {
+ isOpen: true,
+ connect: jest.fn(),
+ set: (key: string, value: string, exp: number) => {
+ values.set(key, value);
+ expirations.set(key, exp);
+ },
+ get: (key: string) => {
+ return values.get(key);
+ },
+ quit: jest.fn(),
+ del: (key: string) => {
+ return values.delete(key);
+ },
+ ttl: (key: string) => {
+ return expirations.get(key);
+ },
+ };
+ }),
+};
diff --git a/packages/system-api/package.json b/packages/system-api/package.json
index 701acc78..6728789a 100644
--- a/packages/system-api/package.json
+++ b/packages/system-api/package.json
@@ -1,6 +1,6 @@
{
"name": "system-api",
- "version": "0.7.1",
+ "version": "0.7.2",
"description": "",
"exports": "./dist/server.js",
"type": "module",
@@ -33,22 +33,23 @@
"cors": "^2.8.5",
"dotenv": "^16.0.0",
"express": "^4.17.3",
- "express-session": "^1.17.3",
"fs-extra": "^10.1.0",
"graphql": "^15.3.0",
"graphql-type-json": "^0.3.2",
"http": "0.0.1-security",
"internal-ip": "^6.0.0",
+ "jsonwebtoken": "^8.5.1",
"node-cache": "^5.1.2",
"node-cron": "^3.0.1",
"node-port-scanner": "^3.0.1",
"pg": "^8.7.3",
+ "redis": "^4.3.1",
"reflect-metadata": "^0.1.13",
"semver": "^7.3.7",
- "session-file-store": "^1.5.0",
"tcp-port-used": "^1.0.2",
"type-graphql": "^1.1.1",
"typeorm": "^0.3.6",
+ "uuid": "^9.0.0",
"validator": "^13.7.0",
"winston": "^3.7.2",
"zod": "^3.19.1"
@@ -59,15 +60,15 @@
"@swc/core": "^1.2.210",
"@types/cors": "^2.8.12",
"@types/express": "^4.17.13",
- "@types/express-session": "^1.17.4",
"@types/fs-extra": "^9.0.13",
"@types/jest": "^27.5.0",
+ "@types/jsonwebtoken": "^8.5.9",
"@types/node": "17.0.31",
"@types/node-cron": "^3.0.2",
"@types/pg": "^8.6.5",
"@types/semver": "^7.3.12",
- "@types/session-file-store": "^1.2.2",
"@types/tcp-port-used": "^1.0.1",
+ "@types/uuid": "^8.3.4",
"@types/validator": "^13.7.2",
"@typescript-eslint/eslint-plugin": "^5.18.0",
"@typescript-eslint/parser": "^5.22.0",
diff --git a/packages/system-api/src/config/TipiCache.ts b/packages/system-api/src/config/TipiCache.ts
index 2531f526..2d3407b7 100644
--- a/packages/system-api/src/config/TipiCache.ts
+++ b/packages/system-api/src/config/TipiCache.ts
@@ -1,5 +1,61 @@
-import cache from 'node-cache';
+import { createClient, RedisClientType } from 'redis';
+import { getConfig } from '../core/config/TipiConfig';
-const TipiCache = new cache({ stdTTL: 7200 });
+const ONE_DAY_IN_SECONDS = 60 * 60 * 24;
-export default TipiCache;
+class TipiCache {
+ private static instance: TipiCache;
+
+ private client: RedisClientType;
+
+ constructor() {
+ const client = createClient({
+ url: `redis://${getConfig().REDIS_HOST}:6379`,
+ });
+
+ this.client = client as RedisClientType;
+ }
+
+ public static getInstance(): TipiCache {
+ if (!TipiCache.instance) {
+ TipiCache.instance = new TipiCache();
+ }
+
+ return TipiCache.instance;
+ }
+
+ private async getClient(): Promise {
+ if (!this.client.isOpen) {
+ await this.client.connect();
+ }
+ return this.client;
+ }
+
+ public async set(key: string, value: string, expiration = ONE_DAY_IN_SECONDS) {
+ const client = await this.getClient();
+ return client.set(key, value, {
+ EX: expiration,
+ });
+ }
+
+ public async get(key: string) {
+ const client = await this.getClient();
+ return client.get(key);
+ }
+
+ public async del(key: string) {
+ const client = await this.getClient();
+ return client.del(key);
+ }
+
+ public async close() {
+ return this.client.quit();
+ }
+
+ public async ttl(key: string) {
+ const client = await this.getClient();
+ return client.ttl(key);
+ }
+}
+
+export default TipiCache.getInstance();
diff --git a/packages/system-api/src/core/config/TipiConfig.ts b/packages/system-api/src/core/config/TipiConfig.ts
index 0346a604..38c07ce3 100644
--- a/packages/system-api/src/core/config/TipiConfig.ts
+++ b/packages/system-api/src/core/config/TipiConfig.ts
@@ -22,11 +22,13 @@ const {
APPS_REPO_URL = '',
DOMAIN = '',
STORAGE_PATH = '/runtipi',
+ REDIS_HOST = 'tipi-redis',
ARCHITECTURE = 'amd64',
} = process.env;
const configSchema = z.object({
NODE_ENV: z.union([z.literal('development'), z.literal('production'), z.literal('test')]),
+ REDIS_HOST: z.string(),
status: z.union([z.literal('RUNNING'), z.literal('UPDATING'), z.literal('RESTARTING')]),
architecture: z.nativeEnum(AppSupportedArchitecturesEnum),
logs: z.object({
@@ -58,6 +60,7 @@ class Config {
LOGS_APP,
LOGS_ERROR,
},
+ REDIS_HOST,
NODE_ENV: NODE_ENV as z.infer['NODE_ENV'],
architecture: ARCHITECTURE as z.infer['architecture'],
rootFolder: '/runtipi',
diff --git a/packages/system-api/src/core/middlewares/__tests__/sessionMiddleware.test.ts b/packages/system-api/src/core/middlewares/__tests__/sessionMiddleware.test.ts
new file mode 100644
index 00000000..0b6d3dd6
--- /dev/null
+++ b/packages/system-api/src/core/middlewares/__tests__/sessionMiddleware.test.ts
@@ -0,0 +1,75 @@
+import { faker } from '@faker-js/faker';
+import jwt from 'jsonwebtoken';
+import TipiCache from '../../../config/TipiCache';
+import { getConfig } from '../../config/TipiConfig';
+import getSessionMiddleware from '../sessionMiddleware';
+
+describe('SessionMiddleware', () => {
+ it('Should append session to request object if a valid token is present', async () => {
+ // Arrange
+ const session = faker.random.alphaNumeric(32);
+ const userId = faker.datatype.number();
+ await TipiCache.set(session, userId.toString());
+ const token = jwt.sign({ id: userId, session }, getConfig().jwtSecret);
+ const req = {
+ headers: {
+ authorization: `Bearer ${token}`,
+ },
+ } as any;
+ const next = jest.fn();
+ const res = {} as any;
+
+ // Act
+ await getSessionMiddleware(req, res, next);
+
+ // Assert
+ expect(req).toHaveProperty('session');
+ expect(req.session).toHaveProperty('id');
+ expect(req.session).toHaveProperty('userId');
+ expect(req.session.id).toBe(session);
+ expect(req.session.userId).toBe(userId);
+ expect(next).toHaveBeenCalled();
+ });
+
+ it('Should not append session to request object if a invalid token is present', async () => {
+ // Arrange
+ const session = faker.random.alphaNumeric(32);
+ const userId = faker.datatype.number();
+ await TipiCache.set(session, userId.toString());
+ const token = jwt.sign({ id: userId, session }, 'invalidSecret');
+ const req = {
+ headers: {
+ authorization: `Bearer ${token}`,
+ },
+ } as any;
+ const next = jest.fn();
+ const res = {} as any;
+
+ // Act
+ await getSessionMiddleware(req, res, next);
+
+ // Assert
+ expect(req).toHaveProperty('session');
+ expect(req.session).not.toHaveProperty('id');
+ expect(req.session).not.toHaveProperty('userId');
+ expect(next).toHaveBeenCalled();
+ });
+
+ it('Should not append session to request object if a token is not present', async () => {
+ // Arrange
+ const req = {
+ headers: {},
+ } as any;
+ const next = jest.fn();
+ const res = {} as any;
+
+ // Act
+ await getSessionMiddleware(req, res, next);
+
+ // Assert
+ expect(req).toHaveProperty('session');
+ expect(req.session).not.toHaveProperty('id');
+ expect(req.session).not.toHaveProperty('userId');
+ expect(next).toHaveBeenCalled();
+ });
+});
diff --git a/packages/system-api/src/core/middlewares/sessionMiddleware.ts b/packages/system-api/src/core/middlewares/sessionMiddleware.ts
index 3c9ad6c6..684ac51d 100644
--- a/packages/system-api/src/core/middlewares/sessionMiddleware.ts
+++ b/packages/system-api/src/core/middlewares/sessionMiddleware.ts
@@ -1,21 +1,32 @@
-import session from 'express-session';
-import SessionFileStore from 'session-file-store';
-import { COOKIE_MAX_AGE, __prod__ } from '../../config/constants/constants';
+import { NextFunction, Request, Response } from 'express';
+import jwt from 'jsonwebtoken';
+import logger from '../../config/logger/logger';
+import TipiCache from '../../config/TipiCache';
import { getConfig } from '../config/TipiConfig';
-const getSessionMiddleware = () => {
- const FileStore = SessionFileStore(session);
+const getSessionMiddleware = async (req: Request, _: Response, next: NextFunction) => {
+ req.session = {};
- const sameSite = __prod__ ? 'lax' : 'none';
+ const token = req.headers.authorization?.split(' ')[1];
- return session({
- name: 'qid',
- store: new FileStore(),
- cookie: { maxAge: COOKIE_MAX_AGE, secure: false, sameSite, httpOnly: true },
- secret: getConfig().jwtSecret,
- resave: false,
- saveUninitialized: false,
- });
+ if (token) {
+ try {
+ const decodedToken = jwt.verify(token, getConfig().jwtSecret) as { id: number; session: string };
+
+ const userId = await TipiCache.get(decodedToken.session);
+
+ if (userId === decodedToken.id.toString()) {
+ req.session = {
+ userId: decodedToken.id,
+ id: decodedToken.session,
+ };
+ }
+ } catch (err) {
+ logger.error(err);
+ }
+ }
+
+ next();
};
export default getSessionMiddleware;
diff --git a/packages/system-api/src/declarations.d.ts b/packages/system-api/src/declarations.d.ts
new file mode 100644
index 00000000..2decf771
--- /dev/null
+++ b/packages/system-api/src/declarations.d.ts
@@ -0,0 +1,9 @@
+declare namespace Express {
+ interface Request {
+ session: {
+ userId?: number;
+ id?: string;
+ };
+ [key: string]: any;
+ }
+}
diff --git a/packages/system-api/src/helpers/helpers.ts b/packages/system-api/src/helpers/helpers.ts
index 1bfccb92..2ca3571b 100644
--- a/packages/system-api/src/helpers/helpers.ts
+++ b/packages/system-api/src/helpers/helpers.ts
@@ -1,3 +1,3 @@
-const objectKeys = (obj: T): (keyof T)[] => Object.keys(obj) as (keyof T)[];
+const objectKeys = (obj: T): (keyof T)[] => Object.keys(obj) as (keyof T)[];
export default { objectKeys };
diff --git a/packages/system-api/src/modules/apps/__tests__/apps.service.test.ts b/packages/system-api/src/modules/apps/__tests__/apps.service.test.ts
index 472b2c15..1da8d02e 100644
--- a/packages/system-api/src/modules/apps/__tests__/apps.service.test.ts
+++ b/packages/system-api/src/modules/apps/__tests__/apps.service.test.ts
@@ -497,7 +497,7 @@ describe('List apps', () => {
it('Should list apps that have no supportedArchitectures specified', async () => {
// Arrange
setConfig('architecture', AppSupportedArchitecturesEnum.ARM);
- const app3 = await createApp({});
+ const app3 = await createApp({ supportedArchitectures: undefined });
// @ts-ignore
fs.__createMockFiles(Object.assign(app3.MockFiles));
diff --git a/packages/system-api/src/modules/apps/apps.service.ts b/packages/system-api/src/modules/apps/apps.service.ts
index e688e46d..75999236 100644
--- a/packages/system-api/src/modules/apps/apps.service.ts
+++ b/packages/system-api/src/modules/apps/apps.service.ts
@@ -159,19 +159,16 @@ const listApps = async (): Promise => {
const apps: AppInfo[] = folders
.map((app) => {
- try {
- return readJsonFile(`/runtipi/repos/${getConfig().appsRepoId}/apps/${app}/config.json`);
- } catch (e) {
- return null;
- }
+ return readJsonFile(`/runtipi/repos/${getConfig().appsRepoId}/apps/${app}/config.json`);
})
.filter(Boolean);
- apps.forEach((app) => {
- app.description = readFile(`/runtipi/repos/${getConfig().appsRepoId}/apps/${app.id}/metadata/description.md`);
+ const filteredApps = filterApps(apps).map((app) => {
+ const description = readFile(`/runtipi/repos/${getConfig().appsRepoId}/apps/${app.id}/metadata/description.md`);
+ return { ...app, description };
});
- return { apps: filterApps(apps), total: apps.length };
+ return { apps: filteredApps, total: apps.length };
};
/**
diff --git a/packages/system-api/src/modules/auth/__tests__/auth.resolver.test.ts b/packages/system-api/src/modules/auth/__tests__/auth.resolver.test.ts
index f593023a..c16777f9 100644
--- a/packages/system-api/src/modules/auth/__tests__/auth.resolver.test.ts
+++ b/packages/system-api/src/modules/auth/__tests__/auth.resolver.test.ts
@@ -1,13 +1,18 @@
import { faker } from '@faker-js/faker';
+import jwt from 'jsonwebtoken';
import { DataSource } from 'typeorm';
+import TipiCache from '../../../config/TipiCache';
+import { getConfig } from '../../../core/config/TipiConfig';
import { setupConnection, teardownConnection } from '../../../test/connection';
import { gcall } from '../../../test/gcall';
import { loginMutation, registerMutation } from '../../../test/mutations';
-import { isConfiguredQuery, MeQuery } from '../../../test/queries';
+import { isConfiguredQuery, MeQuery, refreshTokenQuery } from '../../../test/queries';
import User from '../../auth/user.entity';
-import { UserResponse } from '../auth.types';
+import { TokenResponse } from '../auth.types';
import { createUser } from './user.factory';
+jest.mock('redis');
+
let db: DataSource | null = null;
const TEST_SUITE = 'authresolver';
@@ -58,20 +63,30 @@ describe('Test: register', () => {
const password = faker.internet.password();
it('should register a user', async () => {
- const { data } = await gcall<{ register: UserResponse }>({
+ const { data } = await gcall<{ register: TokenResponse }>({
source: registerMutation,
variableValues: {
input: { username: email, password },
},
});
- expect(data?.register.user?.username).toEqual(email.toLowerCase());
+ expect(data?.register).toBeDefined();
+ expect(data?.register?.token).toBeDefined();
+
+ const decoded = jwt.verify(data?.register?.token || '', getConfig().jwtSecret) as jwt.JwtPayload;
+
+ expect(decoded).toBeDefined();
+ expect(decoded).not.toBeNull();
+ expect(decoded).toHaveProperty('id');
+ expect(decoded).toHaveProperty('iat');
+ expect(decoded).toHaveProperty('exp');
+ expect(decoded).toHaveProperty('session');
});
it('should not register a user with an existing username', async () => {
await createUser(email);
- const { errors } = await gcall<{ register: UserResponse }>({
+ const { errors } = await gcall<{ register: TokenResponse }>({
source: registerMutation,
variableValues: {
input: { username: email, password },
@@ -82,7 +97,7 @@ describe('Test: register', () => {
});
it('should not register a user with a malformed email', async () => {
- const { errors } = await gcall<{ register: UserResponse }>({
+ const { errors } = await gcall<{ register: TokenResponse }>({
source: registerMutation,
variableValues: {
input: { username: 'not an email', password },
@@ -101,18 +116,27 @@ describe('Test: login', () => {
});
it('should login a user', async () => {
- const { data } = await gcall<{ login: UserResponse }>({
+ const { data } = await gcall<{ login: TokenResponse }>({
source: loginMutation,
variableValues: {
input: { username: email, password: 'password' },
},
});
- expect(data?.login.user?.username).toEqual(email.toLowerCase());
+ const token = data?.login.token as string;
+
+ expect(token).toBeDefined();
+
+ const decoded = jwt.verify(token, getConfig().jwtSecret) as { id: string; session: string };
+
+ const user = await User.findOne({ where: { username: email.toLowerCase().trim() } });
+
+ expect(decoded.id).toBeDefined();
+ expect(user?.id).toEqual(decoded.id);
});
it('should not login a user with an incorrect password', async () => {
- const { errors } = await gcall<{ login: UserResponse }>({
+ const { errors } = await gcall<{ login: TokenResponse }>({
source: loginMutation,
variableValues: {
input: { username: email, password: 'wrong password' },
@@ -123,7 +147,7 @@ describe('Test: login', () => {
});
it('should not login a user with a malformed email', async () => {
- const { errors } = await gcall<{ login: UserResponse }>({
+ const { errors } = await gcall<{ login: TokenResponse }>({
source: loginMutation,
variableValues: {
input: { username: 'not an email', password: 'password' },
@@ -146,6 +170,7 @@ describe('Test: logout', () => {
const { data } = await gcall<{ logout: boolean }>({
source: 'mutation { logout }',
userId: user1.id,
+ session: 'session',
});
expect(data?.logout).toBeTruthy();
@@ -171,3 +196,39 @@ describe('Test: isConfigured', () => {
expect(data?.isConfigured).toBeTruthy();
});
});
+
+describe('Test: refreshToken', () => {
+ const email = faker.internet.email();
+ let user1: User;
+
+ beforeEach(async () => {
+ user1 = await createUser(email);
+ });
+
+ it('should return a new token', async () => {
+ // Arrange
+ const session = faker.datatype.uuid();
+ await TipiCache.set(session, user1.id.toString());
+
+ // Act
+ const { data } = await gcall<{ refreshToken: TokenResponse }>({
+ source: refreshTokenQuery,
+ userId: user1.id,
+ session: session,
+ });
+ const decoded = jwt.verify(data?.refreshToken?.token || '', getConfig().jwtSecret) as jwt.JwtPayload;
+
+ // Assert
+ expect(data?.refreshToken).toBeDefined();
+ expect(data?.refreshToken?.token).toBeDefined();
+ expect(decoded).toBeDefined();
+ expect(decoded).not.toBeNull();
+ expect(decoded).toHaveProperty('id');
+ expect(decoded).toHaveProperty('iat');
+ expect(decoded).toHaveProperty('exp');
+ expect(decoded).toHaveProperty('session');
+
+ expect(decoded.id).toEqual(user1.id.toString());
+ expect(decoded.session).not.toEqual(session);
+ });
+});
diff --git a/packages/system-api/src/modules/auth/__tests__/auth.service.test.ts b/packages/system-api/src/modules/auth/__tests__/auth.service.test.ts
index c9703677..384270bb 100644
--- a/packages/system-api/src/modules/auth/__tests__/auth.service.test.ts
+++ b/packages/system-api/src/modules/auth/__tests__/auth.service.test.ts
@@ -1,15 +1,21 @@
import * as argon2 from 'argon2';
+import jwt from 'jsonwebtoken';
import AuthService from '../auth.service';
import { createUser } from './user.factory';
import User from '../user.entity';
import { faker } from '@faker-js/faker';
import { setupConnection, teardownConnection } from '../../../test/connection';
import { DataSource } from 'typeorm';
+import { setConfig } from '../../../core/config/TipiConfig';
+import TipiCache from '../../../config/TipiCache';
let db: DataSource | null = null;
const TEST_SUITE = 'authservice';
+jest.mock('redis');
+
beforeAll(async () => {
+ setConfig('jwtSecret', 'test');
db = await setupConnection(TEST_SUITE);
});
@@ -23,14 +29,24 @@ afterAll(async () => {
});
describe('Login', () => {
- it('Should return user after login', async () => {
+ it('Should return a valid jsonwebtoken containing a user id', async () => {
+ // Arrange
const email = faker.internet.email();
- await createUser(email);
+ const user = await createUser(email);
- const { user } = await AuthService.login({ username: email, password: 'password' });
+ // Act
+ const { token } = await AuthService.login({ username: email, password: 'password' });
+ const decoded = jwt.verify(token, 'test') as jwt.JwtPayload;
- expect(user).toBeDefined();
- expect(user?.id).toBe(1);
+ // Assert
+ expect(decoded).toBeDefined();
+ expect(decoded).toBeDefined();
+ expect(decoded).not.toBeNull();
+ expect(decoded).toHaveProperty('id');
+ expect(decoded.id).toBe(user.id);
+ expect(decoded).toHaveProperty('iat');
+ expect(decoded).toHaveProperty('exp');
+ expect(decoded).toHaveProperty('session');
});
it('Should throw if user does not exist', async () => {
@@ -45,26 +61,41 @@ describe('Login', () => {
});
describe('Register', () => {
- it('Should return new user after register', async () => {
+ it('Should return valid jsonwebtoken after register', async () => {
+ // Arrange
const email = faker.internet.email();
- const { user } = await AuthService.register({ username: email, password: 'test' });
- expect(user).toBeDefined();
+ // Act
+ const { token } = await AuthService.register({ username: email, password: 'password' });
+ const decoded = jwt.verify(token, 'test') as jwt.JwtPayload;
+
+ // Assert
+ expect(decoded).toBeDefined();
+ expect(decoded).not.toBeNull();
+ expect(decoded).toHaveProperty('id');
+ expect(decoded).toHaveProperty('iat');
+ expect(decoded).toHaveProperty('exp');
+ expect(decoded).toHaveProperty('session');
});
it('Should correctly trim and lowercase email', async () => {
+ // Arrange
const email = faker.internet.email();
- await AuthService.register({ username: email, password: 'test' });
+ // Act
+ await AuthService.register({ username: email, password: 'test' });
const user = await User.findOne({ where: { username: email.toLowerCase().trim() } });
+ // Assert
expect(user).toBeDefined();
expect(user?.username).toBe(email.toLowerCase().trim());
});
it('Should throw if user already exists', async () => {
+ // Arrange
const email = faker.internet.email();
+ // Act & Assert
await createUser(email);
await expect(AuthService.register({ username: email, password: 'test' })).rejects.toThrowError('User already exists');
});
@@ -78,11 +109,126 @@ describe('Register', () => {
});
it('Password is correctly hashed', async () => {
- const email = faker.internet.email();
- const { user } = await AuthService.register({ username: email, password: 'test' });
+ // Arrange
+ const email = faker.internet.email().toLowerCase().trim();
+ // Act
+ await AuthService.register({ username: email, password: 'test' });
+ const user = await User.findOne({ where: { username: email } });
const isPasswordValid = await argon2.verify(user?.password || '', 'test');
+ // Assert
expect(isPasswordValid).toBe(true);
});
+
+ it('Should throw if email is invalid', async () => {
+ await expect(AuthService.register({ username: 'test', password: 'test' })).rejects.toThrowError('Invalid username');
+ });
+});
+
+describe('Test: logout', () => {
+ it('Should return true if there is no session to delete', async () => {
+ // Act
+ // @ts-ignore
+ const result = await AuthService.logout();
+
+ // Assert
+ expect(result).toBe(true);
+ });
+
+ it('Should delete session from cache', async () => {
+ // Arrange
+ const session = faker.random.alphaNumeric(32);
+ await TipiCache.set(session, 'test');
+ expect(await TipiCache.get(session)).toBe('test');
+
+ // Act
+ const result = await AuthService.logout(session);
+
+ // Assert
+ expect(result).toBe(true);
+ expect(await TipiCache.get('session')).toBeUndefined();
+ });
+});
+
+describe('Test: refreshToken', () => {
+ it('Should return null if session is not provided', async () => {
+ // Act
+ const result = await AuthService.refreshToken();
+
+ // Assert
+ expect(result).toBeNull();
+ });
+
+ it('Should return null if session is not found in cache', async () => {
+ // Act
+ const result = await AuthService.refreshToken('test');
+
+ // Assert
+ expect(result).toBeNull();
+ });
+
+ it('Should return a new token if session is found in cache', async () => {
+ // Arrange
+ const session = faker.random.alphaNumeric(32);
+ await TipiCache.set(session, 'test');
+
+ // Act
+ const result = await AuthService.refreshToken(session);
+
+ // Assert
+ expect(result).not.toBeNull();
+ expect(result).toHaveProperty('token');
+ expect(result?.token).not.toBe(session);
+ });
+
+ it('Should put expiration in 6 seconds for old session', async () => {
+ // Arrange
+ const session = faker.random.alphaNumeric(32);
+ await TipiCache.set(session, '1');
+
+ // Act
+ const result = await AuthService.refreshToken(session);
+ const expiration = await TipiCache.ttl(session);
+
+ // Assert
+ expect(result).not.toBeNull();
+ expect(result).toHaveProperty('token');
+ expect(result?.token).not.toBe(session);
+ expect(expiration).toMatchObject({ EX: 6 });
+ });
+});
+
+describe('Test: me', () => {
+ it('Should return null if userId is not provided', async () => {
+ // Act
+ const result = await AuthService.me();
+
+ // Assert
+ expect(result).toBeNull();
+ });
+
+ it('Should return null if user does not exist', async () => {
+ // Act
+ const result = await AuthService.me(1);
+
+ // Assert
+ expect(result).toBeNull();
+ });
+
+ it('Should return user if user exists', async () => {
+ // Arrange
+ const email = faker.internet.email();
+ const user = await createUser(email);
+
+ // Act
+ const result = await AuthService.me(user.id);
+
+ // Assert
+ expect(result).not.toBeNull();
+ expect(result).toHaveProperty('id');
+ expect(result).toHaveProperty('username');
+ expect(result).toHaveProperty('createdAt');
+ expect(result).toHaveProperty('updatedAt');
+ });
});
diff --git a/packages/system-api/src/modules/auth/auth.resolver.ts b/packages/system-api/src/modules/auth/auth.resolver.ts
index 493b5898..47c49ff7 100644
--- a/packages/system-api/src/modules/auth/auth.resolver.ts
+++ b/packages/system-api/src/modules/auth/auth.resolver.ts
@@ -1,6 +1,6 @@
import { Arg, Ctx, Mutation, Query, Resolver } from 'type-graphql';
import { MyContext } from '../../types';
-import { UsernamePasswordInput, UserResponse } from './auth.types';
+import { TokenResponse, UsernamePasswordInput } from './auth.types';
import AuthService from './auth.service';
import User from './user.entity';
@@ -9,34 +9,31 @@ import User from './user.entity';
export default class AuthResolver {
@Query(() => User, { nullable: true })
async me(@Ctx() ctx: MyContext): Promise {
- return AuthService.me(ctx.req.session.userId);
+ return AuthService.me(ctx.req?.session?.userId);
}
- @Mutation(() => UserResponse)
- async register(@Arg('input', () => UsernamePasswordInput) input: UsernamePasswordInput, @Ctx() { req }: MyContext): Promise {
- const { user } = await AuthService.register(input);
+ @Mutation(() => TokenResponse)
+ async register(@Arg('input', () => UsernamePasswordInput) input: UsernamePasswordInput): Promise {
+ const { token } = await AuthService.register(input);
- if (user) {
- req.session.userId = user.id;
- }
-
- return { user };
+ return { token };
}
- @Mutation(() => UserResponse)
- async login(@Arg('input', () => UsernamePasswordInput) input: UsernamePasswordInput, @Ctx() { req }: MyContext): Promise {
- const { user } = await AuthService.login(input);
+ @Mutation(() => TokenResponse)
+ async login(@Arg('input', () => UsernamePasswordInput) input: UsernamePasswordInput): Promise {
+ const { token } = await AuthService.login(input);
- if (user) {
- req.session.userId = user.id;
- }
-
- return { user };
+ return { token };
}
@Mutation(() => Boolean)
- logout(@Ctx() { req }: MyContext): boolean {
+ async logout(@Ctx() { req }: MyContext): Promise {
+ if (req.session.id) {
+ await AuthService.logout(req.session?.id);
+ }
+
req.session.userId = undefined;
+ req.session.id = undefined;
return true;
}
@@ -47,4 +44,11 @@ export default class AuthResolver {
return users.length > 0;
}
+
+ @Query(() => TokenResponse, { nullable: true })
+ async refreshToken(@Ctx() { req }: MyContext): Promise {
+ const res = await AuthService.refreshToken(req.session?.id);
+
+ return res;
+ }
}
diff --git a/packages/system-api/src/modules/auth/auth.service.ts b/packages/system-api/src/modules/auth/auth.service.ts
index f69208dd..208f89e2 100644
--- a/packages/system-api/src/modules/auth/auth.service.ts
+++ b/packages/system-api/src/modules/auth/auth.service.ts
@@ -1,9 +1,13 @@
import * as argon2 from 'argon2';
+import { v4 } from 'uuid';
+import jwt from 'jsonwebtoken';
import validator from 'validator';
-import { UsernamePasswordInput, UserResponse } from './auth.types';
+import { getConfig } from '../../core/config/TipiConfig';
+import { TokenResponse, UsernamePasswordInput } from './auth.types';
import User from './user.entity';
+import TipiCache from '../../config/TipiCache';
-const login = async (input: UsernamePasswordInput): Promise => {
+const login = async (input: UsernamePasswordInput): Promise => {
const { password, username } = input;
const user = await User.findOne({ where: { username: username.trim().toLowerCase() } });
@@ -18,10 +22,15 @@ const login = async (input: UsernamePasswordInput): Promise => {
throw new Error('Wrong password');
}
- return { user };
+ const session = v4();
+ const token = jwt.sign({ id: user.id, session }, getConfig().jwtSecret, { expiresIn: '7d' });
+
+ await TipiCache.set(session, user.id.toString());
+
+ return { token };
};
-const register = async (input: UsernamePasswordInput): Promise => {
+const register = async (input: UsernamePasswordInput): Promise => {
const { password, username } = input;
const email = username.trim().toLowerCase();
@@ -42,7 +51,12 @@ const register = async (input: UsernamePasswordInput): Promise =>
const hash = await argon2.hash(password);
const newUser = await User.create({ username: email, password: hash }).save();
- return { user: newUser };
+ const session = v4();
+ const token = jwt.sign({ id: newUser.id, session }, getConfig().jwtSecret, { expiresIn: '1d' });
+
+ await TipiCache.set(session, newUser.id.toString());
+
+ return { token };
};
const me = async (userId?: number): Promise => {
@@ -55,10 +69,34 @@ const me = async (userId?: number): Promise => {
return user;
};
+const logout = async (session: string): Promise => {
+ await TipiCache.del(session);
+
+ return true;
+};
+
+const refreshToken = async (session?: string): Promise => {
+ if (!session) return null;
+
+ const userId = await TipiCache.get(session);
+ if (!userId) return null;
+
+ // Expire token in 6 seconds
+ await TipiCache.set(session, userId, 6);
+
+ const newSession = v4();
+ const token = jwt.sign({ id: userId, session: newSession }, getConfig().jwtSecret, { expiresIn: '1d' });
+ await TipiCache.set(newSession, userId);
+
+ return { token };
+};
+
const AuthService = {
login,
register,
me,
+ logout,
+ refreshToken,
};
export default AuthService;
diff --git a/packages/system-api/src/modules/auth/auth.types.ts b/packages/system-api/src/modules/auth/auth.types.ts
index 11f85e0c..36986f2b 100644
--- a/packages/system-api/src/modules/auth/auth.types.ts
+++ b/packages/system-api/src/modules/auth/auth.types.ts
@@ -11,9 +11,9 @@ class UsernamePasswordInput {
}
@ObjectType()
-class UserResponse {
- @Field(() => User, { nullable: true })
- user?: User;
+class TokenResponse {
+ @Field(() => String, { nullable: false })
+ token!: string;
}
-export { UsernamePasswordInput, UserResponse };
+export { UsernamePasswordInput, TokenResponse };
diff --git a/packages/system-api/src/modules/system/__tests__/system.resolver.test.ts b/packages/system-api/src/modules/system/__tests__/system.resolver.test.ts
index e684080e..9393d485 100644
--- a/packages/system-api/src/modules/system/__tests__/system.resolver.test.ts
+++ b/packages/system-api/src/modules/system/__tests__/system.resolver.test.ts
@@ -16,6 +16,7 @@ import EventDispatcher from '../../../core/config/EventDispatcher';
jest.mock('fs-extra');
jest.mock('axios');
+jest.mock('redis');
beforeEach(async () => {
jest.resetModules();
@@ -42,8 +43,6 @@ beforeEach(async () => {
});
describe('Test: systemInfo', () => {
- beforeEach(async () => {});
-
it('Should return correct system info from file', async () => {
const systemInfo = {
cpu: { load: 10 },
diff --git a/packages/system-api/src/modules/system/__tests__/system.service.test.ts b/packages/system-api/src/modules/system/__tests__/system.service.test.ts
index 1a81f825..ead6b4c6 100644
--- a/packages/system-api/src/modules/system/__tests__/system.service.test.ts
+++ b/packages/system-api/src/modules/system/__tests__/system.service.test.ts
@@ -10,6 +10,7 @@ import EventDispatcher from '../../../core/config/EventDispatcher';
jest.mock('fs-extra');
jest.mock('axios');
+jest.mock('redis');
beforeEach(async () => {
jest.resetModules();
diff --git a/packages/system-api/src/modules/system/system.service.ts b/packages/system-api/src/modules/system/system.service.ts
index f824f3ef..a1c833ee 100644
--- a/packages/system-api/src/modules/system/system.service.ts
+++ b/packages/system-api/src/modules/system/system.service.ts
@@ -37,16 +37,15 @@ const systemInfo = (): z.infer => {
const getVersion = async (): Promise<{ current: string; latest?: string }> => {
try {
- let version = TipiCache.get('latestVersion');
+ let version = await TipiCache.get('latestVersion');
if (!version) {
const { data } = await axios.get('https://api.github.com/repos/meienberger/runtipi/releases/latest');
- TipiCache.set('latestVersion', data.name);
version = data.name.replace('v', '');
}
- TipiCache.set('latestVersion', version?.replace('v', ''));
+ await TipiCache.set('latestVersion', version?.replace('v', '') || '');
return { current: getConfig().version, latest: version?.replace('v', '') };
} catch (e) {
diff --git a/packages/system-api/src/server.ts b/packages/system-api/src/server.ts
index 63b833ab..c6e29373 100644
--- a/packages/system-api/src/server.ts
+++ b/packages/system-api/src/server.ts
@@ -9,7 +9,6 @@ import logger from './config/logger/logger';
import getSessionMiddleware from './core/middlewares/sessionMiddleware';
import { MyContext } from './types';
import { __prod__ } from './config/constants/constants';
-import cors from 'cors';
import datasource from './config/datasource';
import appsService from './modules/apps/apps.service';
import { runUpdates } from './core/updates/run';
@@ -19,28 +18,7 @@ import { applyJsonConfig, getConfig, setConfig } from './core/config/TipiConfig'
import { ZodError } from 'zod';
import systemController from './modules/system/system.controller';
import { eventDispatcher, EventTypes } from './core/config/EventDispatcher';
-
-let corsOptions = {
- credentials: true,
- origin: function (origin: any, callback: any) {
- if (!__prod__) {
- return callback(null, true);
- }
- // disallow requests with no origin
- if (!origin) {
- logger.error('No origin');
- return callback(new Error('Not allowed by CORS'), false);
- }
-
- if (getConfig().clientUrls.includes(origin)) {
- return callback(null, true);
- }
-
- logger.error(`Origin ${origin} not allowed by CORS`);
- const message = "The CORS policy for this origin doesn't allow access from the particular origin.";
- return callback(new Error(message), false);
- },
-};
+import cors from 'cors';
const applyCustomConfig = () => {
try {
@@ -64,9 +42,9 @@ const main = async () => {
const port = 3001;
app.use(express.static(`${getConfig().rootFolder}/repos/${getConfig().appsRepoId}`));
+ app.use(cors());
app.use('/status', systemController.status);
- app.use(cors(corsOptions));
- app.use(getSessionMiddleware());
+ app.use(getSessionMiddleware);
await datasource.initialize();
@@ -75,7 +53,7 @@ const main = async () => {
const plugins = [ApolloLogs];
if (!__prod__) {
- plugins.push(Playground({ settings: { 'request.credentials': 'include' } }));
+ plugins.push(Playground());
}
const apolloServer = new ApolloServer({
@@ -85,7 +63,7 @@ const main = async () => {
});
await apolloServer.start();
- apolloServer.applyMiddleware({ app, cors: corsOptions });
+ apolloServer.applyMiddleware({ app });
try {
await datasource.runMigrations();
diff --git a/packages/system-api/src/test/gcall.ts b/packages/system-api/src/test/gcall.ts
index fae2fd7c..8fe9dcf9 100644
--- a/packages/system-api/src/test/gcall.ts
+++ b/packages/system-api/src/test/gcall.ts
@@ -8,11 +8,12 @@ interface Options {
[key: string]: any;
}>;
userId?: number;
+ session?: string;
}
let schema: GraphQLSchema | null = null;
-export const gcall = async ({ source, variableValues, userId }: Options): Promise> => {
+export const gcall = async ({ source, variableValues, userId, session }: Options): Promise> => {
if (!schema) {
schema = await createSchema();
}
@@ -21,6 +22,6 @@ export const gcall = async ({ source, variableValues, userId }: Options): Pro
schema,
source,
variableValues,
- contextValue: { req: { session: { userId } } },
+ contextValue: { req: { session: { userId, id: session } } },
}) as any;
};
diff --git a/packages/system-api/src/test/mutations/login.graphql b/packages/system-api/src/test/mutations/login.graphql
index 126c2e4f..43b8317f 100644
--- a/packages/system-api/src/test/mutations/login.graphql
+++ b/packages/system-api/src/test/mutations/login.graphql
@@ -1,9 +1,6 @@
# Write your query or mutation here
mutation Login($input: UsernamePasswordInput!) {
login(input: $input) {
- user {
- id
- username
- }
+ token
}
}
diff --git a/packages/system-api/src/test/mutations/register.graphql b/packages/system-api/src/test/mutations/register.graphql
index 3e32ef0e..249c2c37 100644
--- a/packages/system-api/src/test/mutations/register.graphql
+++ b/packages/system-api/src/test/mutations/register.graphql
@@ -1,9 +1,6 @@
# Write your query or mutation here
mutation Register($input: UsernamePasswordInput!) {
register(input: $input) {
- user {
- id
- username
- }
+ token
}
}
diff --git a/packages/system-api/src/test/queries/index.ts b/packages/system-api/src/test/queries/index.ts
index c540106e..b1314529 100644
--- a/packages/system-api/src/test/queries/index.ts
+++ b/packages/system-api/src/test/queries/index.ts
@@ -9,6 +9,7 @@ import * as Me from './me.graphql';
import * as isConfigured from './isConfigured.graphql';
import * as systemInfo from './systemInfo.graphql';
import * as version from './version.graphql';
+import * as refreshToken from './refreshToken.graphql';
export const listAppInfosQuery = print(listAppInfos);
export const getAppQuery = print(getApp);
@@ -17,3 +18,4 @@ export const MeQuery = print(Me);
export const isConfiguredQuery = print(isConfigured);
export const systemInfoQuery = print(systemInfo);
export const versionQuery = print(version);
+export const refreshTokenQuery = print(refreshToken);
diff --git a/packages/system-api/src/test/queries/refreshToken.graphql b/packages/system-api/src/test/queries/refreshToken.graphql
new file mode 100644
index 00000000..4a53d9d0
--- /dev/null
+++ b/packages/system-api/src/test/queries/refreshToken.graphql
@@ -0,0 +1,6 @@
+# Write your query or mutation here
+query RefreshToken {
+ refreshToken {
+ token
+ }
+}
diff --git a/packages/system-api/src/types.ts b/packages/system-api/src/types.ts
index a147ee91..e18e62b6 100644
--- a/packages/system-api/src/types.ts
+++ b/packages/system-api/src/types.ts
@@ -1,11 +1,4 @@
import { Request, Response } from 'express';
-import 'express-session';
-
-declare module 'express-session' {
- interface SessionData {
- userId: number;
- }
-}
export type MyContext = {
req: Request;
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 82effd32..2636c52d 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -124,15 +124,15 @@ importers:
'@swc/core': ^1.2.210
'@types/cors': ^2.8.12
'@types/express': ^4.17.13
- '@types/express-session': ^1.17.4
'@types/fs-extra': ^9.0.13
'@types/jest': ^27.5.0
+ '@types/jsonwebtoken': ^8.5.9
'@types/node': 17.0.31
'@types/node-cron': ^3.0.2
'@types/pg': ^8.6.5
'@types/semver': ^7.3.12
- '@types/session-file-store': ^1.2.2
'@types/tcp-port-used': ^1.0.1
+ '@types/uuid': ^8.3.4
'@types/validator': ^13.7.2
'@typescript-eslint/eslint-plugin': ^5.18.0
'@typescript-eslint/parser': ^5.22.0
@@ -150,7 +150,6 @@ importers:
eslint-plugin-import: ^2.26.0
eslint-plugin-prettier: ^4.0.0
express: ^4.17.3
- express-session: ^1.17.3
fs-extra: ^10.1.0
graphql: ^15.3.0
graphql-import-node: ^0.0.5
@@ -158,22 +157,24 @@ importers:
http: 0.0.1-security
internal-ip: ^6.0.0
jest: ^28.1.0
+ jsonwebtoken: ^8.5.1
node-cache: ^5.1.2
node-cron: ^3.0.1
node-port-scanner: ^3.0.1
nodemon: ^2.0.15
pg: ^8.7.3
prettier: 2.6.2
+ redis: ^4.3.1
reflect-metadata: ^0.1.13
rimraf: ^3.0.2
semver: ^7.3.7
- session-file-store: ^1.5.0
tcp-port-used: ^1.0.2
ts-jest: ^28.0.2
ts-node: ^10.8.2
type-graphql: ^1.1.1
typeorm: ^0.3.6
typescript: 4.6.4
+ uuid: ^9.0.0
validator: ^13.7.0
winston: ^3.7.2
zod: ^3.19.1
@@ -186,22 +187,23 @@ importers:
cors: 2.8.5
dotenv: 16.0.0
express: 4.18.1
- express-session: 1.17.3
fs-extra: 10.1.0
graphql: 15.8.0
graphql-type-json: 0.3.2_graphql@15.8.0
http: 0.0.1-security
internal-ip: 6.2.0
+ jsonwebtoken: 8.5.1
node-cache: 5.1.2
node-cron: 3.0.1
node-port-scanner: 3.0.1
pg: 8.7.3
+ redis: 4.3.1
reflect-metadata: 0.1.13
semver: 7.3.7
- session-file-store: 1.5.0
tcp-port-used: 1.0.2
type-graphql: 1.1.1_v2revtygxcm7xrdg2oz3ssohfu
- typeorm: 0.3.6_pg@8.7.3+ts-node@10.8.2
+ typeorm: 0.3.6_rymjtjxvmmxrsowl5wrmwxcyqa
+ uuid: 9.0.0
validator: 13.7.0
winston: 3.7.2
zod: 3.19.1
@@ -211,15 +213,15 @@ importers:
'@swc/core': 1.2.210
'@types/cors': 2.8.12
'@types/express': 4.17.13
- '@types/express-session': 1.17.4
'@types/fs-extra': 9.0.13
'@types/jest': 27.5.0
+ '@types/jsonwebtoken': 8.5.9
'@types/node': 17.0.31
'@types/node-cron': 3.0.2
'@types/pg': 8.6.5
'@types/semver': 7.3.12
- '@types/session-file-store': 1.2.2
'@types/tcp-port-used': 1.0.1
+ '@types/uuid': 8.3.4
'@types/validator': 13.7.2
'@typescript-eslint/eslint-plugin': 5.22.0_tal4xlmvnofklupd3hwjtzfb4q
'@typescript-eslint/parser': 5.22.0_hcfsmds2fshutdssjqluwm76uu
@@ -3369,6 +3371,55 @@ packages:
resolution: {integrity: sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==}
dev: false
+ /@redis/bloom/1.0.2_@redis+client@1.3.0:
+ resolution: {integrity: sha512-EBw7Ag1hPgFzdznK2PBblc1kdlj5B5Cw3XwI9/oG7tSn85/HKy3X9xHy/8tm/eNXJYHLXHJL/pkwBpFMVVefkw==}
+ peerDependencies:
+ '@redis/client': ^1.0.0
+ dependencies:
+ '@redis/client': 1.3.0
+ dev: false
+
+ /@redis/client/1.3.0:
+ resolution: {integrity: sha512-XCFV60nloXAefDsPnYMjHGtvbtHR8fV5Om8cQ0JYqTNbWcQo/4AryzJ2luRj4blveWazRK/j40gES8M7Cp6cfQ==}
+ engines: {node: '>=14'}
+ dependencies:
+ cluster-key-slot: 1.1.0
+ generic-pool: 3.8.2
+ yallist: 4.0.0
+ dev: false
+
+ /@redis/graph/1.0.1_@redis+client@1.3.0:
+ resolution: {integrity: sha512-oDE4myMCJOCVKYMygEMWuriBgqlS5FqdWerikMoJxzmmTUErnTRRgmIDa2VcgytACZMFqpAOWDzops4DOlnkfQ==}
+ peerDependencies:
+ '@redis/client': ^1.0.0
+ dependencies:
+ '@redis/client': 1.3.0
+ dev: false
+
+ /@redis/json/1.0.4_@redis+client@1.3.0:
+ resolution: {integrity: sha512-LUZE2Gdrhg0Rx7AN+cZkb1e6HjoSKaeeW8rYnt89Tly13GBI5eP4CwDVr+MY8BAYfCg4/N15OUrtLoona9uSgw==}
+ peerDependencies:
+ '@redis/client': ^1.0.0
+ dependencies:
+ '@redis/client': 1.3.0
+ dev: false
+
+ /@redis/search/1.1.0_@redis+client@1.3.0:
+ resolution: {integrity: sha512-NyFZEVnxIJEybpy+YskjgOJRNsfTYqaPbK/Buv6W2kmFNaRk85JiqjJZA5QkRmWvGbyQYwoO5QfDi2wHskKrQQ==}
+ peerDependencies:
+ '@redis/client': ^1.0.0
+ dependencies:
+ '@redis/client': 1.3.0
+ dev: false
+
+ /@redis/time-series/1.0.3_@redis+client@1.3.0:
+ resolution: {integrity: sha512-OFp0q4SGrTH0Mruf6oFsHGea58u8vS/iI5+NpYdicaM+7BgqBZH8FFvNZ8rYYLrUO/QRqMq72NpXmxLVNcdmjA==}
+ peerDependencies:
+ '@redis/client': ^1.0.0
+ dependencies:
+ '@redis/client': 1.3.0
+ dev: false
+
/@rushstack/eslint-patch/1.0.8:
resolution: {integrity: sha512-ZK5v4bJwgXldAUA8r3q9YKfCwOqoHTK/ZqRjSeRXQrBXWouoPnS4MQtgC4AXGiiBuUu5wxrRgTlv0ktmM4P1Aw==}
dev: true
@@ -3691,12 +3742,6 @@ packages:
'@types/range-parser': 1.2.4
dev: false
- /@types/express-session/1.17.4:
- resolution: {integrity: sha512-7cNlSI8+oOBUHTfPXMwDxF/Lchx5aJ3ho7+p9jJZYVg9dVDJFh3qdMXmJtRsysnvS+C6x46k9DRYmrmCkE+MVg==}
- dependencies:
- '@types/express': 4.17.13
- dev: true
-
/@types/express/4.17.13:
resolution: {integrity: sha512-6bSZTPaTIACxn48l50SR+axgrqm6qXFIxrdAKaG6PaJk3+zuUr35hBlgT7vOmJcum+OEaIBLtHV/qloEAFITeA==}
dependencies:
@@ -3779,6 +3824,12 @@ packages:
'@types/node': 17.0.31
dev: true
+ /@types/jsonwebtoken/8.5.9:
+ resolution: {integrity: sha512-272FMnFGzAVMGtu9tkr29hRL6bZj4Zs1KZNeHLnKqAvp06tAIcarTMwOh8/8bz4FmKRcMxZhZNeUAQsNLoiPhg==}
+ dependencies:
+ '@types/node': 17.0.31
+ dev: true
+
/@types/keyv/3.1.4:
resolution: {integrity: sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg==}
dependencies:
@@ -3906,13 +3957,6 @@ packages:
'@types/mime': 1.3.2
'@types/node': 17.0.31
- /@types/session-file-store/1.2.2:
- resolution: {integrity: sha512-l9yZ+PQ8vaXhch03MrV+25BIbhKpeWfZB++3njPIm6lKeDGRS2qF2elLuVa4XrhfJbObqW0puhB3A6FCbkraZg==}
- dependencies:
- '@types/express': 4.17.13
- '@types/express-session': 1.17.4
- dev: true
-
/@types/stack-utils/2.0.1:
resolution: {integrity: sha512-Hl219/BT5fLAaz6NDkSuhzasy49dwQS/DSdu4MdggFB8zcXv7vflBI3xp7FEmkmdDkBUI2bPUNeMttp2knYdxw==}
dev: true
@@ -3925,6 +3969,10 @@ packages:
resolution: {integrity: sha512-PBjIUxZHOuj0R15/xuwJYjFi+KZdNFrehocChv4g5hu6aFroHue8m0lBP0POdK2nKzbw0cgV1mws8+V/JAcEkQ==}
dev: false
+ /@types/uuid/8.3.4:
+ resolution: {integrity: sha512-c/I8ZRb51j+pYGAu5CrFMRxqZ2ke4y2grEBO5AUjgSkSk+qT2Ea+OdWElz/OiMf5MNpn2b17kuVBwZLQJXzihw==}
+ dev: true
+
/@types/validator/13.7.2:
resolution: {integrity: sha512-KFcchQ3h0OPQgFirBRPZr5F/sVjxZsOrQHedj3zi8AH3Zv/hOLx2OLR4hxR5HcfoU+33n69ZuOfzthKVdMoTiw==}
dev: true
@@ -4662,15 +4710,6 @@ packages:
resolution: {integrity: sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==}
dev: true
- /asn1.js/5.4.1:
- resolution: {integrity: sha512-+I//4cYPccV8LdmBLiX8CYvf9Sp3vQsrqu2QNXRcrbiWvcx/UdlFiqUJJzxRQxgsZmvhXhn4cSKeSmoFjVdupA==}
- dependencies:
- bn.js: 4.12.0
- inherits: 2.0.4
- minimalistic-assert: 1.0.1
- safer-buffer: 2.1.2
- dev: false
-
/ast-types-flow/0.0.7:
resolution: {integrity: sha1-9wtzXGvKGlycItmCw+Oef+ujva0=}
dev: true
@@ -4859,10 +4898,6 @@ packages:
babel-preset-current-node-syntax: 1.0.1_@babel+core@7.17.10
dev: true
- /bagpipe/0.3.5:
- resolution: {integrity: sha512-42sAlmPDKes1nLm/aly+0VdaopSU9br+jkRELedhQxI5uXHgtk47I83Mpmf4zoNTRMASdLFtUkimlu/Z9zQ8+g==}
- dev: false
-
/bail/2.0.2:
resolution: {integrity: sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==}
dev: false
@@ -4886,10 +4921,6 @@ packages:
readable-stream: 3.6.0
dev: true
- /bn.js/4.12.0:
- resolution: {integrity: sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==}
- dev: false
-
/body-parser/1.20.0:
resolution: {integrity: sha512-DfJ+q6EPcGKZD1QWUjSpqp+Q7bDQTsQIF4zfUAtZ6qk+H/3/QRhg9CEp39ss+/T2vw0+HaidC0ecJj/DRLIaKg==}
engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16}
@@ -4963,8 +4994,7 @@ packages:
dev: true
/buffer-equal-constant-time/1.0.1:
- resolution: {integrity: sha1-+OcRMvf/5uAaXJaXpMbz5I1cyBk=}
- dev: true
+ resolution: {integrity: sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==}
/buffer-from/1.1.2:
resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==}
@@ -5288,6 +5318,11 @@ packages:
engines: {node: '>=6'}
dev: false
+ /cluster-key-slot/1.1.0:
+ resolution: {integrity: sha512-2Nii8p3RwAPiFwsnZvukotvow2rIHM+yQ6ZcBXGHdniadkYGZYiGmkHJIbZPIV9nfv7m/U1IPMVVcAhoWFeklw==}
+ engines: {node: '>=0.10.0'}
+ dev: false
+
/co/4.6.0:
resolution: {integrity: sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==}
engines: {iojs: '>= 1.0.0', node: '>= 0.12.0'}
@@ -5506,11 +5541,6 @@ packages:
resolution: {integrity: sha1-4wOogrNCzD7oylE6eZmXNNqzriw=}
dev: false
- /cookie/0.4.2:
- resolution: {integrity: sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA==}
- engines: {node: '>= 0.6'}
- dev: false
-
/cookie/0.5.0:
resolution: {integrity: sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==}
engines: {node: '>= 0.6'}
@@ -5957,7 +5987,6 @@ packages:
resolution: {integrity: sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==}
dependencies:
safe-buffer: 5.2.1
- dev: true
/ee-first/1.1.1:
resolution: {integrity: sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=}
@@ -6663,22 +6692,6 @@ packages:
jest-util: 28.1.0
dev: true
- /express-session/1.17.3:
- resolution: {integrity: sha512-4+otWXlShYlG1Ma+2Jnn+xgKUZTMJ5QD3YvfilX3AcocOAbIkVylSWEklzALe/+Pu4qV6TYBj5GwOBFfdKqLBw==}
- engines: {node: '>= 0.8.0'}
- dependencies:
- cookie: 0.4.2
- cookie-signature: 1.0.6
- debug: 2.6.9
- depd: 2.0.0
- on-headers: 1.0.2
- parseurl: 1.3.3
- safe-buffer: 5.2.1
- uid-safe: 2.1.5
- transitivePeerDependencies:
- - supports-color
- dev: false
-
/express/4.18.1:
resolution: {integrity: sha512-zZBcOX9TfehHQhtupq57OF8lFZ3UZi08Y97dwFCkD8p9d/d2Y3M+ykKcwaMDEL+4qyUolgBDX6AblpR3fL212Q==}
engines: {node: '>= 0.10.0'}
@@ -7006,15 +7019,6 @@ packages:
jsonfile: 6.1.0
universalify: 2.0.0
- /fs-extra/8.1.0:
- resolution: {integrity: sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==}
- engines: {node: '>=6 <7 || >=8'}
- dependencies:
- graceful-fs: 4.2.10
- jsonfile: 4.0.0
- universalify: 0.1.2
- dev: false
-
/fs-extra/9.1.0:
resolution: {integrity: sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==}
engines: {node: '>=10'}
@@ -7079,6 +7083,11 @@ packages:
wide-align: 1.1.5
dev: false
+ /generic-pool/3.8.2:
+ resolution: {integrity: sha512-nGToKy6p3PAbYQ7p1UlWl6vSPwfwU6TMSWK7TTu+WUY4ZjyZQGniGGt2oNVvyNSpyZYSB43zMXVLcBm08MTMkg==}
+ engines: {node: '>= 4'}
+ dev: false
+
/gensync/1.0.0-beta.2:
resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==}
engines: {node: '>=6.9.0'}
@@ -7577,6 +7586,7 @@ packages:
/imurmurhash/0.1.4:
resolution: {integrity: sha1-khi5srkoojixPcT7a21XbyMUU+o=}
engines: {node: '>=0.8.19'}
+ dev: true
/indent-string/3.2.0:
resolution: {integrity: sha512-BYqTHXTGUIvg7t1r4sJNKcbDZkL92nkXA8YtRpbjFHRHGDL/NtUeiBJMeE60kIFN/Mg8ESaWQvftaYMGJzQZCQ==}
@@ -7914,6 +7924,7 @@ packages:
/is-typedarray/1.0.0:
resolution: {integrity: sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==}
+ dev: true
/is-unc-path/1.0.0:
resolution: {integrity: sha512-mrGpVd0fs7WWLfVsStvgF6iEJnbjDFZh9/emhRDcGWTduTfNHd9CHeUwH3gYIjdbwo4On6hunkztwOaAw0yllQ==}
@@ -8621,12 +8632,6 @@ packages:
hasBin: true
dev: true
- /jsonfile/4.0.0:
- resolution: {integrity: sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==}
- optionalDependencies:
- graceful-fs: 4.2.10
- dev: false
-
/jsonfile/6.1.0:
resolution: {integrity: sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==}
dependencies:
@@ -8657,7 +8662,6 @@ packages:
lodash.once: 4.1.1
ms: 2.1.3
semver: 5.7.1
- dev: true
/jsx-ast-utils/3.3.0:
resolution: {integrity: sha512-XzO9luP6L0xkxwhIJMTJQpZo/eeN60K08jHdexfD569AGxeNug6UketeHXEhROoM8aR7EcUoOQmIhcJQjcuq8Q==}
@@ -8673,14 +8677,12 @@ packages:
buffer-equal-constant-time: 1.0.1
ecdsa-sig-formatter: 1.0.11
safe-buffer: 5.2.1
- dev: true
/jws/3.2.2:
resolution: {integrity: sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==}
dependencies:
jwa: 1.4.1
safe-buffer: 5.2.1
- dev: true
/keyv/3.1.0:
resolution: {integrity: sha512-9ykJ/46SN/9KPM/sichzQ7OvXyGDYKGTaDlKMGCAlg2UK8KRy4jb0d8sFc+0Tt0YYnThq8X2RZgCg74RPxgcVA==}
@@ -8703,13 +8705,6 @@ packages:
engines: {node: '>=6'}
dev: false
- /kruptein/2.2.3:
- resolution: {integrity: sha512-BTwprBPTzkFT9oTugxKd3WnWrX630MqUDsnmBuoa98eQs12oD4n4TeI0GbpdGcYn/73Xueg2rfnw+oK4dovnJg==}
- engines: {node: '>6'}
- dependencies:
- asn1.js: 5.4.1
- dev: false
-
/kuler/2.0.0:
resolution: {integrity: sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A==}
dev: false
@@ -8833,27 +8828,21 @@ packages:
/lodash.includes/4.3.0:
resolution: {integrity: sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==}
- dev: true
/lodash.isboolean/3.0.3:
resolution: {integrity: sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==}
- dev: true
/lodash.isinteger/4.0.4:
resolution: {integrity: sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==}
- dev: true
/lodash.isnumber/3.0.3:
resolution: {integrity: sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==}
- dev: true
/lodash.isplainobject/4.0.6:
resolution: {integrity: sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==}
- dev: true
/lodash.isstring/4.0.1:
resolution: {integrity: sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==}
- dev: true
/lodash.map/4.6.0:
resolution: {integrity: sha512-worNHGKLDetmcEYDvh2stPCrrQRkP20E4l0iIS7F8EvzMqBBi7ltvFN5m1HvTf1P7Jk1txKhvFcmYsCr8O2F1Q==}
@@ -8873,7 +8862,6 @@ packages:
/lodash.once/4.1.1:
resolution: {integrity: sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==}
- dev: true
/lodash.sortby/4.7.0:
resolution: {integrity: sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA==}
@@ -9617,10 +9605,6 @@ packages:
engines: {node: '>=4'}
dev: true
- /minimalistic-assert/1.0.1:
- resolution: {integrity: sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==}
- dev: false
-
/minimatch/3.1.2:
resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==}
dependencies:
@@ -9968,11 +9952,6 @@ packages:
ee-first: 1.1.1
dev: false
- /on-headers/1.0.2:
- resolution: {integrity: sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==}
- engines: {node: '>= 0.8'}
- dev: false
-
/once/1.4.0:
resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==}
dependencies:
@@ -10545,11 +10524,6 @@ packages:
engines: {node: '>=10'}
dev: true
- /random-bytes/1.0.0:
- resolution: {integrity: sha512-iv7LhNVO047HzYR3InF6pUcUsPQiHTM1Qal51DcGSuZFBil1aBBWG5eHPNek7bvILMaYJ/8RU1e8w1AMdHmLQQ==}
- engines: {node: '>= 0.8'}
- dev: false
-
/range-parser/1.2.1:
resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==}
engines: {node: '>= 0.6'}
@@ -10808,6 +10782,17 @@ packages:
strip-indent: 3.0.0
dev: true
+ /redis/4.3.1:
+ resolution: {integrity: sha512-cM7yFU5CA6zyCF7N/+SSTcSJQSRMEKN0k0Whhu6J7n9mmXRoXugfWDBo5iOzGwABmsWKSwGPTU5J4Bxbl+0mrA==}
+ dependencies:
+ '@redis/bloom': 1.0.2_@redis+client@1.3.0
+ '@redis/client': 1.3.0
+ '@redis/graph': 1.0.1_@redis+client@1.3.0
+ '@redis/json': 1.0.4_@redis+client@1.3.0
+ '@redis/search': 1.1.0_@redis+client@1.3.0
+ '@redis/time-series': 1.0.3_@redis+client@1.3.0
+ dev: false
+
/reflect-metadata/0.1.13:
resolution: {integrity: sha512-Ts1Y/anZELhSsjMcU605fU9RE4Oi3p5ORujwbIKXfWa+0Zxs510Qrmrce5/Jowq3cHSZSJqBjypxmHarc+vEWg==}
dev: false
@@ -11003,11 +10988,6 @@ packages:
signal-exit: 3.0.7
dev: true
- /retry/0.12.0:
- resolution: {integrity: sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==}
- engines: {node: '>= 4'}
- dev: false
-
/retry/0.13.1:
resolution: {integrity: sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==}
engines: {node: '>= 4'}
@@ -11093,7 +11073,6 @@ packages:
/semver/5.7.1:
resolution: {integrity: sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==}
hasBin: true
- dev: true
/semver/6.3.0:
resolution: {integrity: sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==}
@@ -11147,18 +11126,6 @@ packages:
- supports-color
dev: false
- /session-file-store/1.5.0:
- resolution: {integrity: sha512-60IZaJNzyu2tIeHutkYE8RiXVx3KRvacOxfLr2Mj92SIsRIroDsH0IlUUR6fJAjoTW4RQISbaOApa2IZpIwFdQ==}
- engines: {node: '>= 6'}
- dependencies:
- bagpipe: 0.3.5
- fs-extra: 8.1.0
- kruptein: 2.2.3
- object-assign: 4.1.1
- retry: 0.12.0
- write-file-atomic: 3.0.3
- dev: false
-
/set-blocking/2.0.0:
resolution: {integrity: sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==}
@@ -12005,8 +11972,9 @@ packages:
resolution: {integrity: sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==}
dependencies:
is-typedarray: 1.0.0
+ dev: true
- /typeorm/0.3.6_pg@8.7.3+ts-node@10.8.2:
+ /typeorm/0.3.6_rymjtjxvmmxrsowl5wrmwxcyqa:
resolution: {integrity: sha512-DRqgfqcelMiGgWSMbBmVoJNFN2nPNA3EeY2gC324ndr2DZoGRTb9ILtp2oGVGnlA+cu5zgQ6it5oqKFNkte7Aw==}
engines: {node: '>= 12.9.0'}
hasBin: true
@@ -12076,6 +12044,7 @@ packages:
js-yaml: 4.1.0
mkdirp: 1.0.4
pg: 8.7.3
+ redis: 4.3.1
reflect-metadata: 0.1.13
sha.js: 2.4.11
ts-node: 10.8.2_uva6s4l7h33czpzezvop6ux5pe
@@ -12097,13 +12066,6 @@ packages:
resolution: {integrity: sha512-qLK/Xe9E2uzmYI3qLeOmI0tEOt+TBBQyUIAh4aAgU05FVYzeZrKUdkAZfBNVGRaHVgV0TDkdEngJSw/SyQchkQ==}
dev: true
- /uid-safe/2.1.5:
- resolution: {integrity: sha512-KPHm4VL5dDXKz01UuEd88Df+KzynaohSL9fBh096KWAxSKZQDI2uBrVqtvRM4rwrIrRRKsdLNML/lnaaVSRioA==}
- engines: {node: '>= 0.8'}
- dependencies:
- random-bytes: 1.0.0
- dev: false
-
/unbox-primitive/1.0.2:
resolution: {integrity: sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==}
dependencies:
@@ -12215,11 +12177,6 @@ packages:
unist-util-visit-parents: 5.1.0
dev: false
- /universalify/0.1.2:
- resolution: {integrity: sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==}
- engines: {node: '>= 4.0.0'}
- dev: false
-
/universalify/2.0.0:
resolution: {integrity: sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==}
engines: {node: '>= 10.0.0'}
@@ -12333,6 +12290,11 @@ packages:
hasBin: true
dev: false
+ /uuid/9.0.0:
+ resolution: {integrity: sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg==}
+ hasBin: true
+ dev: false
+
/uvu/0.5.3:
resolution: {integrity: sha512-brFwqA3FXzilmtnIyJ+CxdkInkY/i4ErvP7uV0DnUVxQcQ55reuHphorpF+tZoVHK2MniZ/VJzI7zJQoc9T9Yw==}
engines: {node: '>=8'}
@@ -12382,7 +12344,7 @@ packages:
engines: {node: '>=12'}
/vary/1.1.2:
- resolution: {integrity: sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=}
+ resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==}
engines: {node: '>= 0.8'}
dev: false
@@ -12555,6 +12517,7 @@ packages:
is-typedarray: 1.0.0
signal-exit: 3.0.7
typedarray-to-buffer: 3.1.5
+ dev: true
/write-file-atomic/4.0.1:
resolution: {integrity: sha512-nSKUxgAbyioruk6hU87QzVbY279oYT6uiwgDoujth2ju4mJ+TZau7SQBhtbTmUyuNYTuXnSyRn66FV0+eCgcrQ==}
diff --git a/scripts/common.sh b/scripts/common.sh
index 09c94776..f0b0f4b5 100644
--- a/scripts/common.sh
+++ b/scripts/common.sh
@@ -54,7 +54,7 @@ function ensure_linux() {
function clean_logs() {
# Clean logs folder
- logs_folder="${ROOT_FOLDER}/logs"
+ local logs_folder="${ROOT_FOLDER}/logs"
# Create the folder if it doesn't exist
if [[ ! -d "${logs_folder}" ]]; then
@@ -64,7 +64,7 @@ function clean_logs() {
if [ "$(find "${logs_folder}" -maxdepth 1 -type f | wc -l)" -gt 0 ]; then
echo "Cleaning logs folder..."
- files=($(ls -d "${logs_folder}"/* | xargs -n 1 basename | sed 's/\///g'))
+ local files=($(ls -d "${logs_folder}"/* | xargs -n 1 basename | sed 's/\///g'))
for file in "${files[@]}"; do
echo "Removing ${file}"
@@ -74,16 +74,18 @@ function clean_logs() {
}
function kill_watcher() {
- watcher_pid="$(ps aux | grep "scripts/watcher" | grep -v grep | awk '{print $2}')"
+ local watcher_pid="$(ps aux | grep "scripts/watcher" | grep -v grep | awk '{print $2}')"
# kill it if it's running
if [[ -n $watcher_pid ]]; then
# If multiline kill each pid
if [[ $watcher_pid == *" "* ]]; then
for pid in $watcher_pid; do
+ # shellcheck disable=SC2086
kill -9 $pid
done
else
+ # shellcheck disable=SC2086
kill -9 $watcher_pid
fi
fi
diff --git a/scripts/start-dev.sh b/scripts/start-dev.sh
index 00a48b65..3cf9c63e 100755
--- a/scripts/start-dev.sh
+++ b/scripts/start-dev.sh
@@ -1,5 +1,10 @@
#!/usr/bin/env bash
-set -e # Exit immediately if a command exits with a non-zero status.
+set -o errexit
+set -o nounset
+set -o pipefail
+if [[ "${TRACE-0}" == "1" ]]; then
+ set -o xtrace
+fi
source "${BASH_SOURCE%/*}/common.sh"
diff --git a/scripts/start.sh b/scripts/start.sh
index 1f3cb6cc..6db3787f 100755
--- a/scripts/start.sh
+++ b/scripts/start.sh
@@ -1,7 +1,11 @@
#!/usr/bin/env bash
-set -e # Exit immediately if a command exits with a non-zero status.
-
+set -o errexit
+set -o nounset
+set -o pipefail
+if [[ "${TRACE-0}" == "1" ]]; then
+ set -o xtrace
+fi
source "${BASH_SOURCE%/*}/common.sh"
ROOT_FOLDER="${PWD}"
@@ -80,12 +84,12 @@ fi
### --------------------------------
### CLI arguments
### --------------------------------
-while [ -n "$1" ]; do
+while [ -n "${1-}" ]; do
case "$1" in
--rc) rc="true" ;;
--ci) ci="true" ;;
--port)
- port="$2"
+ port="${2-}"
if [[ "${port}" =~ ^[0-9]+$ ]]; then
NGINX_PORT="${port}"
@@ -96,7 +100,7 @@ while [ -n "$1" ]; do
shift
;;
--ssl-port)
- ssl_port="$2"
+ ssl_port="${2-}"
if [[ "${ssl_port}" =~ ^[0-9]+$ ]]; then
NGINX_PORT_SSL="${ssl_port}"
@@ -107,7 +111,7 @@ while [ -n "$1" ]; do
shift
;;
--domain)
- domain="$2"
+ domain="${2-}"
if [[ "${domain}" =~ ^[a-zA-Z0-9.-]+$ ]]; then
DOMAIN="${domain}"
@@ -118,7 +122,7 @@ while [ -n "$1" ]; do
shift
;;
--listen-ip)
- listen_ip="$2"
+ listen_ip="${2-}"
if [[ "${listen_ip}" =~ ^[a-fA-F0-9.:]+$ ]]; then
INTERNAL_IP="${listen_ip}"
@@ -230,9 +234,9 @@ mv -f "$ENV_FILE" "$ROOT_FOLDER/.env"
### --------------------------------
### Start the project
### --------------------------------
-if [[ ! $ci == "true" ]]; then
+if [[ ! "${ci-false}" == "true" ]]; then
- if [[ $rc == "true" ]]; then
+ if [[ "${rc-false}" == "true" ]]; then
docker compose -f docker-compose.rc.yml --env-file "${ROOT_FOLDER}/.env" pull
# Run docker compose
docker compose -f docker-compose.rc.yml --env-file "${ROOT_FOLDER}/.env" up --detach --remove-orphans --build || {
diff --git a/scripts/stop.sh b/scripts/stop.sh
index 810d9788..cda90052 100755
--- a/scripts/stop.sh
+++ b/scripts/stop.sh
@@ -16,7 +16,7 @@ export COMPOSE_HTTP_TIMEOUT=240
# Stop all installed apps if there are any
apps_folder="${ROOT_FOLDER}/apps"
if [ "$(find "${apps_folder}" -maxdepth 1 -type d | wc -l)" -gt 1 ]; then
- apps_names=($(ls -d ${apps_folder}/*/ | xargs -n 1 basename | sed 's/\///g'))
+ apps_names=($(ls -d "${apps_folder}"/*/ | xargs -n 1 basename | sed 's/\///g'))
for app_name in "${apps_names[@]}"; do
# if folder ${ROOT_FOLDER}/app-data/app_name exists, then stop app
@@ -29,6 +29,7 @@ else
echo "No app installed that can be stopped."
fi
+kill_watcher
echo "Stopping Docker services..."
echo
docker compose down --remove-orphans --rmi local
diff --git a/scripts/watcher.sh b/scripts/watcher.sh
index 9c1fb194..ce93056c 100755
--- a/scripts/watcher.sh
+++ b/scripts/watcher.sh
@@ -6,8 +6,6 @@ ROOT_FOLDER="${PWD}"
WATCH_FILE="${ROOT_FOLDER}/state/events"
function clean_events() {
- echo "Cleaning events..."
-
# Create the file if it doesn't exist
if [[ ! -f "${WATCH_FILE}" ]]; then
touch "${WATCH_FILE}"
@@ -43,8 +41,6 @@ function run_command() {
local result=$?
- echo "Command ${command_path} exited with code ${result}"
-
if [[ $result -eq 0 ]]; then
set_status "$id" "success"
else
@@ -105,7 +101,6 @@ function select_command() {
return 0
fi
- echo "Unknown command ${command}"
return 0
}