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
This commit is contained in:
parent
d287175b07
commit
48c1e2607f
51 changed files with 856 additions and 341 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -11,6 +11,7 @@ github.secrets
|
||||||
node_modules/
|
node_modules/
|
||||||
app-data/*
|
app-data/*
|
||||||
data/postgres
|
data/postgres
|
||||||
|
data/redis
|
||||||
!app-data/.gitkeep
|
!app-data/.gitkeep
|
||||||
repos/*
|
repos/*
|
||||||
!repos/.gitkeep
|
!repos/.gitkeep
|
||||||
|
|
63
README.md
63
README.md
|
@ -1,7 +1,11 @@
|
||||||
# ⛺️ Tipi — A personal homeserver for everyone
|
# Tipi — A personal homeserver for everyone
|
||||||
|
|
||||||
<!-- ALL-CONTRIBUTORS-BADGE:START - Do not remove or modify this section -->
|
<!-- ALL-CONTRIBUTORS-BADGE:START - Do not remove or modify this section -->
|
||||||
|
|
||||||
[](#contributors-)
|
[](#contributors-)
|
||||||
|
|
||||||
<!-- ALL-CONTRIBUTORS-BADGE:END -->
|
<!-- ALL-CONTRIBUTORS-BADGE:END -->
|
||||||
|
|
||||||
[](https://github.com/meienberger/runtipi/blob/master/LICENSE)
|
[](https://github.com/meienberger/runtipi/blob/master/LICENSE)
|
||||||
[](https://github.com/meienberger/runtipi/releases)
|
[](https://github.com/meienberger/runtipi/releases)
|
||||||

|

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

|

|
||||||
[](https://codecov.io/gh/meienberger/runtipi)
|
[](https://codecov.io/gh/meienberger/runtipi)
|
||||||
|
|
||||||
#### Join the discussion
|
#### Join the discussion
|
||||||
|
|
||||||
[](https://discord.gg/Bu9qEPnHsc)
|
[](https://discord.gg/Bu9qEPnHsc)
|
||||||
[](https://matrix.to/#/#runtipi:matrix.org)
|
[](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 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.
|
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,6 +28,7 @@ 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**
|
Check our demo instance : **[demo.runtipi.com](https://demo.runtipi.com)** / username: **user@runtipi.com** / password: **runtipi**
|
||||||
|
|
||||||
## Apps available
|
## Apps available
|
||||||
|
|
||||||
- [Adguard Home](https://github.com/AdguardTeam/AdGuardHome) - Adguard Home DNS adblocker
|
- [Adguard Home](https://github.com/AdguardTeam/AdGuardHome) - Adguard Home DNS adblocker
|
||||||
- [Booksonic](https://github.com/popeen) - A server for streaming your audiobooks
|
- [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.
|
- [BookStack](https://www.bookstackapp.com/) - BookStack is a self-hosted platform for organising and storing information.
|
||||||
|
@ -84,9 +92,11 @@ You can find and submit new apps inside of the [RunTipi Appstore](https://github
|
||||||
## 🛠 Installation
|
## 🛠 Installation
|
||||||
|
|
||||||
### Installation Requirements
|
### 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.
|
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
|
### Step 1. Download Tipi
|
||||||
|
|
||||||
Run this in an empty directory where you want to install Tipi.
|
Run this in an empty directory where you want to install Tipi.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
@ -94,6 +104,7 @@ git clone https://github.com/meienberger/runtipi.git
|
||||||
```
|
```
|
||||||
|
|
||||||
### Step 2. Run Tipi
|
### Step 2. Run Tipi
|
||||||
|
|
||||||
cd into the downloaded directory and run the start script.
|
cd into the downloaded directory and run the start script.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
@ -115,45 +126,73 @@ sudo ./scripts/stop.sh
|
||||||
```
|
```
|
||||||
|
|
||||||
### Custom settings
|
### 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
|
```json
|
||||||
{
|
{
|
||||||
"dnsIp": "9.9.9.9", // DNS IP address
|
"dnsIp": "9.9.9.9",
|
||||||
"domain": "mydomain.com", // Domain name to link to the dashboard
|
"domain": "mydomain.com"
|
||||||
"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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## 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.
|
If you want to link a domain to your dashboard, you can do so by providing the `--domain` option in the start script.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
sudo ./scripts/start.sh --domain mydomain.com
|
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.
|
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
|
## ❤️ Contributing
|
||||||
|
|
||||||
Tipi is made to be very easy to plug in new apps. We welcome and appreciate new contributions.
|
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.
|
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
|
## 📜 License
|
||||||
|
|
||||||
[](https://github.com/meienberger/runtipi/blob/master/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.
|
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
|
## 🗣 Community
|
||||||
|
|
||||||
- [Matrix](https://matrix.to/#/#runtipi:matrix.org)<br />
|
- [Matrix](https://matrix.to/#/#runtipi:matrix.org)<br />
|
||||||
- [Twitter](https://twitter.com/runtipi)
|
- [Twitter](https://twitter.com/runtipi)
|
||||||
- [Telegram](https://t.me/+72-y10MnLBw2ZGI0)
|
- [Telegram](https://t.me/+72-y10MnLBw2ZGI0)
|
||||||
|
|
|
@ -4,7 +4,7 @@ services:
|
||||||
reverse-proxy:
|
reverse-proxy:
|
||||||
container_name: reverse-proxy
|
container_name: reverse-proxy
|
||||||
image: traefik:v2.8
|
image: traefik:v2.8
|
||||||
restart: always
|
restart: unless-stopped
|
||||||
ports:
|
ports:
|
||||||
- ${NGINX_PORT-80}:80
|
- ${NGINX_PORT-80}:80
|
||||||
- ${NGINX_PORT_SSL-443}:443
|
- ${NGINX_PORT_SSL-443}:443
|
||||||
|
@ -21,7 +21,7 @@ services:
|
||||||
tipi-db:
|
tipi-db:
|
||||||
container_name: tipi-db
|
container_name: tipi-db
|
||||||
image: postgres:14
|
image: postgres:14
|
||||||
restart: on-failure
|
restart: unless-stopped
|
||||||
stop_grace_period: 1m
|
stop_grace_period: 1m
|
||||||
volumes:
|
volumes:
|
||||||
- ./data/postgres:/var/lib/postgresql/data
|
- ./data/postgres:/var/lib/postgresql/data
|
||||||
|
@ -39,6 +39,15 @@ services:
|
||||||
networks:
|
networks:
|
||||||
- tipi_main_network
|
- tipi_main_network
|
||||||
|
|
||||||
|
tipi-redis:
|
||||||
|
container_name: tipi-redis
|
||||||
|
image: redis:alpine
|
||||||
|
restart: unless-stopped
|
||||||
|
volumes:
|
||||||
|
- ./data/redis:/data
|
||||||
|
networks:
|
||||||
|
- tipi_main_network
|
||||||
|
|
||||||
api:
|
api:
|
||||||
build:
|
build:
|
||||||
context: .
|
context: .
|
||||||
|
|
|
@ -4,7 +4,7 @@ services:
|
||||||
reverse-proxy:
|
reverse-proxy:
|
||||||
container_name: reverse-proxy
|
container_name: reverse-proxy
|
||||||
image: traefik:v2.8
|
image: traefik:v2.8
|
||||||
restart: always
|
restart: unless-stopped
|
||||||
ports:
|
ports:
|
||||||
- ${NGINX_PORT-80}:80
|
- ${NGINX_PORT-80}:80
|
||||||
- ${NGINX_PORT_SSL-443}:443
|
- ${NGINX_PORT_SSL-443}:443
|
||||||
|
@ -20,7 +20,7 @@ services:
|
||||||
tipi-db:
|
tipi-db:
|
||||||
container_name: tipi-db
|
container_name: tipi-db
|
||||||
image: postgres:14
|
image: postgres:14
|
||||||
restart: on-failure
|
restart: unless-stopped
|
||||||
stop_grace_period: 1m
|
stop_grace_period: 1m
|
||||||
volumes:
|
volumes:
|
||||||
- ./data/postgres:/var/lib/postgresql/data
|
- ./data/postgres:/var/lib/postgresql/data
|
||||||
|
@ -36,6 +36,15 @@ services:
|
||||||
networks:
|
networks:
|
||||||
- tipi_main_network
|
- tipi_main_network
|
||||||
|
|
||||||
|
tipi-redis:
|
||||||
|
container_name: tipi-redis
|
||||||
|
image: redis:alpine
|
||||||
|
restart: unless-stopped
|
||||||
|
volumes:
|
||||||
|
- ./data/redis:/data
|
||||||
|
networks:
|
||||||
|
- tipi_main_network
|
||||||
|
|
||||||
api:
|
api:
|
||||||
image: meienberger/runtipi:rc-${TIPI_VERSION}
|
image: meienberger/runtipi:rc-${TIPI_VERSION}
|
||||||
command: /bin/sh -c "cd /api && npm run start"
|
command: /bin/sh -c "cd /api && npm run start"
|
||||||
|
|
|
@ -4,7 +4,7 @@ services:
|
||||||
reverse-proxy:
|
reverse-proxy:
|
||||||
container_name: reverse-proxy
|
container_name: reverse-proxy
|
||||||
image: traefik:v2.8
|
image: traefik:v2.8
|
||||||
restart: always
|
restart: unless-stopped
|
||||||
ports:
|
ports:
|
||||||
- ${NGINX_PORT-80}:80
|
- ${NGINX_PORT-80}:80
|
||||||
- ${NGINX_PORT_SSL-443}:443
|
- ${NGINX_PORT_SSL-443}:443
|
||||||
|
@ -19,7 +19,7 @@ services:
|
||||||
tipi-db:
|
tipi-db:
|
||||||
container_name: tipi-db
|
container_name: tipi-db
|
||||||
image: postgres:14
|
image: postgres:14
|
||||||
restart: on-failure
|
restart: unless-stopped
|
||||||
stop_grace_period: 1m
|
stop_grace_period: 1m
|
||||||
volumes:
|
volumes:
|
||||||
- ${PWD}/data/postgres:/var/lib/postgresql/data
|
- ${PWD}/data/postgres:/var/lib/postgresql/data
|
||||||
|
@ -35,6 +35,15 @@ services:
|
||||||
networks:
|
networks:
|
||||||
- tipi_main_network
|
- tipi_main_network
|
||||||
|
|
||||||
|
tipi-redis:
|
||||||
|
container_name: tipi-redis
|
||||||
|
image: redis:alpine
|
||||||
|
restart: unless-stopped
|
||||||
|
volumes:
|
||||||
|
- ./data/redis:/data
|
||||||
|
networks:
|
||||||
|
- tipi_main_network
|
||||||
|
|
||||||
api:
|
api:
|
||||||
image: meienberger/runtipi:${TIPI_VERSION}
|
image: meienberger/runtipi:${TIPI_VERSION}
|
||||||
command: /bin/sh -c "cd /api && npm run start"
|
command: /bin/sh -c "cd /api && npm run start"
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "runtipi",
|
"name": "runtipi",
|
||||||
"version": "0.7.1",
|
"version": "0.7.2",
|
||||||
"description": "A homeserver for everyone",
|
"description": "A homeserver for everyone",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"prepare": "husky install",
|
"prepare": "husky install",
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
/** @type {import('ts-jest/dist/types').InitialOptionsTsJest} */
|
/** @type {import('ts-jest/dist/types').InitialOptionsTsJest} */
|
||||||
module.exports = {
|
module.exports = {
|
||||||
preset: 'ts-jest',
|
|
||||||
verbose: true,
|
verbose: true,
|
||||||
// testEnvironment: 'node',
|
// testEnvironment: 'node',
|
||||||
testMatch: ['**/__tests__/**/*.test.ts'],
|
testMatch: ['**/__tests__/**/*.test.ts'],
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "dashboard",
|
"name": "dashboard",
|
||||||
"version": "0.7.1",
|
"version": "0.7.2",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"test": "jest --colors",
|
"test": "jest --colors",
|
||||||
|
|
|
@ -1,11 +1,12 @@
|
||||||
import { Flex, useDisclosure, Spinner, Breadcrumb, BreadcrumbItem, useColorModeValue, Box } from '@chakra-ui/react';
|
import { Flex, useDisclosure, Spinner, Breadcrumb, BreadcrumbItem, useColorModeValue, Box } from '@chakra-ui/react';
|
||||||
import Head from 'next/head';
|
import Head from 'next/head';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import React from 'react';
|
import React, { useEffect } from 'react';
|
||||||
import { FiChevronRight } from 'react-icons/fi';
|
import { FiChevronRight } from 'react-icons/fi';
|
||||||
import Header from './Header';
|
import Header from './Header';
|
||||||
import Menu from './SideMenu';
|
import Menu from './SideMenu';
|
||||||
import MenuDrawer from './MenuDrawer';
|
import MenuDrawer from './MenuDrawer';
|
||||||
|
import { useRefreshTokenQuery } from '../../generated/graphql';
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
loading?: boolean;
|
loading?: boolean;
|
||||||
|
@ -15,6 +16,13 @@ interface IProps {
|
||||||
|
|
||||||
const Layout: React.FC<IProps> = ({ children, loading, breadcrumbs }) => {
|
const Layout: React.FC<IProps> = ({ children, loading, breadcrumbs }) => {
|
||||||
const { isOpen, onClose, onOpen } = useDisclosure();
|
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 menubg = useColorModeValue('#F1F3F4', '#202736');
|
||||||
const bg = useColorModeValue('white', '#1a202c');
|
const bg = useColorModeValue('white', '#1a202c');
|
||||||
|
|
|
@ -45,6 +45,11 @@ const SideMenu: React.FC = () => {
|
||||||
setColorMode(checked ? 'dark' : 'light');
|
setColorMode(checked ? 'dark' : 'light');
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleLogout = async () => {
|
||||||
|
localStorage.removeItem('token');
|
||||||
|
logout();
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box className="flex-1 flex flex-col p-0 md:p-4">
|
<Box className="flex-1 flex flex-col p-0 md:p-4">
|
||||||
<img className="self-center mb-5 logo mt-0 md:mt-5" src={getUrl('tipi.png')} width={512} height={512} />
|
<img className="self-center mb-5 logo mt-0 md:mt-5" src={getUrl('tipi.png')} width={512} height={512} />
|
||||||
|
@ -64,7 +69,7 @@ const SideMenu: React.FC = () => {
|
||||||
<p className="flex-1 mb-1 text-md">Donate</p>
|
<p className="flex-1 mb-1 text-md">Donate</p>
|
||||||
</ListItem>
|
</ListItem>
|
||||||
</a>
|
</a>
|
||||||
<ListItem onClick={() => logout()} className="cursor-pointer hover:font-bold flex items-center mb-5">
|
<ListItem onClick={handleLogout} className="cursor-pointer hover:font-bold flex items-center mb-5">
|
||||||
<FiLogOut size={20} className="mr-3" />
|
<FiLogOut size={20} className="mr-3" />
|
||||||
<p className="flex-1">Log out</p>
|
<p className="flex-1">Log out</p>
|
||||||
</ListItem>
|
</ListItem>
|
||||||
|
|
|
@ -2,7 +2,7 @@ import { ApolloClient, from, InMemoryCache } from '@apollo/client';
|
||||||
import links from './links';
|
import links from './links';
|
||||||
|
|
||||||
export const createApolloClient = async (url: string): Promise<ApolloClient<any>> => {
|
export const createApolloClient = async (url: string): Promise<ApolloClient<any>> => {
|
||||||
const additiveLink = from([links.errorLink, links.httpLink(url)]);
|
const additiveLink = from([links.errorLink, links.authLink, links.httpLink(url)]);
|
||||||
|
|
||||||
return new ApolloClient({
|
return new ApolloClient({
|
||||||
link: additiveLink,
|
link: additiveLink,
|
||||||
|
|
14
packages/dashboard/src/core/apollo/links/authLink.ts
Normal file
14
packages/dashboard/src/core/apollo/links/authLink.ts
Normal file
|
@ -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;
|
|
@ -3,7 +3,6 @@ import { HttpLink } from '@apollo/client';
|
||||||
const httpLink = (url: string) => {
|
const httpLink = (url: string) => {
|
||||||
return new HttpLink({
|
return new HttpLink({
|
||||||
uri: `${url}/graphql`,
|
uri: `${url}/graphql`,
|
||||||
credentials: 'include',
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -1,9 +1,11 @@
|
||||||
import errorLink from './errorLink';
|
import errorLink from './errorLink';
|
||||||
import httpLink from './httpLink';
|
import httpLink from './httpLink';
|
||||||
|
import authLink from './authLink';
|
||||||
|
|
||||||
const links = {
|
const links = {
|
||||||
errorLink,
|
errorLink,
|
||||||
httpLink,
|
httpLink,
|
||||||
|
authLink,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default links;
|
export default links;
|
||||||
|
|
|
@ -134,9 +134,9 @@ export type ListAppsResonse = {
|
||||||
export type Mutation = {
|
export type Mutation = {
|
||||||
__typename?: 'Mutation';
|
__typename?: 'Mutation';
|
||||||
installApp: App;
|
installApp: App;
|
||||||
login: UserResponse;
|
login: TokenResponse;
|
||||||
logout: Scalars['Boolean'];
|
logout: Scalars['Boolean'];
|
||||||
register: UserResponse;
|
register: TokenResponse;
|
||||||
restart: Scalars['Boolean'];
|
restart: Scalars['Boolean'];
|
||||||
startApp: App;
|
startApp: App;
|
||||||
stopApp: App;
|
stopApp: App;
|
||||||
|
@ -185,6 +185,7 @@ export type Query = {
|
||||||
isConfigured: Scalars['Boolean'];
|
isConfigured: Scalars['Boolean'];
|
||||||
listAppsInfo: ListAppsResonse;
|
listAppsInfo: ListAppsResonse;
|
||||||
me?: Maybe<User>;
|
me?: Maybe<User>;
|
||||||
|
refreshToken?: Maybe<TokenResponse>;
|
||||||
systemInfo?: Maybe<SystemInfoResponse>;
|
systemInfo?: Maybe<SystemInfoResponse>;
|
||||||
version: VersionResponse;
|
version: VersionResponse;
|
||||||
};
|
};
|
||||||
|
@ -200,6 +201,11 @@ export type SystemInfoResponse = {
|
||||||
memory: DiskMemory;
|
memory: DiskMemory;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type TokenResponse = {
|
||||||
|
__typename?: 'TokenResponse';
|
||||||
|
token: Scalars['String'];
|
||||||
|
};
|
||||||
|
|
||||||
export type UpdateInfo = {
|
export type UpdateInfo = {
|
||||||
__typename?: 'UpdateInfo';
|
__typename?: 'UpdateInfo';
|
||||||
current: Scalars['Float'];
|
current: Scalars['Float'];
|
||||||
|
@ -215,11 +221,6 @@ export type User = {
|
||||||
username: Scalars['String'];
|
username: Scalars['String'];
|
||||||
};
|
};
|
||||||
|
|
||||||
export type UserResponse = {
|
|
||||||
__typename?: 'UserResponse';
|
|
||||||
user?: Maybe<User>;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type UsernamePasswordInput = {
|
export type UsernamePasswordInput = {
|
||||||
password: Scalars['String'];
|
password: Scalars['String'];
|
||||||
username: Scalars['String'];
|
username: Scalars['String'];
|
||||||
|
@ -241,7 +242,7 @@ export type LoginMutationVariables = Exact<{
|
||||||
input: UsernamePasswordInput;
|
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 }>;
|
export type LogoutMutationVariables = Exact<{ [key: string]: never }>;
|
||||||
|
|
||||||
|
@ -251,7 +252,7 @@ export type RegisterMutationVariables = Exact<{
|
||||||
input: UsernamePasswordInput;
|
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 }>;
|
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 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 SystemInfoQueryVariables = Exact<{ [key: string]: never }>;
|
||||||
|
|
||||||
export type SystemInfoQuery = {
|
export type SystemInfoQuery = {
|
||||||
|
@ -436,9 +441,7 @@ export type InstallAppMutationOptions = Apollo.BaseMutationOptions<InstallAppMut
|
||||||
export const LoginDocument = gql`
|
export const LoginDocument = gql`
|
||||||
mutation Login($input: UsernamePasswordInput!) {
|
mutation Login($input: UsernamePasswordInput!) {
|
||||||
login(input: $input) {
|
login(input: $input) {
|
||||||
user {
|
token
|
||||||
id
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
@ -501,9 +504,7 @@ export type LogoutMutationOptions = Apollo.BaseMutationOptions<LogoutMutation, L
|
||||||
export const RegisterDocument = gql`
|
export const RegisterDocument = gql`
|
||||||
mutation Register($input: UsernamePasswordInput!) {
|
mutation Register($input: UsernamePasswordInput!) {
|
||||||
register(input: $input) {
|
register(input: $input) {
|
||||||
user {
|
token
|
||||||
id
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
@ -1000,6 +1001,40 @@ export function useMeLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<MeQuery
|
||||||
export type MeQueryHookResult = ReturnType<typeof useMeQuery>;
|
export type MeQueryHookResult = ReturnType<typeof useMeQuery>;
|
||||||
export type MeLazyQueryHookResult = ReturnType<typeof useMeLazyQuery>;
|
export type MeLazyQueryHookResult = ReturnType<typeof useMeLazyQuery>;
|
||||||
export type MeQueryResult = Apollo.QueryResult<MeQuery, MeQueryVariables>;
|
export type MeQueryResult = Apollo.QueryResult<MeQuery, MeQueryVariables>;
|
||||||
|
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<RefreshTokenQuery, RefreshTokenQueryVariables>) {
|
||||||
|
const options = { ...defaultOptions, ...baseOptions };
|
||||||
|
return Apollo.useQuery<RefreshTokenQuery, RefreshTokenQueryVariables>(RefreshTokenDocument, options);
|
||||||
|
}
|
||||||
|
export function useRefreshTokenLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<RefreshTokenQuery, RefreshTokenQueryVariables>) {
|
||||||
|
const options = { ...defaultOptions, ...baseOptions };
|
||||||
|
return Apollo.useLazyQuery<RefreshTokenQuery, RefreshTokenQueryVariables>(RefreshTokenDocument, options);
|
||||||
|
}
|
||||||
|
export type RefreshTokenQueryHookResult = ReturnType<typeof useRefreshTokenQuery>;
|
||||||
|
export type RefreshTokenLazyQueryHookResult = ReturnType<typeof useRefreshTokenLazyQuery>;
|
||||||
|
export type RefreshTokenQueryResult = Apollo.QueryResult<RefreshTokenQuery, RefreshTokenQueryVariables>;
|
||||||
export const SystemInfoDocument = gql`
|
export const SystemInfoDocument = gql`
|
||||||
query SystemInfo {
|
query SystemInfo {
|
||||||
systemInfo {
|
systemInfo {
|
||||||
|
|
|
@ -1,7 +1,5 @@
|
||||||
mutation Login($input: UsernamePasswordInput!) {
|
mutation Login($input: UsernamePasswordInput!) {
|
||||||
login(input: $input) {
|
login(input: $input) {
|
||||||
user {
|
token
|
||||||
id
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,5 @@
|
||||||
mutation Register($input: UsernamePasswordInput!) {
|
mutation Register($input: UsernamePasswordInput!) {
|
||||||
register(input: $input) {
|
register(input: $input) {
|
||||||
user {
|
token
|
||||||
id
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,5 @@
|
||||||
|
query RefreshToken {
|
||||||
|
refreshToken {
|
||||||
|
token
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,3 +1,4 @@
|
||||||
|
import { useApolloClient } from '@apollo/client';
|
||||||
import { useToast } from '@chakra-ui/react';
|
import { useToast } from '@chakra-ui/react';
|
||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { useLoginMutation } from '../../../generated/graphql';
|
import { useLoginMutation } from '../../../generated/graphql';
|
||||||
|
@ -7,11 +8,13 @@ import LoginForm from '../components/LoginForm';
|
||||||
type FormValues = { email: string; password: string };
|
type FormValues = { email: string; password: string };
|
||||||
|
|
||||||
const Login: React.FC = () => {
|
const Login: React.FC = () => {
|
||||||
const [login] = useLoginMutation({ refetchQueries: ['Me'] });
|
const client = useApolloClient();
|
||||||
|
const [login] = useLoginMutation({});
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const toast = useToast();
|
const toast = useToast();
|
||||||
|
|
||||||
const handleError = (error: unknown) => {
|
const handleError = (error: unknown) => {
|
||||||
|
localStorage.removeItem('token');
|
||||||
if (error instanceof Error) {
|
if (error instanceof Error) {
|
||||||
toast({
|
toast({
|
||||||
title: 'Error',
|
title: 'Error',
|
||||||
|
@ -26,7 +29,13 @@ const Login: React.FC = () => {
|
||||||
const handleLogin = async (values: FormValues) => {
|
const handleLogin = async (values: FormValues) => {
|
||||||
try {
|
try {
|
||||||
setLoading(true);
|
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) {
|
} catch (error) {
|
||||||
handleError(error);
|
handleError(error);
|
||||||
} finally {
|
} finally {
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import { useApolloClient } from '@apollo/client';
|
||||||
import { useToast } from '@chakra-ui/react';
|
import { useToast } from '@chakra-ui/react';
|
||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { useRegisterMutation } from '../../../generated/graphql';
|
import { useRegisterMutation } from '../../../generated/graphql';
|
||||||
|
@ -5,6 +6,7 @@ import AuthFormLayout from '../components/AuthFormLayout';
|
||||||
import RegisterForm from '../components/RegisterForm';
|
import RegisterForm from '../components/RegisterForm';
|
||||||
|
|
||||||
const Onboarding: React.FC = () => {
|
const Onboarding: React.FC = () => {
|
||||||
|
const client = useApolloClient();
|
||||||
const toast = useToast();
|
const toast = useToast();
|
||||||
const [register] = useRegisterMutation({ refetchQueries: ['Me'] });
|
const [register] = useRegisterMutation({ refetchQueries: ['Me'] });
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
|
@ -24,7 +26,13 @@ const Onboarding: React.FC = () => {
|
||||||
const handleRegister = async (values: { email: string; password: string }) => {
|
const handleRegister = async (values: { email: string; password: string }) => {
|
||||||
try {
|
try {
|
||||||
setLoading(true);
|
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) {
|
} catch (error) {
|
||||||
handleError(error);
|
handleError(error);
|
||||||
} finally {
|
} finally {
|
||||||
|
|
24
packages/system-api/__mocks__/redis.ts
Normal file
24
packages/system-api/__mocks__/redis.ts
Normal file
|
@ -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);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
};
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "system-api",
|
"name": "system-api",
|
||||||
"version": "0.7.1",
|
"version": "0.7.2",
|
||||||
"description": "",
|
"description": "",
|
||||||
"exports": "./dist/server.js",
|
"exports": "./dist/server.js",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
@ -33,22 +33,23 @@
|
||||||
"cors": "^2.8.5",
|
"cors": "^2.8.5",
|
||||||
"dotenv": "^16.0.0",
|
"dotenv": "^16.0.0",
|
||||||
"express": "^4.17.3",
|
"express": "^4.17.3",
|
||||||
"express-session": "^1.17.3",
|
|
||||||
"fs-extra": "^10.1.0",
|
"fs-extra": "^10.1.0",
|
||||||
"graphql": "^15.3.0",
|
"graphql": "^15.3.0",
|
||||||
"graphql-type-json": "^0.3.2",
|
"graphql-type-json": "^0.3.2",
|
||||||
"http": "0.0.1-security",
|
"http": "0.0.1-security",
|
||||||
"internal-ip": "^6.0.0",
|
"internal-ip": "^6.0.0",
|
||||||
|
"jsonwebtoken": "^8.5.1",
|
||||||
"node-cache": "^5.1.2",
|
"node-cache": "^5.1.2",
|
||||||
"node-cron": "^3.0.1",
|
"node-cron": "^3.0.1",
|
||||||
"node-port-scanner": "^3.0.1",
|
"node-port-scanner": "^3.0.1",
|
||||||
"pg": "^8.7.3",
|
"pg": "^8.7.3",
|
||||||
|
"redis": "^4.3.1",
|
||||||
"reflect-metadata": "^0.1.13",
|
"reflect-metadata": "^0.1.13",
|
||||||
"semver": "^7.3.7",
|
"semver": "^7.3.7",
|
||||||
"session-file-store": "^1.5.0",
|
|
||||||
"tcp-port-used": "^1.0.2",
|
"tcp-port-used": "^1.0.2",
|
||||||
"type-graphql": "^1.1.1",
|
"type-graphql": "^1.1.1",
|
||||||
"typeorm": "^0.3.6",
|
"typeorm": "^0.3.6",
|
||||||
|
"uuid": "^9.0.0",
|
||||||
"validator": "^13.7.0",
|
"validator": "^13.7.0",
|
||||||
"winston": "^3.7.2",
|
"winston": "^3.7.2",
|
||||||
"zod": "^3.19.1"
|
"zod": "^3.19.1"
|
||||||
|
@ -59,15 +60,15 @@
|
||||||
"@swc/core": "^1.2.210",
|
"@swc/core": "^1.2.210",
|
||||||
"@types/cors": "^2.8.12",
|
"@types/cors": "^2.8.12",
|
||||||
"@types/express": "^4.17.13",
|
"@types/express": "^4.17.13",
|
||||||
"@types/express-session": "^1.17.4",
|
|
||||||
"@types/fs-extra": "^9.0.13",
|
"@types/fs-extra": "^9.0.13",
|
||||||
"@types/jest": "^27.5.0",
|
"@types/jest": "^27.5.0",
|
||||||
|
"@types/jsonwebtoken": "^8.5.9",
|
||||||
"@types/node": "17.0.31",
|
"@types/node": "17.0.31",
|
||||||
"@types/node-cron": "^3.0.2",
|
"@types/node-cron": "^3.0.2",
|
||||||
"@types/pg": "^8.6.5",
|
"@types/pg": "^8.6.5",
|
||||||
"@types/semver": "^7.3.12",
|
"@types/semver": "^7.3.12",
|
||||||
"@types/session-file-store": "^1.2.2",
|
|
||||||
"@types/tcp-port-used": "^1.0.1",
|
"@types/tcp-port-used": "^1.0.1",
|
||||||
|
"@types/uuid": "^8.3.4",
|
||||||
"@types/validator": "^13.7.2",
|
"@types/validator": "^13.7.2",
|
||||||
"@typescript-eslint/eslint-plugin": "^5.18.0",
|
"@typescript-eslint/eslint-plugin": "^5.18.0",
|
||||||
"@typescript-eslint/parser": "^5.22.0",
|
"@typescript-eslint/parser": "^5.22.0",
|
||||||
|
|
|
@ -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<RedisClientType> {
|
||||||
|
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();
|
||||||
|
|
|
@ -22,11 +22,13 @@ const {
|
||||||
APPS_REPO_URL = '',
|
APPS_REPO_URL = '',
|
||||||
DOMAIN = '',
|
DOMAIN = '',
|
||||||
STORAGE_PATH = '/runtipi',
|
STORAGE_PATH = '/runtipi',
|
||||||
|
REDIS_HOST = 'tipi-redis',
|
||||||
ARCHITECTURE = 'amd64',
|
ARCHITECTURE = 'amd64',
|
||||||
} = process.env;
|
} = process.env;
|
||||||
|
|
||||||
const configSchema = z.object({
|
const configSchema = z.object({
|
||||||
NODE_ENV: z.union([z.literal('development'), z.literal('production'), z.literal('test')]),
|
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')]),
|
status: z.union([z.literal('RUNNING'), z.literal('UPDATING'), z.literal('RESTARTING')]),
|
||||||
architecture: z.nativeEnum(AppSupportedArchitecturesEnum),
|
architecture: z.nativeEnum(AppSupportedArchitecturesEnum),
|
||||||
logs: z.object({
|
logs: z.object({
|
||||||
|
@ -58,6 +60,7 @@ class Config {
|
||||||
LOGS_APP,
|
LOGS_APP,
|
||||||
LOGS_ERROR,
|
LOGS_ERROR,
|
||||||
},
|
},
|
||||||
|
REDIS_HOST,
|
||||||
NODE_ENV: NODE_ENV as z.infer<typeof configSchema>['NODE_ENV'],
|
NODE_ENV: NODE_ENV as z.infer<typeof configSchema>['NODE_ENV'],
|
||||||
architecture: ARCHITECTURE as z.infer<typeof configSchema>['architecture'],
|
architecture: ARCHITECTURE as z.infer<typeof configSchema>['architecture'],
|
||||||
rootFolder: '/runtipi',
|
rootFolder: '/runtipi',
|
||||||
|
|
|
@ -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();
|
||||||
|
});
|
||||||
|
});
|
|
@ -1,21 +1,32 @@
|
||||||
import session from 'express-session';
|
import { NextFunction, Request, Response } from 'express';
|
||||||
import SessionFileStore from 'session-file-store';
|
import jwt from 'jsonwebtoken';
|
||||||
import { COOKIE_MAX_AGE, __prod__ } from '../../config/constants/constants';
|
import logger from '../../config/logger/logger';
|
||||||
|
import TipiCache from '../../config/TipiCache';
|
||||||
import { getConfig } from '../config/TipiConfig';
|
import { getConfig } from '../config/TipiConfig';
|
||||||
|
|
||||||
const getSessionMiddleware = () => {
|
const getSessionMiddleware = async (req: Request, _: Response, next: NextFunction) => {
|
||||||
const FileStore = SessionFileStore(session);
|
req.session = {};
|
||||||
|
|
||||||
const sameSite = __prod__ ? 'lax' : 'none';
|
const token = req.headers.authorization?.split(' ')[1];
|
||||||
|
|
||||||
return session({
|
if (token) {
|
||||||
name: 'qid',
|
try {
|
||||||
store: new FileStore(),
|
const decodedToken = jwt.verify(token, getConfig().jwtSecret) as { id: number; session: string };
|
||||||
cookie: { maxAge: COOKIE_MAX_AGE, secure: false, sameSite, httpOnly: true },
|
|
||||||
secret: getConfig().jwtSecret,
|
const userId = await TipiCache.get(decodedToken.session);
|
||||||
resave: false,
|
|
||||||
saveUninitialized: false,
|
if (userId === decodedToken.id.toString()) {
|
||||||
});
|
req.session = {
|
||||||
|
userId: decodedToken.id,
|
||||||
|
id: decodedToken.session,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
logger.error(err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
next();
|
||||||
};
|
};
|
||||||
|
|
||||||
export default getSessionMiddleware;
|
export default getSessionMiddleware;
|
||||||
|
|
9
packages/system-api/src/declarations.d.ts
vendored
Normal file
9
packages/system-api/src/declarations.d.ts
vendored
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
declare namespace Express {
|
||||||
|
interface Request {
|
||||||
|
session: {
|
||||||
|
userId?: number;
|
||||||
|
id?: string;
|
||||||
|
};
|
||||||
|
[key: string]: any;
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,3 +1,3 @@
|
||||||
const objectKeys = <T>(obj: T): (keyof T)[] => Object.keys(obj) as (keyof T)[];
|
const objectKeys = <T extends object>(obj: T): (keyof T)[] => Object.keys(obj) as (keyof T)[];
|
||||||
|
|
||||||
export default { objectKeys };
|
export default { objectKeys };
|
||||||
|
|
|
@ -497,7 +497,7 @@ describe('List apps', () => {
|
||||||
it('Should list apps that have no supportedArchitectures specified', async () => {
|
it('Should list apps that have no supportedArchitectures specified', async () => {
|
||||||
// Arrange
|
// Arrange
|
||||||
setConfig('architecture', AppSupportedArchitecturesEnum.ARM);
|
setConfig('architecture', AppSupportedArchitecturesEnum.ARM);
|
||||||
const app3 = await createApp({});
|
const app3 = await createApp({ supportedArchitectures: undefined });
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
fs.__createMockFiles(Object.assign(app3.MockFiles));
|
fs.__createMockFiles(Object.assign(app3.MockFiles));
|
||||||
|
|
||||||
|
|
|
@ -159,19 +159,16 @@ const listApps = async (): Promise<ListAppsResonse> => {
|
||||||
|
|
||||||
const apps: AppInfo[] = folders
|
const apps: AppInfo[] = folders
|
||||||
.map((app) => {
|
.map((app) => {
|
||||||
try {
|
return readJsonFile(`/runtipi/repos/${getConfig().appsRepoId}/apps/${app}/config.json`);
|
||||||
return readJsonFile(`/runtipi/repos/${getConfig().appsRepoId}/apps/${app}/config.json`);
|
|
||||||
} catch (e) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
.filter(Boolean);
|
.filter(Boolean);
|
||||||
|
|
||||||
apps.forEach((app) => {
|
const filteredApps = filterApps(apps).map((app) => {
|
||||||
app.description = readFile(`/runtipi/repos/${getConfig().appsRepoId}/apps/${app.id}/metadata/description.md`);
|
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 };
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -1,13 +1,18 @@
|
||||||
import { faker } from '@faker-js/faker';
|
import { faker } from '@faker-js/faker';
|
||||||
|
import jwt from 'jsonwebtoken';
|
||||||
import { DataSource } from 'typeorm';
|
import { DataSource } from 'typeorm';
|
||||||
|
import TipiCache from '../../../config/TipiCache';
|
||||||
|
import { getConfig } from '../../../core/config/TipiConfig';
|
||||||
import { setupConnection, teardownConnection } from '../../../test/connection';
|
import { setupConnection, teardownConnection } from '../../../test/connection';
|
||||||
import { gcall } from '../../../test/gcall';
|
import { gcall } from '../../../test/gcall';
|
||||||
import { loginMutation, registerMutation } from '../../../test/mutations';
|
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 User from '../../auth/user.entity';
|
||||||
import { UserResponse } from '../auth.types';
|
import { TokenResponse } from '../auth.types';
|
||||||
import { createUser } from './user.factory';
|
import { createUser } from './user.factory';
|
||||||
|
|
||||||
|
jest.mock('redis');
|
||||||
|
|
||||||
let db: DataSource | null = null;
|
let db: DataSource | null = null;
|
||||||
const TEST_SUITE = 'authresolver';
|
const TEST_SUITE = 'authresolver';
|
||||||
|
|
||||||
|
@ -58,20 +63,30 @@ describe('Test: register', () => {
|
||||||
const password = faker.internet.password();
|
const password = faker.internet.password();
|
||||||
|
|
||||||
it('should register a user', async () => {
|
it('should register a user', async () => {
|
||||||
const { data } = await gcall<{ register: UserResponse }>({
|
const { data } = await gcall<{ register: TokenResponse }>({
|
||||||
source: registerMutation,
|
source: registerMutation,
|
||||||
variableValues: {
|
variableValues: {
|
||||||
input: { username: email, password },
|
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 () => {
|
it('should not register a user with an existing username', async () => {
|
||||||
await createUser(email);
|
await createUser(email);
|
||||||
|
|
||||||
const { errors } = await gcall<{ register: UserResponse }>({
|
const { errors } = await gcall<{ register: TokenResponse }>({
|
||||||
source: registerMutation,
|
source: registerMutation,
|
||||||
variableValues: {
|
variableValues: {
|
||||||
input: { username: email, password },
|
input: { username: email, password },
|
||||||
|
@ -82,7 +97,7 @@ describe('Test: register', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not register a user with a malformed email', async () => {
|
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,
|
source: registerMutation,
|
||||||
variableValues: {
|
variableValues: {
|
||||||
input: { username: 'not an email', password },
|
input: { username: 'not an email', password },
|
||||||
|
@ -101,18 +116,27 @@ describe('Test: login', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should login a user', async () => {
|
it('should login a user', async () => {
|
||||||
const { data } = await gcall<{ login: UserResponse }>({
|
const { data } = await gcall<{ login: TokenResponse }>({
|
||||||
source: loginMutation,
|
source: loginMutation,
|
||||||
variableValues: {
|
variableValues: {
|
||||||
input: { username: email, password: 'password' },
|
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 () => {
|
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,
|
source: loginMutation,
|
||||||
variableValues: {
|
variableValues: {
|
||||||
input: { username: email, password: 'wrong password' },
|
input: { username: email, password: 'wrong password' },
|
||||||
|
@ -123,7 +147,7 @@ describe('Test: login', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not login a user with a malformed email', async () => {
|
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,
|
source: loginMutation,
|
||||||
variableValues: {
|
variableValues: {
|
||||||
input: { username: 'not an email', password: 'password' },
|
input: { username: 'not an email', password: 'password' },
|
||||||
|
@ -146,6 +170,7 @@ describe('Test: logout', () => {
|
||||||
const { data } = await gcall<{ logout: boolean }>({
|
const { data } = await gcall<{ logout: boolean }>({
|
||||||
source: 'mutation { logout }',
|
source: 'mutation { logout }',
|
||||||
userId: user1.id,
|
userId: user1.id,
|
||||||
|
session: 'session',
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(data?.logout).toBeTruthy();
|
expect(data?.logout).toBeTruthy();
|
||||||
|
@ -171,3 +196,39 @@ describe('Test: isConfigured', () => {
|
||||||
expect(data?.isConfigured).toBeTruthy();
|
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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
|
@ -1,15 +1,21 @@
|
||||||
import * as argon2 from 'argon2';
|
import * as argon2 from 'argon2';
|
||||||
|
import jwt from 'jsonwebtoken';
|
||||||
import AuthService from '../auth.service';
|
import AuthService from '../auth.service';
|
||||||
import { createUser } from './user.factory';
|
import { createUser } from './user.factory';
|
||||||
import User from '../user.entity';
|
import User from '../user.entity';
|
||||||
import { faker } from '@faker-js/faker';
|
import { faker } from '@faker-js/faker';
|
||||||
import { setupConnection, teardownConnection } from '../../../test/connection';
|
import { setupConnection, teardownConnection } from '../../../test/connection';
|
||||||
import { DataSource } from 'typeorm';
|
import { DataSource } from 'typeorm';
|
||||||
|
import { setConfig } from '../../../core/config/TipiConfig';
|
||||||
|
import TipiCache from '../../../config/TipiCache';
|
||||||
|
|
||||||
let db: DataSource | null = null;
|
let db: DataSource | null = null;
|
||||||
const TEST_SUITE = 'authservice';
|
const TEST_SUITE = 'authservice';
|
||||||
|
|
||||||
|
jest.mock('redis');
|
||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
|
setConfig('jwtSecret', 'test');
|
||||||
db = await setupConnection(TEST_SUITE);
|
db = await setupConnection(TEST_SUITE);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -23,14 +29,24 @@ afterAll(async () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('Login', () => {
|
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();
|
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();
|
// Assert
|
||||||
expect(user?.id).toBe(1);
|
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 () => {
|
it('Should throw if user does not exist', async () => {
|
||||||
|
@ -45,26 +61,41 @@ describe('Login', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('Register', () => {
|
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 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 () => {
|
it('Should correctly trim and lowercase email', async () => {
|
||||||
|
// Arrange
|
||||||
const email = faker.internet.email();
|
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() } });
|
const user = await User.findOne({ where: { username: email.toLowerCase().trim() } });
|
||||||
|
|
||||||
|
// Assert
|
||||||
expect(user).toBeDefined();
|
expect(user).toBeDefined();
|
||||||
expect(user?.username).toBe(email.toLowerCase().trim());
|
expect(user?.username).toBe(email.toLowerCase().trim());
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Should throw if user already exists', async () => {
|
it('Should throw if user already exists', async () => {
|
||||||
|
// Arrange
|
||||||
const email = faker.internet.email();
|
const email = faker.internet.email();
|
||||||
|
|
||||||
|
// Act & Assert
|
||||||
await createUser(email);
|
await createUser(email);
|
||||||
await expect(AuthService.register({ username: email, password: 'test' })).rejects.toThrowError('User already exists');
|
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 () => {
|
it('Password is correctly hashed', async () => {
|
||||||
const email = faker.internet.email();
|
// Arrange
|
||||||
const { user } = await AuthService.register({ username: email, password: 'test' });
|
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');
|
const isPasswordValid = await argon2.verify(user?.password || '', 'test');
|
||||||
|
|
||||||
|
// Assert
|
||||||
expect(isPasswordValid).toBe(true);
|
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');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { Arg, Ctx, Mutation, Query, Resolver } from 'type-graphql';
|
import { Arg, Ctx, Mutation, Query, Resolver } from 'type-graphql';
|
||||||
import { MyContext } from '../../types';
|
import { MyContext } from '../../types';
|
||||||
import { UsernamePasswordInput, UserResponse } from './auth.types';
|
import { TokenResponse, UsernamePasswordInput } from './auth.types';
|
||||||
|
|
||||||
import AuthService from './auth.service';
|
import AuthService from './auth.service';
|
||||||
import User from './user.entity';
|
import User from './user.entity';
|
||||||
|
@ -9,34 +9,31 @@ import User from './user.entity';
|
||||||
export default class AuthResolver {
|
export default class AuthResolver {
|
||||||
@Query(() => User, { nullable: true })
|
@Query(() => User, { nullable: true })
|
||||||
async me(@Ctx() ctx: MyContext): Promise<User | null> {
|
async me(@Ctx() ctx: MyContext): Promise<User | null> {
|
||||||
return AuthService.me(ctx.req.session.userId);
|
return AuthService.me(ctx.req?.session?.userId);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Mutation(() => UserResponse)
|
@Mutation(() => TokenResponse)
|
||||||
async register(@Arg('input', () => UsernamePasswordInput) input: UsernamePasswordInput, @Ctx() { req }: MyContext): Promise<UserResponse> {
|
async register(@Arg('input', () => UsernamePasswordInput) input: UsernamePasswordInput): Promise<TokenResponse> {
|
||||||
const { user } = await AuthService.register(input);
|
const { token } = await AuthService.register(input);
|
||||||
|
|
||||||
if (user) {
|
return { token };
|
||||||
req.session.userId = user.id;
|
|
||||||
}
|
|
||||||
|
|
||||||
return { user };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Mutation(() => UserResponse)
|
@Mutation(() => TokenResponse)
|
||||||
async login(@Arg('input', () => UsernamePasswordInput) input: UsernamePasswordInput, @Ctx() { req }: MyContext): Promise<UserResponse> {
|
async login(@Arg('input', () => UsernamePasswordInput) input: UsernamePasswordInput): Promise<TokenResponse> {
|
||||||
const { user } = await AuthService.login(input);
|
const { token } = await AuthService.login(input);
|
||||||
|
|
||||||
if (user) {
|
return { token };
|
||||||
req.session.userId = user.id;
|
|
||||||
}
|
|
||||||
|
|
||||||
return { user };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Mutation(() => Boolean)
|
@Mutation(() => Boolean)
|
||||||
logout(@Ctx() { req }: MyContext): boolean {
|
async logout(@Ctx() { req }: MyContext): Promise<boolean> {
|
||||||
|
if (req.session.id) {
|
||||||
|
await AuthService.logout(req.session?.id);
|
||||||
|
}
|
||||||
|
|
||||||
req.session.userId = undefined;
|
req.session.userId = undefined;
|
||||||
|
req.session.id = undefined;
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
@ -47,4 +44,11 @@ export default class AuthResolver {
|
||||||
|
|
||||||
return users.length > 0;
|
return users.length > 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Query(() => TokenResponse, { nullable: true })
|
||||||
|
async refreshToken(@Ctx() { req }: MyContext): Promise<TokenResponse | null> {
|
||||||
|
const res = await AuthService.refreshToken(req.session?.id);
|
||||||
|
|
||||||
|
return res;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,9 +1,13 @@
|
||||||
import * as argon2 from 'argon2';
|
import * as argon2 from 'argon2';
|
||||||
|
import { v4 } from 'uuid';
|
||||||
|
import jwt from 'jsonwebtoken';
|
||||||
import validator from 'validator';
|
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 User from './user.entity';
|
||||||
|
import TipiCache from '../../config/TipiCache';
|
||||||
|
|
||||||
const login = async (input: UsernamePasswordInput): Promise<UserResponse> => {
|
const login = async (input: UsernamePasswordInput): Promise<TokenResponse> => {
|
||||||
const { password, username } = input;
|
const { password, username } = input;
|
||||||
|
|
||||||
const user = await User.findOne({ where: { username: username.trim().toLowerCase() } });
|
const user = await User.findOne({ where: { username: username.trim().toLowerCase() } });
|
||||||
|
@ -18,10 +22,15 @@ const login = async (input: UsernamePasswordInput): Promise<UserResponse> => {
|
||||||
throw new Error('Wrong password');
|
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<UserResponse> => {
|
const register = async (input: UsernamePasswordInput): Promise<TokenResponse> => {
|
||||||
const { password, username } = input;
|
const { password, username } = input;
|
||||||
const email = username.trim().toLowerCase();
|
const email = username.trim().toLowerCase();
|
||||||
|
|
||||||
|
@ -42,7 +51,12 @@ const register = async (input: UsernamePasswordInput): Promise<UserResponse> =>
|
||||||
const hash = await argon2.hash(password);
|
const hash = await argon2.hash(password);
|
||||||
const newUser = await User.create({ username: email, password: hash }).save();
|
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<User | null> => {
|
const me = async (userId?: number): Promise<User | null> => {
|
||||||
|
@ -55,10 +69,34 @@ const me = async (userId?: number): Promise<User | null> => {
|
||||||
return user;
|
return user;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const logout = async (session: string): Promise<boolean> => {
|
||||||
|
await TipiCache.del(session);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const refreshToken = async (session?: string): Promise<TokenResponse | null> => {
|
||||||
|
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 = {
|
const AuthService = {
|
||||||
login,
|
login,
|
||||||
register,
|
register,
|
||||||
me,
|
me,
|
||||||
|
logout,
|
||||||
|
refreshToken,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default AuthService;
|
export default AuthService;
|
||||||
|
|
|
@ -11,9 +11,9 @@ class UsernamePasswordInput {
|
||||||
}
|
}
|
||||||
|
|
||||||
@ObjectType()
|
@ObjectType()
|
||||||
class UserResponse {
|
class TokenResponse {
|
||||||
@Field(() => User, { nullable: true })
|
@Field(() => String, { nullable: false })
|
||||||
user?: User;
|
token!: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export { UsernamePasswordInput, UserResponse };
|
export { UsernamePasswordInput, TokenResponse };
|
||||||
|
|
|
@ -16,6 +16,7 @@ import EventDispatcher from '../../../core/config/EventDispatcher';
|
||||||
|
|
||||||
jest.mock('fs-extra');
|
jest.mock('fs-extra');
|
||||||
jest.mock('axios');
|
jest.mock('axios');
|
||||||
|
jest.mock('redis');
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
jest.resetModules();
|
jest.resetModules();
|
||||||
|
@ -42,8 +43,6 @@ beforeEach(async () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('Test: systemInfo', () => {
|
describe('Test: systemInfo', () => {
|
||||||
beforeEach(async () => {});
|
|
||||||
|
|
||||||
it('Should return correct system info from file', async () => {
|
it('Should return correct system info from file', async () => {
|
||||||
const systemInfo = {
|
const systemInfo = {
|
||||||
cpu: { load: 10 },
|
cpu: { load: 10 },
|
||||||
|
|
|
@ -10,6 +10,7 @@ import EventDispatcher from '../../../core/config/EventDispatcher';
|
||||||
|
|
||||||
jest.mock('fs-extra');
|
jest.mock('fs-extra');
|
||||||
jest.mock('axios');
|
jest.mock('axios');
|
||||||
|
jest.mock('redis');
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
jest.resetModules();
|
jest.resetModules();
|
||||||
|
|
|
@ -37,16 +37,15 @@ const systemInfo = (): z.infer<typeof systemInfoSchema> => {
|
||||||
|
|
||||||
const getVersion = async (): Promise<{ current: string; latest?: string }> => {
|
const getVersion = async (): Promise<{ current: string; latest?: string }> => {
|
||||||
try {
|
try {
|
||||||
let version = TipiCache.get<string>('latestVersion');
|
let version = await TipiCache.get('latestVersion');
|
||||||
|
|
||||||
if (!version) {
|
if (!version) {
|
||||||
const { data } = await axios.get('https://api.github.com/repos/meienberger/runtipi/releases/latest');
|
const { data } = await axios.get('https://api.github.com/repos/meienberger/runtipi/releases/latest');
|
||||||
|
|
||||||
TipiCache.set('latestVersion', data.name);
|
|
||||||
version = data.name.replace('v', '');
|
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', '') };
|
return { current: getConfig().version, latest: version?.replace('v', '') };
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|
|
@ -9,7 +9,6 @@ import logger from './config/logger/logger';
|
||||||
import getSessionMiddleware from './core/middlewares/sessionMiddleware';
|
import getSessionMiddleware from './core/middlewares/sessionMiddleware';
|
||||||
import { MyContext } from './types';
|
import { MyContext } from './types';
|
||||||
import { __prod__ } from './config/constants/constants';
|
import { __prod__ } from './config/constants/constants';
|
||||||
import cors from 'cors';
|
|
||||||
import datasource from './config/datasource';
|
import datasource from './config/datasource';
|
||||||
import appsService from './modules/apps/apps.service';
|
import appsService from './modules/apps/apps.service';
|
||||||
import { runUpdates } from './core/updates/run';
|
import { runUpdates } from './core/updates/run';
|
||||||
|
@ -19,28 +18,7 @@ import { applyJsonConfig, getConfig, setConfig } from './core/config/TipiConfig'
|
||||||
import { ZodError } from 'zod';
|
import { ZodError } from 'zod';
|
||||||
import systemController from './modules/system/system.controller';
|
import systemController from './modules/system/system.controller';
|
||||||
import { eventDispatcher, EventTypes } from './core/config/EventDispatcher';
|
import { eventDispatcher, EventTypes } from './core/config/EventDispatcher';
|
||||||
|
import cors from 'cors';
|
||||||
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);
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const applyCustomConfig = () => {
|
const applyCustomConfig = () => {
|
||||||
try {
|
try {
|
||||||
|
@ -64,9 +42,9 @@ const main = async () => {
|
||||||
const port = 3001;
|
const port = 3001;
|
||||||
|
|
||||||
app.use(express.static(`${getConfig().rootFolder}/repos/${getConfig().appsRepoId}`));
|
app.use(express.static(`${getConfig().rootFolder}/repos/${getConfig().appsRepoId}`));
|
||||||
|
app.use(cors());
|
||||||
app.use('/status', systemController.status);
|
app.use('/status', systemController.status);
|
||||||
app.use(cors(corsOptions));
|
app.use(getSessionMiddleware);
|
||||||
app.use(getSessionMiddleware());
|
|
||||||
|
|
||||||
await datasource.initialize();
|
await datasource.initialize();
|
||||||
|
|
||||||
|
@ -75,7 +53,7 @@ const main = async () => {
|
||||||
const plugins = [ApolloLogs];
|
const plugins = [ApolloLogs];
|
||||||
|
|
||||||
if (!__prod__) {
|
if (!__prod__) {
|
||||||
plugins.push(Playground({ settings: { 'request.credentials': 'include' } }));
|
plugins.push(Playground());
|
||||||
}
|
}
|
||||||
|
|
||||||
const apolloServer = new ApolloServer({
|
const apolloServer = new ApolloServer({
|
||||||
|
@ -85,7 +63,7 @@ const main = async () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
await apolloServer.start();
|
await apolloServer.start();
|
||||||
apolloServer.applyMiddleware({ app, cors: corsOptions });
|
apolloServer.applyMiddleware({ app });
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await datasource.runMigrations();
|
await datasource.runMigrations();
|
||||||
|
|
|
@ -8,11 +8,12 @@ interface Options {
|
||||||
[key: string]: any;
|
[key: string]: any;
|
||||||
}>;
|
}>;
|
||||||
userId?: number;
|
userId?: number;
|
||||||
|
session?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
let schema: GraphQLSchema | null = null;
|
let schema: GraphQLSchema | null = null;
|
||||||
|
|
||||||
export const gcall = async <T>({ source, variableValues, userId }: Options): Promise<ExecutionResult<T, { [key: string]: any }>> => {
|
export const gcall = async <T>({ source, variableValues, userId, session }: Options): Promise<ExecutionResult<T, { [key: string]: any }>> => {
|
||||||
if (!schema) {
|
if (!schema) {
|
||||||
schema = await createSchema();
|
schema = await createSchema();
|
||||||
}
|
}
|
||||||
|
@ -21,6 +22,6 @@ export const gcall = async <T>({ source, variableValues, userId }: Options): Pro
|
||||||
schema,
|
schema,
|
||||||
source,
|
source,
|
||||||
variableValues,
|
variableValues,
|
||||||
contextValue: { req: { session: { userId } } },
|
contextValue: { req: { session: { userId, id: session } } },
|
||||||
}) as any;
|
}) as any;
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,9 +1,6 @@
|
||||||
# Write your query or mutation here
|
# Write your query or mutation here
|
||||||
mutation Login($input: UsernamePasswordInput!) {
|
mutation Login($input: UsernamePasswordInput!) {
|
||||||
login(input: $input) {
|
login(input: $input) {
|
||||||
user {
|
token
|
||||||
id
|
|
||||||
username
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,9 +1,6 @@
|
||||||
# Write your query or mutation here
|
# Write your query or mutation here
|
||||||
mutation Register($input: UsernamePasswordInput!) {
|
mutation Register($input: UsernamePasswordInput!) {
|
||||||
register(input: $input) {
|
register(input: $input) {
|
||||||
user {
|
token
|
||||||
id
|
|
||||||
username
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,6 +9,7 @@ import * as Me from './me.graphql';
|
||||||
import * as isConfigured from './isConfigured.graphql';
|
import * as isConfigured from './isConfigured.graphql';
|
||||||
import * as systemInfo from './systemInfo.graphql';
|
import * as systemInfo from './systemInfo.graphql';
|
||||||
import * as version from './version.graphql';
|
import * as version from './version.graphql';
|
||||||
|
import * as refreshToken from './refreshToken.graphql';
|
||||||
|
|
||||||
export const listAppInfosQuery = print(listAppInfos);
|
export const listAppInfosQuery = print(listAppInfos);
|
||||||
export const getAppQuery = print(getApp);
|
export const getAppQuery = print(getApp);
|
||||||
|
@ -17,3 +18,4 @@ export const MeQuery = print(Me);
|
||||||
export const isConfiguredQuery = print(isConfigured);
|
export const isConfiguredQuery = print(isConfigured);
|
||||||
export const systemInfoQuery = print(systemInfo);
|
export const systemInfoQuery = print(systemInfo);
|
||||||
export const versionQuery = print(version);
|
export const versionQuery = print(version);
|
||||||
|
export const refreshTokenQuery = print(refreshToken);
|
||||||
|
|
|
@ -0,0 +1,6 @@
|
||||||
|
# Write your query or mutation here
|
||||||
|
query RefreshToken {
|
||||||
|
refreshToken {
|
||||||
|
token
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,11 +1,4 @@
|
||||||
import { Request, Response } from 'express';
|
import { Request, Response } from 'express';
|
||||||
import 'express-session';
|
|
||||||
|
|
||||||
declare module 'express-session' {
|
|
||||||
interface SessionData {
|
|
||||||
userId: number;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export type MyContext = {
|
export type MyContext = {
|
||||||
req: Request;
|
req: Request;
|
||||||
|
|
245
pnpm-lock.yaml
generated
245
pnpm-lock.yaml
generated
|
@ -124,15 +124,15 @@ importers:
|
||||||
'@swc/core': ^1.2.210
|
'@swc/core': ^1.2.210
|
||||||
'@types/cors': ^2.8.12
|
'@types/cors': ^2.8.12
|
||||||
'@types/express': ^4.17.13
|
'@types/express': ^4.17.13
|
||||||
'@types/express-session': ^1.17.4
|
|
||||||
'@types/fs-extra': ^9.0.13
|
'@types/fs-extra': ^9.0.13
|
||||||
'@types/jest': ^27.5.0
|
'@types/jest': ^27.5.0
|
||||||
|
'@types/jsonwebtoken': ^8.5.9
|
||||||
'@types/node': 17.0.31
|
'@types/node': 17.0.31
|
||||||
'@types/node-cron': ^3.0.2
|
'@types/node-cron': ^3.0.2
|
||||||
'@types/pg': ^8.6.5
|
'@types/pg': ^8.6.5
|
||||||
'@types/semver': ^7.3.12
|
'@types/semver': ^7.3.12
|
||||||
'@types/session-file-store': ^1.2.2
|
|
||||||
'@types/tcp-port-used': ^1.0.1
|
'@types/tcp-port-used': ^1.0.1
|
||||||
|
'@types/uuid': ^8.3.4
|
||||||
'@types/validator': ^13.7.2
|
'@types/validator': ^13.7.2
|
||||||
'@typescript-eslint/eslint-plugin': ^5.18.0
|
'@typescript-eslint/eslint-plugin': ^5.18.0
|
||||||
'@typescript-eslint/parser': ^5.22.0
|
'@typescript-eslint/parser': ^5.22.0
|
||||||
|
@ -150,7 +150,6 @@ importers:
|
||||||
eslint-plugin-import: ^2.26.0
|
eslint-plugin-import: ^2.26.0
|
||||||
eslint-plugin-prettier: ^4.0.0
|
eslint-plugin-prettier: ^4.0.0
|
||||||
express: ^4.17.3
|
express: ^4.17.3
|
||||||
express-session: ^1.17.3
|
|
||||||
fs-extra: ^10.1.0
|
fs-extra: ^10.1.0
|
||||||
graphql: ^15.3.0
|
graphql: ^15.3.0
|
||||||
graphql-import-node: ^0.0.5
|
graphql-import-node: ^0.0.5
|
||||||
|
@ -158,22 +157,24 @@ importers:
|
||||||
http: 0.0.1-security
|
http: 0.0.1-security
|
||||||
internal-ip: ^6.0.0
|
internal-ip: ^6.0.0
|
||||||
jest: ^28.1.0
|
jest: ^28.1.0
|
||||||
|
jsonwebtoken: ^8.5.1
|
||||||
node-cache: ^5.1.2
|
node-cache: ^5.1.2
|
||||||
node-cron: ^3.0.1
|
node-cron: ^3.0.1
|
||||||
node-port-scanner: ^3.0.1
|
node-port-scanner: ^3.0.1
|
||||||
nodemon: ^2.0.15
|
nodemon: ^2.0.15
|
||||||
pg: ^8.7.3
|
pg: ^8.7.3
|
||||||
prettier: 2.6.2
|
prettier: 2.6.2
|
||||||
|
redis: ^4.3.1
|
||||||
reflect-metadata: ^0.1.13
|
reflect-metadata: ^0.1.13
|
||||||
rimraf: ^3.0.2
|
rimraf: ^3.0.2
|
||||||
semver: ^7.3.7
|
semver: ^7.3.7
|
||||||
session-file-store: ^1.5.0
|
|
||||||
tcp-port-used: ^1.0.2
|
tcp-port-used: ^1.0.2
|
||||||
ts-jest: ^28.0.2
|
ts-jest: ^28.0.2
|
||||||
ts-node: ^10.8.2
|
ts-node: ^10.8.2
|
||||||
type-graphql: ^1.1.1
|
type-graphql: ^1.1.1
|
||||||
typeorm: ^0.3.6
|
typeorm: ^0.3.6
|
||||||
typescript: 4.6.4
|
typescript: 4.6.4
|
||||||
|
uuid: ^9.0.0
|
||||||
validator: ^13.7.0
|
validator: ^13.7.0
|
||||||
winston: ^3.7.2
|
winston: ^3.7.2
|
||||||
zod: ^3.19.1
|
zod: ^3.19.1
|
||||||
|
@ -186,22 +187,23 @@ importers:
|
||||||
cors: 2.8.5
|
cors: 2.8.5
|
||||||
dotenv: 16.0.0
|
dotenv: 16.0.0
|
||||||
express: 4.18.1
|
express: 4.18.1
|
||||||
express-session: 1.17.3
|
|
||||||
fs-extra: 10.1.0
|
fs-extra: 10.1.0
|
||||||
graphql: 15.8.0
|
graphql: 15.8.0
|
||||||
graphql-type-json: 0.3.2_graphql@15.8.0
|
graphql-type-json: 0.3.2_graphql@15.8.0
|
||||||
http: 0.0.1-security
|
http: 0.0.1-security
|
||||||
internal-ip: 6.2.0
|
internal-ip: 6.2.0
|
||||||
|
jsonwebtoken: 8.5.1
|
||||||
node-cache: 5.1.2
|
node-cache: 5.1.2
|
||||||
node-cron: 3.0.1
|
node-cron: 3.0.1
|
||||||
node-port-scanner: 3.0.1
|
node-port-scanner: 3.0.1
|
||||||
pg: 8.7.3
|
pg: 8.7.3
|
||||||
|
redis: 4.3.1
|
||||||
reflect-metadata: 0.1.13
|
reflect-metadata: 0.1.13
|
||||||
semver: 7.3.7
|
semver: 7.3.7
|
||||||
session-file-store: 1.5.0
|
|
||||||
tcp-port-used: 1.0.2
|
tcp-port-used: 1.0.2
|
||||||
type-graphql: 1.1.1_v2revtygxcm7xrdg2oz3ssohfu
|
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
|
validator: 13.7.0
|
||||||
winston: 3.7.2
|
winston: 3.7.2
|
||||||
zod: 3.19.1
|
zod: 3.19.1
|
||||||
|
@ -211,15 +213,15 @@ importers:
|
||||||
'@swc/core': 1.2.210
|
'@swc/core': 1.2.210
|
||||||
'@types/cors': 2.8.12
|
'@types/cors': 2.8.12
|
||||||
'@types/express': 4.17.13
|
'@types/express': 4.17.13
|
||||||
'@types/express-session': 1.17.4
|
|
||||||
'@types/fs-extra': 9.0.13
|
'@types/fs-extra': 9.0.13
|
||||||
'@types/jest': 27.5.0
|
'@types/jest': 27.5.0
|
||||||
|
'@types/jsonwebtoken': 8.5.9
|
||||||
'@types/node': 17.0.31
|
'@types/node': 17.0.31
|
||||||
'@types/node-cron': 3.0.2
|
'@types/node-cron': 3.0.2
|
||||||
'@types/pg': 8.6.5
|
'@types/pg': 8.6.5
|
||||||
'@types/semver': 7.3.12
|
'@types/semver': 7.3.12
|
||||||
'@types/session-file-store': 1.2.2
|
|
||||||
'@types/tcp-port-used': 1.0.1
|
'@types/tcp-port-used': 1.0.1
|
||||||
|
'@types/uuid': 8.3.4
|
||||||
'@types/validator': 13.7.2
|
'@types/validator': 13.7.2
|
||||||
'@typescript-eslint/eslint-plugin': 5.22.0_tal4xlmvnofklupd3hwjtzfb4q
|
'@typescript-eslint/eslint-plugin': 5.22.0_tal4xlmvnofklupd3hwjtzfb4q
|
||||||
'@typescript-eslint/parser': 5.22.0_hcfsmds2fshutdssjqluwm76uu
|
'@typescript-eslint/parser': 5.22.0_hcfsmds2fshutdssjqluwm76uu
|
||||||
|
@ -3369,6 +3371,55 @@ packages:
|
||||||
resolution: {integrity: sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==}
|
resolution: {integrity: sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==}
|
||||||
dev: false
|
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:
|
/@rushstack/eslint-patch/1.0.8:
|
||||||
resolution: {integrity: sha512-ZK5v4bJwgXldAUA8r3q9YKfCwOqoHTK/ZqRjSeRXQrBXWouoPnS4MQtgC4AXGiiBuUu5wxrRgTlv0ktmM4P1Aw==}
|
resolution: {integrity: sha512-ZK5v4bJwgXldAUA8r3q9YKfCwOqoHTK/ZqRjSeRXQrBXWouoPnS4MQtgC4AXGiiBuUu5wxrRgTlv0ktmM4P1Aw==}
|
||||||
dev: true
|
dev: true
|
||||||
|
@ -3691,12 +3742,6 @@ packages:
|
||||||
'@types/range-parser': 1.2.4
|
'@types/range-parser': 1.2.4
|
||||||
dev: false
|
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:
|
/@types/express/4.17.13:
|
||||||
resolution: {integrity: sha512-6bSZTPaTIACxn48l50SR+axgrqm6qXFIxrdAKaG6PaJk3+zuUr35hBlgT7vOmJcum+OEaIBLtHV/qloEAFITeA==}
|
resolution: {integrity: sha512-6bSZTPaTIACxn48l50SR+axgrqm6qXFIxrdAKaG6PaJk3+zuUr35hBlgT7vOmJcum+OEaIBLtHV/qloEAFITeA==}
|
||||||
dependencies:
|
dependencies:
|
||||||
|
@ -3779,6 +3824,12 @@ packages:
|
||||||
'@types/node': 17.0.31
|
'@types/node': 17.0.31
|
||||||
dev: true
|
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:
|
/@types/keyv/3.1.4:
|
||||||
resolution: {integrity: sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg==}
|
resolution: {integrity: sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg==}
|
||||||
dependencies:
|
dependencies:
|
||||||
|
@ -3906,13 +3957,6 @@ packages:
|
||||||
'@types/mime': 1.3.2
|
'@types/mime': 1.3.2
|
||||||
'@types/node': 17.0.31
|
'@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:
|
/@types/stack-utils/2.0.1:
|
||||||
resolution: {integrity: sha512-Hl219/BT5fLAaz6NDkSuhzasy49dwQS/DSdu4MdggFB8zcXv7vflBI3xp7FEmkmdDkBUI2bPUNeMttp2knYdxw==}
|
resolution: {integrity: sha512-Hl219/BT5fLAaz6NDkSuhzasy49dwQS/DSdu4MdggFB8zcXv7vflBI3xp7FEmkmdDkBUI2bPUNeMttp2knYdxw==}
|
||||||
dev: true
|
dev: true
|
||||||
|
@ -3925,6 +3969,10 @@ packages:
|
||||||
resolution: {integrity: sha512-PBjIUxZHOuj0R15/xuwJYjFi+KZdNFrehocChv4g5hu6aFroHue8m0lBP0POdK2nKzbw0cgV1mws8+V/JAcEkQ==}
|
resolution: {integrity: sha512-PBjIUxZHOuj0R15/xuwJYjFi+KZdNFrehocChv4g5hu6aFroHue8m0lBP0POdK2nKzbw0cgV1mws8+V/JAcEkQ==}
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/@types/uuid/8.3.4:
|
||||||
|
resolution: {integrity: sha512-c/I8ZRb51j+pYGAu5CrFMRxqZ2ke4y2grEBO5AUjgSkSk+qT2Ea+OdWElz/OiMf5MNpn2b17kuVBwZLQJXzihw==}
|
||||||
|
dev: true
|
||||||
|
|
||||||
/@types/validator/13.7.2:
|
/@types/validator/13.7.2:
|
||||||
resolution: {integrity: sha512-KFcchQ3h0OPQgFirBRPZr5F/sVjxZsOrQHedj3zi8AH3Zv/hOLx2OLR4hxR5HcfoU+33n69ZuOfzthKVdMoTiw==}
|
resolution: {integrity: sha512-KFcchQ3h0OPQgFirBRPZr5F/sVjxZsOrQHedj3zi8AH3Zv/hOLx2OLR4hxR5HcfoU+33n69ZuOfzthKVdMoTiw==}
|
||||||
dev: true
|
dev: true
|
||||||
|
@ -4662,15 +4710,6 @@ packages:
|
||||||
resolution: {integrity: sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==}
|
resolution: {integrity: sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==}
|
||||||
dev: true
|
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:
|
/ast-types-flow/0.0.7:
|
||||||
resolution: {integrity: sha1-9wtzXGvKGlycItmCw+Oef+ujva0=}
|
resolution: {integrity: sha1-9wtzXGvKGlycItmCw+Oef+ujva0=}
|
||||||
dev: true
|
dev: true
|
||||||
|
@ -4859,10 +4898,6 @@ packages:
|
||||||
babel-preset-current-node-syntax: 1.0.1_@babel+core@7.17.10
|
babel-preset-current-node-syntax: 1.0.1_@babel+core@7.17.10
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
/bagpipe/0.3.5:
|
|
||||||
resolution: {integrity: sha512-42sAlmPDKes1nLm/aly+0VdaopSU9br+jkRELedhQxI5uXHgtk47I83Mpmf4zoNTRMASdLFtUkimlu/Z9zQ8+g==}
|
|
||||||
dev: false
|
|
||||||
|
|
||||||
/bail/2.0.2:
|
/bail/2.0.2:
|
||||||
resolution: {integrity: sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==}
|
resolution: {integrity: sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==}
|
||||||
dev: false
|
dev: false
|
||||||
|
@ -4886,10 +4921,6 @@ packages:
|
||||||
readable-stream: 3.6.0
|
readable-stream: 3.6.0
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
/bn.js/4.12.0:
|
|
||||||
resolution: {integrity: sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==}
|
|
||||||
dev: false
|
|
||||||
|
|
||||||
/body-parser/1.20.0:
|
/body-parser/1.20.0:
|
||||||
resolution: {integrity: sha512-DfJ+q6EPcGKZD1QWUjSpqp+Q7bDQTsQIF4zfUAtZ6qk+H/3/QRhg9CEp39ss+/T2vw0+HaidC0ecJj/DRLIaKg==}
|
resolution: {integrity: sha512-DfJ+q6EPcGKZD1QWUjSpqp+Q7bDQTsQIF4zfUAtZ6qk+H/3/QRhg9CEp39ss+/T2vw0+HaidC0ecJj/DRLIaKg==}
|
||||||
engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16}
|
engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16}
|
||||||
|
@ -4963,8 +4994,7 @@ packages:
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
/buffer-equal-constant-time/1.0.1:
|
/buffer-equal-constant-time/1.0.1:
|
||||||
resolution: {integrity: sha1-+OcRMvf/5uAaXJaXpMbz5I1cyBk=}
|
resolution: {integrity: sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==}
|
||||||
dev: true
|
|
||||||
|
|
||||||
/buffer-from/1.1.2:
|
/buffer-from/1.1.2:
|
||||||
resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==}
|
resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==}
|
||||||
|
@ -5288,6 +5318,11 @@ packages:
|
||||||
engines: {node: '>=6'}
|
engines: {node: '>=6'}
|
||||||
dev: false
|
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:
|
/co/4.6.0:
|
||||||
resolution: {integrity: sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==}
|
resolution: {integrity: sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==}
|
||||||
engines: {iojs: '>= 1.0.0', node: '>= 0.12.0'}
|
engines: {iojs: '>= 1.0.0', node: '>= 0.12.0'}
|
||||||
|
@ -5506,11 +5541,6 @@ packages:
|
||||||
resolution: {integrity: sha1-4wOogrNCzD7oylE6eZmXNNqzriw=}
|
resolution: {integrity: sha1-4wOogrNCzD7oylE6eZmXNNqzriw=}
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
/cookie/0.4.2:
|
|
||||||
resolution: {integrity: sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA==}
|
|
||||||
engines: {node: '>= 0.6'}
|
|
||||||
dev: false
|
|
||||||
|
|
||||||
/cookie/0.5.0:
|
/cookie/0.5.0:
|
||||||
resolution: {integrity: sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==}
|
resolution: {integrity: sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==}
|
||||||
engines: {node: '>= 0.6'}
|
engines: {node: '>= 0.6'}
|
||||||
|
@ -5957,7 +5987,6 @@ packages:
|
||||||
resolution: {integrity: sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==}
|
resolution: {integrity: sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==}
|
||||||
dependencies:
|
dependencies:
|
||||||
safe-buffer: 5.2.1
|
safe-buffer: 5.2.1
|
||||||
dev: true
|
|
||||||
|
|
||||||
/ee-first/1.1.1:
|
/ee-first/1.1.1:
|
||||||
resolution: {integrity: sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=}
|
resolution: {integrity: sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=}
|
||||||
|
@ -6663,22 +6692,6 @@ packages:
|
||||||
jest-util: 28.1.0
|
jest-util: 28.1.0
|
||||||
dev: true
|
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:
|
/express/4.18.1:
|
||||||
resolution: {integrity: sha512-zZBcOX9TfehHQhtupq57OF8lFZ3UZi08Y97dwFCkD8p9d/d2Y3M+ykKcwaMDEL+4qyUolgBDX6AblpR3fL212Q==}
|
resolution: {integrity: sha512-zZBcOX9TfehHQhtupq57OF8lFZ3UZi08Y97dwFCkD8p9d/d2Y3M+ykKcwaMDEL+4qyUolgBDX6AblpR3fL212Q==}
|
||||||
engines: {node: '>= 0.10.0'}
|
engines: {node: '>= 0.10.0'}
|
||||||
|
@ -7006,15 +7019,6 @@ packages:
|
||||||
jsonfile: 6.1.0
|
jsonfile: 6.1.0
|
||||||
universalify: 2.0.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:
|
/fs-extra/9.1.0:
|
||||||
resolution: {integrity: sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==}
|
resolution: {integrity: sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==}
|
||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
|
@ -7079,6 +7083,11 @@ packages:
|
||||||
wide-align: 1.1.5
|
wide-align: 1.1.5
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/generic-pool/3.8.2:
|
||||||
|
resolution: {integrity: sha512-nGToKy6p3PAbYQ7p1UlWl6vSPwfwU6TMSWK7TTu+WUY4ZjyZQGniGGt2oNVvyNSpyZYSB43zMXVLcBm08MTMkg==}
|
||||||
|
engines: {node: '>= 4'}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/gensync/1.0.0-beta.2:
|
/gensync/1.0.0-beta.2:
|
||||||
resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==}
|
resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==}
|
||||||
engines: {node: '>=6.9.0'}
|
engines: {node: '>=6.9.0'}
|
||||||
|
@ -7577,6 +7586,7 @@ packages:
|
||||||
/imurmurhash/0.1.4:
|
/imurmurhash/0.1.4:
|
||||||
resolution: {integrity: sha1-khi5srkoojixPcT7a21XbyMUU+o=}
|
resolution: {integrity: sha1-khi5srkoojixPcT7a21XbyMUU+o=}
|
||||||
engines: {node: '>=0.8.19'}
|
engines: {node: '>=0.8.19'}
|
||||||
|
dev: true
|
||||||
|
|
||||||
/indent-string/3.2.0:
|
/indent-string/3.2.0:
|
||||||
resolution: {integrity: sha512-BYqTHXTGUIvg7t1r4sJNKcbDZkL92nkXA8YtRpbjFHRHGDL/NtUeiBJMeE60kIFN/Mg8ESaWQvftaYMGJzQZCQ==}
|
resolution: {integrity: sha512-BYqTHXTGUIvg7t1r4sJNKcbDZkL92nkXA8YtRpbjFHRHGDL/NtUeiBJMeE60kIFN/Mg8ESaWQvftaYMGJzQZCQ==}
|
||||||
|
@ -7914,6 +7924,7 @@ packages:
|
||||||
|
|
||||||
/is-typedarray/1.0.0:
|
/is-typedarray/1.0.0:
|
||||||
resolution: {integrity: sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==}
|
resolution: {integrity: sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==}
|
||||||
|
dev: true
|
||||||
|
|
||||||
/is-unc-path/1.0.0:
|
/is-unc-path/1.0.0:
|
||||||
resolution: {integrity: sha512-mrGpVd0fs7WWLfVsStvgF6iEJnbjDFZh9/emhRDcGWTduTfNHd9CHeUwH3gYIjdbwo4On6hunkztwOaAw0yllQ==}
|
resolution: {integrity: sha512-mrGpVd0fs7WWLfVsStvgF6iEJnbjDFZh9/emhRDcGWTduTfNHd9CHeUwH3gYIjdbwo4On6hunkztwOaAw0yllQ==}
|
||||||
|
@ -8621,12 +8632,6 @@ packages:
|
||||||
hasBin: true
|
hasBin: true
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
/jsonfile/4.0.0:
|
|
||||||
resolution: {integrity: sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==}
|
|
||||||
optionalDependencies:
|
|
||||||
graceful-fs: 4.2.10
|
|
||||||
dev: false
|
|
||||||
|
|
||||||
/jsonfile/6.1.0:
|
/jsonfile/6.1.0:
|
||||||
resolution: {integrity: sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==}
|
resolution: {integrity: sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==}
|
||||||
dependencies:
|
dependencies:
|
||||||
|
@ -8657,7 +8662,6 @@ packages:
|
||||||
lodash.once: 4.1.1
|
lodash.once: 4.1.1
|
||||||
ms: 2.1.3
|
ms: 2.1.3
|
||||||
semver: 5.7.1
|
semver: 5.7.1
|
||||||
dev: true
|
|
||||||
|
|
||||||
/jsx-ast-utils/3.3.0:
|
/jsx-ast-utils/3.3.0:
|
||||||
resolution: {integrity: sha512-XzO9luP6L0xkxwhIJMTJQpZo/eeN60K08jHdexfD569AGxeNug6UketeHXEhROoM8aR7EcUoOQmIhcJQjcuq8Q==}
|
resolution: {integrity: sha512-XzO9luP6L0xkxwhIJMTJQpZo/eeN60K08jHdexfD569AGxeNug6UketeHXEhROoM8aR7EcUoOQmIhcJQjcuq8Q==}
|
||||||
|
@ -8673,14 +8677,12 @@ packages:
|
||||||
buffer-equal-constant-time: 1.0.1
|
buffer-equal-constant-time: 1.0.1
|
||||||
ecdsa-sig-formatter: 1.0.11
|
ecdsa-sig-formatter: 1.0.11
|
||||||
safe-buffer: 5.2.1
|
safe-buffer: 5.2.1
|
||||||
dev: true
|
|
||||||
|
|
||||||
/jws/3.2.2:
|
/jws/3.2.2:
|
||||||
resolution: {integrity: sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==}
|
resolution: {integrity: sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==}
|
||||||
dependencies:
|
dependencies:
|
||||||
jwa: 1.4.1
|
jwa: 1.4.1
|
||||||
safe-buffer: 5.2.1
|
safe-buffer: 5.2.1
|
||||||
dev: true
|
|
||||||
|
|
||||||
/keyv/3.1.0:
|
/keyv/3.1.0:
|
||||||
resolution: {integrity: sha512-9ykJ/46SN/9KPM/sichzQ7OvXyGDYKGTaDlKMGCAlg2UK8KRy4jb0d8sFc+0Tt0YYnThq8X2RZgCg74RPxgcVA==}
|
resolution: {integrity: sha512-9ykJ/46SN/9KPM/sichzQ7OvXyGDYKGTaDlKMGCAlg2UK8KRy4jb0d8sFc+0Tt0YYnThq8X2RZgCg74RPxgcVA==}
|
||||||
|
@ -8703,13 +8705,6 @@ packages:
|
||||||
engines: {node: '>=6'}
|
engines: {node: '>=6'}
|
||||||
dev: false
|
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:
|
/kuler/2.0.0:
|
||||||
resolution: {integrity: sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A==}
|
resolution: {integrity: sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A==}
|
||||||
dev: false
|
dev: false
|
||||||
|
@ -8833,27 +8828,21 @@ packages:
|
||||||
|
|
||||||
/lodash.includes/4.3.0:
|
/lodash.includes/4.3.0:
|
||||||
resolution: {integrity: sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==}
|
resolution: {integrity: sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==}
|
||||||
dev: true
|
|
||||||
|
|
||||||
/lodash.isboolean/3.0.3:
|
/lodash.isboolean/3.0.3:
|
||||||
resolution: {integrity: sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==}
|
resolution: {integrity: sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==}
|
||||||
dev: true
|
|
||||||
|
|
||||||
/lodash.isinteger/4.0.4:
|
/lodash.isinteger/4.0.4:
|
||||||
resolution: {integrity: sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==}
|
resolution: {integrity: sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==}
|
||||||
dev: true
|
|
||||||
|
|
||||||
/lodash.isnumber/3.0.3:
|
/lodash.isnumber/3.0.3:
|
||||||
resolution: {integrity: sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==}
|
resolution: {integrity: sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==}
|
||||||
dev: true
|
|
||||||
|
|
||||||
/lodash.isplainobject/4.0.6:
|
/lodash.isplainobject/4.0.6:
|
||||||
resolution: {integrity: sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==}
|
resolution: {integrity: sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==}
|
||||||
dev: true
|
|
||||||
|
|
||||||
/lodash.isstring/4.0.1:
|
/lodash.isstring/4.0.1:
|
||||||
resolution: {integrity: sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==}
|
resolution: {integrity: sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==}
|
||||||
dev: true
|
|
||||||
|
|
||||||
/lodash.map/4.6.0:
|
/lodash.map/4.6.0:
|
||||||
resolution: {integrity: sha512-worNHGKLDetmcEYDvh2stPCrrQRkP20E4l0iIS7F8EvzMqBBi7ltvFN5m1HvTf1P7Jk1txKhvFcmYsCr8O2F1Q==}
|
resolution: {integrity: sha512-worNHGKLDetmcEYDvh2stPCrrQRkP20E4l0iIS7F8EvzMqBBi7ltvFN5m1HvTf1P7Jk1txKhvFcmYsCr8O2F1Q==}
|
||||||
|
@ -8873,7 +8862,6 @@ packages:
|
||||||
|
|
||||||
/lodash.once/4.1.1:
|
/lodash.once/4.1.1:
|
||||||
resolution: {integrity: sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==}
|
resolution: {integrity: sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==}
|
||||||
dev: true
|
|
||||||
|
|
||||||
/lodash.sortby/4.7.0:
|
/lodash.sortby/4.7.0:
|
||||||
resolution: {integrity: sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA==}
|
resolution: {integrity: sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA==}
|
||||||
|
@ -9617,10 +9605,6 @@ packages:
|
||||||
engines: {node: '>=4'}
|
engines: {node: '>=4'}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
/minimalistic-assert/1.0.1:
|
|
||||||
resolution: {integrity: sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==}
|
|
||||||
dev: false
|
|
||||||
|
|
||||||
/minimatch/3.1.2:
|
/minimatch/3.1.2:
|
||||||
resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==}
|
resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==}
|
||||||
dependencies:
|
dependencies:
|
||||||
|
@ -9968,11 +9952,6 @@ packages:
|
||||||
ee-first: 1.1.1
|
ee-first: 1.1.1
|
||||||
dev: false
|
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:
|
/once/1.4.0:
|
||||||
resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==}
|
resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==}
|
||||||
dependencies:
|
dependencies:
|
||||||
|
@ -10545,11 +10524,6 @@ packages:
|
||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
/random-bytes/1.0.0:
|
|
||||||
resolution: {integrity: sha512-iv7LhNVO047HzYR3InF6pUcUsPQiHTM1Qal51DcGSuZFBil1aBBWG5eHPNek7bvILMaYJ/8RU1e8w1AMdHmLQQ==}
|
|
||||||
engines: {node: '>= 0.8'}
|
|
||||||
dev: false
|
|
||||||
|
|
||||||
/range-parser/1.2.1:
|
/range-parser/1.2.1:
|
||||||
resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==}
|
resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==}
|
||||||
engines: {node: '>= 0.6'}
|
engines: {node: '>= 0.6'}
|
||||||
|
@ -10808,6 +10782,17 @@ packages:
|
||||||
strip-indent: 3.0.0
|
strip-indent: 3.0.0
|
||||||
dev: true
|
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:
|
/reflect-metadata/0.1.13:
|
||||||
resolution: {integrity: sha512-Ts1Y/anZELhSsjMcU605fU9RE4Oi3p5ORujwbIKXfWa+0Zxs510Qrmrce5/Jowq3cHSZSJqBjypxmHarc+vEWg==}
|
resolution: {integrity: sha512-Ts1Y/anZELhSsjMcU605fU9RE4Oi3p5ORujwbIKXfWa+0Zxs510Qrmrce5/Jowq3cHSZSJqBjypxmHarc+vEWg==}
|
||||||
dev: false
|
dev: false
|
||||||
|
@ -11003,11 +10988,6 @@ packages:
|
||||||
signal-exit: 3.0.7
|
signal-exit: 3.0.7
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
/retry/0.12.0:
|
|
||||||
resolution: {integrity: sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==}
|
|
||||||
engines: {node: '>= 4'}
|
|
||||||
dev: false
|
|
||||||
|
|
||||||
/retry/0.13.1:
|
/retry/0.13.1:
|
||||||
resolution: {integrity: sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==}
|
resolution: {integrity: sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==}
|
||||||
engines: {node: '>= 4'}
|
engines: {node: '>= 4'}
|
||||||
|
@ -11093,7 +11073,6 @@ packages:
|
||||||
/semver/5.7.1:
|
/semver/5.7.1:
|
||||||
resolution: {integrity: sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==}
|
resolution: {integrity: sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
dev: true
|
|
||||||
|
|
||||||
/semver/6.3.0:
|
/semver/6.3.0:
|
||||||
resolution: {integrity: sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==}
|
resolution: {integrity: sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==}
|
||||||
|
@ -11147,18 +11126,6 @@ packages:
|
||||||
- supports-color
|
- supports-color
|
||||||
dev: false
|
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:
|
/set-blocking/2.0.0:
|
||||||
resolution: {integrity: sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==}
|
resolution: {integrity: sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==}
|
||||||
|
|
||||||
|
@ -12005,8 +11972,9 @@ packages:
|
||||||
resolution: {integrity: sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==}
|
resolution: {integrity: sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==}
|
||||||
dependencies:
|
dependencies:
|
||||||
is-typedarray: 1.0.0
|
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==}
|
resolution: {integrity: sha512-DRqgfqcelMiGgWSMbBmVoJNFN2nPNA3EeY2gC324ndr2DZoGRTb9ILtp2oGVGnlA+cu5zgQ6it5oqKFNkte7Aw==}
|
||||||
engines: {node: '>= 12.9.0'}
|
engines: {node: '>= 12.9.0'}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
@ -12076,6 +12044,7 @@ packages:
|
||||||
js-yaml: 4.1.0
|
js-yaml: 4.1.0
|
||||||
mkdirp: 1.0.4
|
mkdirp: 1.0.4
|
||||||
pg: 8.7.3
|
pg: 8.7.3
|
||||||
|
redis: 4.3.1
|
||||||
reflect-metadata: 0.1.13
|
reflect-metadata: 0.1.13
|
||||||
sha.js: 2.4.11
|
sha.js: 2.4.11
|
||||||
ts-node: 10.8.2_uva6s4l7h33czpzezvop6ux5pe
|
ts-node: 10.8.2_uva6s4l7h33czpzezvop6ux5pe
|
||||||
|
@ -12097,13 +12066,6 @@ packages:
|
||||||
resolution: {integrity: sha512-qLK/Xe9E2uzmYI3qLeOmI0tEOt+TBBQyUIAh4aAgU05FVYzeZrKUdkAZfBNVGRaHVgV0TDkdEngJSw/SyQchkQ==}
|
resolution: {integrity: sha512-qLK/Xe9E2uzmYI3qLeOmI0tEOt+TBBQyUIAh4aAgU05FVYzeZrKUdkAZfBNVGRaHVgV0TDkdEngJSw/SyQchkQ==}
|
||||||
dev: true
|
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:
|
/unbox-primitive/1.0.2:
|
||||||
resolution: {integrity: sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==}
|
resolution: {integrity: sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==}
|
||||||
dependencies:
|
dependencies:
|
||||||
|
@ -12215,11 +12177,6 @@ packages:
|
||||||
unist-util-visit-parents: 5.1.0
|
unist-util-visit-parents: 5.1.0
|
||||||
dev: false
|
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:
|
/universalify/2.0.0:
|
||||||
resolution: {integrity: sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==}
|
resolution: {integrity: sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==}
|
||||||
engines: {node: '>= 10.0.0'}
|
engines: {node: '>= 10.0.0'}
|
||||||
|
@ -12333,6 +12290,11 @@ packages:
|
||||||
hasBin: true
|
hasBin: true
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/uuid/9.0.0:
|
||||||
|
resolution: {integrity: sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg==}
|
||||||
|
hasBin: true
|
||||||
|
dev: false
|
||||||
|
|
||||||
/uvu/0.5.3:
|
/uvu/0.5.3:
|
||||||
resolution: {integrity: sha512-brFwqA3FXzilmtnIyJ+CxdkInkY/i4ErvP7uV0DnUVxQcQ55reuHphorpF+tZoVHK2MniZ/VJzI7zJQoc9T9Yw==}
|
resolution: {integrity: sha512-brFwqA3FXzilmtnIyJ+CxdkInkY/i4ErvP7uV0DnUVxQcQ55reuHphorpF+tZoVHK2MniZ/VJzI7zJQoc9T9Yw==}
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
|
@ -12382,7 +12344,7 @@ packages:
|
||||||
engines: {node: '>=12'}
|
engines: {node: '>=12'}
|
||||||
|
|
||||||
/vary/1.1.2:
|
/vary/1.1.2:
|
||||||
resolution: {integrity: sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=}
|
resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==}
|
||||||
engines: {node: '>= 0.8'}
|
engines: {node: '>= 0.8'}
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
@ -12555,6 +12517,7 @@ packages:
|
||||||
is-typedarray: 1.0.0
|
is-typedarray: 1.0.0
|
||||||
signal-exit: 3.0.7
|
signal-exit: 3.0.7
|
||||||
typedarray-to-buffer: 3.1.5
|
typedarray-to-buffer: 3.1.5
|
||||||
|
dev: true
|
||||||
|
|
||||||
/write-file-atomic/4.0.1:
|
/write-file-atomic/4.0.1:
|
||||||
resolution: {integrity: sha512-nSKUxgAbyioruk6hU87QzVbY279oYT6uiwgDoujth2ju4mJ+TZau7SQBhtbTmUyuNYTuXnSyRn66FV0+eCgcrQ==}
|
resolution: {integrity: sha512-nSKUxgAbyioruk6hU87QzVbY279oYT6uiwgDoujth2ju4mJ+TZau7SQBhtbTmUyuNYTuXnSyRn66FV0+eCgcrQ==}
|
||||||
|
|
|
@ -54,7 +54,7 @@ function ensure_linux() {
|
||||||
|
|
||||||
function clean_logs() {
|
function clean_logs() {
|
||||||
# Clean logs folder
|
# Clean logs folder
|
||||||
logs_folder="${ROOT_FOLDER}/logs"
|
local logs_folder="${ROOT_FOLDER}/logs"
|
||||||
|
|
||||||
# Create the folder if it doesn't exist
|
# Create the folder if it doesn't exist
|
||||||
if [[ ! -d "${logs_folder}" ]]; then
|
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
|
if [ "$(find "${logs_folder}" -maxdepth 1 -type f | wc -l)" -gt 0 ]; then
|
||||||
echo "Cleaning logs folder..."
|
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
|
for file in "${files[@]}"; do
|
||||||
echo "Removing ${file}"
|
echo "Removing ${file}"
|
||||||
|
@ -74,16 +74,18 @@ function clean_logs() {
|
||||||
}
|
}
|
||||||
|
|
||||||
function kill_watcher() {
|
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
|
# kill it if it's running
|
||||||
if [[ -n $watcher_pid ]]; then
|
if [[ -n $watcher_pid ]]; then
|
||||||
# If multiline kill each pid
|
# If multiline kill each pid
|
||||||
if [[ $watcher_pid == *" "* ]]; then
|
if [[ $watcher_pid == *" "* ]]; then
|
||||||
for pid in $watcher_pid; do
|
for pid in $watcher_pid; do
|
||||||
|
# shellcheck disable=SC2086
|
||||||
kill -9 $pid
|
kill -9 $pid
|
||||||
done
|
done
|
||||||
else
|
else
|
||||||
|
# shellcheck disable=SC2086
|
||||||
kill -9 $watcher_pid
|
kill -9 $watcher_pid
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
|
@ -1,5 +1,10 @@
|
||||||
#!/usr/bin/env bash
|
#!/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"
|
source "${BASH_SOURCE%/*}/common.sh"
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,11 @@
|
||||||
#!/usr/bin/env bash
|
#!/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"
|
source "${BASH_SOURCE%/*}/common.sh"
|
||||||
|
|
||||||
ROOT_FOLDER="${PWD}"
|
ROOT_FOLDER="${PWD}"
|
||||||
|
@ -80,12 +84,12 @@ fi
|
||||||
### --------------------------------
|
### --------------------------------
|
||||||
### CLI arguments
|
### CLI arguments
|
||||||
### --------------------------------
|
### --------------------------------
|
||||||
while [ -n "$1" ]; do
|
while [ -n "${1-}" ]; do
|
||||||
case "$1" in
|
case "$1" in
|
||||||
--rc) rc="true" ;;
|
--rc) rc="true" ;;
|
||||||
--ci) ci="true" ;;
|
--ci) ci="true" ;;
|
||||||
--port)
|
--port)
|
||||||
port="$2"
|
port="${2-}"
|
||||||
|
|
||||||
if [[ "${port}" =~ ^[0-9]+$ ]]; then
|
if [[ "${port}" =~ ^[0-9]+$ ]]; then
|
||||||
NGINX_PORT="${port}"
|
NGINX_PORT="${port}"
|
||||||
|
@ -96,7 +100,7 @@ while [ -n "$1" ]; do
|
||||||
shift
|
shift
|
||||||
;;
|
;;
|
||||||
--ssl-port)
|
--ssl-port)
|
||||||
ssl_port="$2"
|
ssl_port="${2-}"
|
||||||
|
|
||||||
if [[ "${ssl_port}" =~ ^[0-9]+$ ]]; then
|
if [[ "${ssl_port}" =~ ^[0-9]+$ ]]; then
|
||||||
NGINX_PORT_SSL="${ssl_port}"
|
NGINX_PORT_SSL="${ssl_port}"
|
||||||
|
@ -107,7 +111,7 @@ while [ -n "$1" ]; do
|
||||||
shift
|
shift
|
||||||
;;
|
;;
|
||||||
--domain)
|
--domain)
|
||||||
domain="$2"
|
domain="${2-}"
|
||||||
|
|
||||||
if [[ "${domain}" =~ ^[a-zA-Z0-9.-]+$ ]]; then
|
if [[ "${domain}" =~ ^[a-zA-Z0-9.-]+$ ]]; then
|
||||||
DOMAIN="${domain}"
|
DOMAIN="${domain}"
|
||||||
|
@ -118,7 +122,7 @@ while [ -n "$1" ]; do
|
||||||
shift
|
shift
|
||||||
;;
|
;;
|
||||||
--listen-ip)
|
--listen-ip)
|
||||||
listen_ip="$2"
|
listen_ip="${2-}"
|
||||||
|
|
||||||
if [[ "${listen_ip}" =~ ^[a-fA-F0-9.:]+$ ]]; then
|
if [[ "${listen_ip}" =~ ^[a-fA-F0-9.:]+$ ]]; then
|
||||||
INTERNAL_IP="${listen_ip}"
|
INTERNAL_IP="${listen_ip}"
|
||||||
|
@ -230,9 +234,9 @@ mv -f "$ENV_FILE" "$ROOT_FOLDER/.env"
|
||||||
### --------------------------------
|
### --------------------------------
|
||||||
### Start the project
|
### 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
|
docker compose -f docker-compose.rc.yml --env-file "${ROOT_FOLDER}/.env" pull
|
||||||
# Run docker compose
|
# Run docker compose
|
||||||
docker compose -f docker-compose.rc.yml --env-file "${ROOT_FOLDER}/.env" up --detach --remove-orphans --build || {
|
docker compose -f docker-compose.rc.yml --env-file "${ROOT_FOLDER}/.env" up --detach --remove-orphans --build || {
|
||||||
|
|
|
@ -16,7 +16,7 @@ export COMPOSE_HTTP_TIMEOUT=240
|
||||||
# Stop all installed apps if there are any
|
# Stop all installed apps if there are any
|
||||||
apps_folder="${ROOT_FOLDER}/apps"
|
apps_folder="${ROOT_FOLDER}/apps"
|
||||||
if [ "$(find "${apps_folder}" -maxdepth 1 -type d | wc -l)" -gt 1 ]; then
|
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
|
for app_name in "${apps_names[@]}"; do
|
||||||
# if folder ${ROOT_FOLDER}/app-data/app_name exists, then stop app
|
# 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."
|
echo "No app installed that can be stopped."
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
kill_watcher
|
||||||
echo "Stopping Docker services..."
|
echo "Stopping Docker services..."
|
||||||
echo
|
echo
|
||||||
docker compose down --remove-orphans --rmi local
|
docker compose down --remove-orphans --rmi local
|
||||||
|
|
|
@ -6,8 +6,6 @@ ROOT_FOLDER="${PWD}"
|
||||||
WATCH_FILE="${ROOT_FOLDER}/state/events"
|
WATCH_FILE="${ROOT_FOLDER}/state/events"
|
||||||
|
|
||||||
function clean_events() {
|
function clean_events() {
|
||||||
echo "Cleaning events..."
|
|
||||||
|
|
||||||
# Create the file if it doesn't exist
|
# Create the file if it doesn't exist
|
||||||
if [[ ! -f "${WATCH_FILE}" ]]; then
|
if [[ ! -f "${WATCH_FILE}" ]]; then
|
||||||
touch "${WATCH_FILE}"
|
touch "${WATCH_FILE}"
|
||||||
|
@ -43,8 +41,6 @@ function run_command() {
|
||||||
|
|
||||||
local result=$?
|
local result=$?
|
||||||
|
|
||||||
echo "Command ${command_path} exited with code ${result}"
|
|
||||||
|
|
||||||
if [[ $result -eq 0 ]]; then
|
if [[ $result -eq 0 ]]; then
|
||||||
set_status "$id" "success"
|
set_status "$id" "success"
|
||||||
else
|
else
|
||||||
|
@ -105,7 +101,6 @@ function select_command() {
|
||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo "Unknown command ${command}"
|
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Add table
Reference in a new issue