Merge branch 'release/0.7.3' into develop

This commit is contained in:
Nicolas Meienberger 2022-11-08 20:52:13 +01:00
commit a7b97ed34c
53 changed files with 602 additions and 804 deletions

View file

@ -57,8 +57,6 @@ services:
tipi-db:
condition: service_healthy
container_name: api
ports:
- 3001:3001
volumes:
- ${PWD}/repos:/runtipi/repos:ro
- ${PWD}/apps:/runtipi/apps
@ -103,14 +101,8 @@ services:
depends_on:
api:
condition: service_started
ports:
- 3000:3000
networks:
- tipi_main_network
environment:
INTERNAL_IP: ${INTERNAL_IP}
DOMAIN: ${DOMAIN}
NGINX_PORT: ${NGINX_PORT-80}
volumes:
- ${PWD}/packages/dashboard/src:/dashboard/src
# - /dashboard/node_modules

View file

@ -103,10 +103,7 @@ services:
api:
condition: service_started
environment:
INTERNAL_IP: ${INTERNAL_IP}
NODE_ENV: production
DOMAIN: ${DOMAIN}
NGINX_PORT: ${NGINX_PORT-80}
labels:
traefik.enable: true
traefik.http.routers.dashboard-redirect.rule: PathPrefix("/")

View file

@ -104,10 +104,7 @@ services:
api:
condition: service_started
environment:
INTERNAL_IP: ${INTERNAL_IP}
NODE_ENV: production
DOMAIN: ${DOMAIN}
NGINX_PORT: ${NGINX_PORT-80}
labels:
traefik.enable: true
traefik.http.routers.dashboard-redirect.rule: PathPrefix("/")

View file

@ -1,6 +1,6 @@
{
"name": "runtipi",
"version": "0.7.2",
"version": "0.7.3",
"description": "A homeserver for everyone",
"scripts": {
"prepare": "husky install",

View file

@ -32,6 +32,7 @@
"remark-breaks": "^3.0.2",
"remark-gfm": "^3.0.1",
"remark-mdx": "^2.1.1",
"semver": "^7.3.7",
"swr": "^1.3.0",
"tslib": "^2.4.0",
"validator": "^13.7.0",
@ -48,6 +49,7 @@
"@types/react": "18.0.8",
"@types/react-dom": "18.0.3",
"@types/react-slick": "^0.23.8",
"@types/semver": "^7.3.12",
"@types/validator": "^13.7.2",
"@typescript-eslint/eslint-plugin": "^5.18.0",
"@typescript-eslint/parser": "^5.0.0",

View file

@ -67,6 +67,7 @@ export type AppInfo = {
requirements?: Maybe<Scalars['JSONObject']>;
short_desc: Scalars['String'];
source: Scalars['String'];
supported_architectures?: Maybe<Array<AppSupportedArchitecturesEnum>>;
tipi_version: Scalars['Float'];
url_suffix?: Maybe<Scalars['String']>;
version?: Maybe<Scalars['String']>;
@ -90,6 +91,12 @@ export enum AppStatusEnum {
Updating = 'UPDATING',
}
export enum AppSupportedArchitecturesEnum {
Amd64 = 'AMD64',
Arm = 'ARM',
Arm64 = 'ARM64',
}
export type Cpu = {
__typename?: 'Cpu';
load: Scalars['Float'];
@ -146,34 +153,42 @@ export type Mutation = {
updateAppConfig: App;
};
export type MutationInstallAppArgs = {
input: AppInputType;
};
export type MutationLoginArgs = {
input: UsernamePasswordInput;
};
export type MutationRegisterArgs = {
input: UsernamePasswordInput;
};
export type MutationStartAppArgs = {
id: Scalars['String'];
};
export type MutationStopAppArgs = {
id: Scalars['String'];
};
export type MutationUninstallAppArgs = {
id: Scalars['String'];
};
export type MutationUpdateAppArgs = {
id: Scalars['String'];
};
export type MutationUpdateAppConfigArgs = {
input: AppInputType;
};
@ -190,6 +205,7 @@ export type Query = {
version: VersionResponse;
};
export type QueryGetAppArgs = {
id: Scalars['String'];
};
@ -236,182 +252,125 @@ export type InstallAppMutationVariables = Exact<{
input: AppInputType;
}>;
export type InstallAppMutation = { __typename?: 'Mutation'; installApp: { __typename: 'App'; id: string; status: AppStatusEnum } };
export type InstallAppMutation = { __typename?: 'Mutation', installApp: { __typename: 'App', id: string, status: AppStatusEnum } };
export type LoginMutationVariables = Exact<{
input: UsernamePasswordInput;
}>;
export type LoginMutation = { __typename?: 'Mutation'; login: { __typename?: 'TokenResponse'; token: string } };
export type LogoutMutationVariables = Exact<{ [key: string]: never }>;
export type LoginMutation = { __typename?: 'Mutation', login: { __typename?: 'TokenResponse', token: string } };
export type LogoutMutation = { __typename?: 'Mutation'; logout: boolean };
export type LogoutMutationVariables = Exact<{ [key: string]: never; }>;
export type LogoutMutation = { __typename?: 'Mutation', logout: boolean };
export type RegisterMutationVariables = Exact<{
input: UsernamePasswordInput;
}>;
export type RegisterMutation = { __typename?: 'Mutation'; register: { __typename?: 'TokenResponse'; token: string } };
export type RestartMutationVariables = Exact<{ [key: string]: never }>;
export type RegisterMutation = { __typename?: 'Mutation', register: { __typename?: 'TokenResponse', token: string } };
export type RestartMutation = { __typename?: 'Mutation'; restart: boolean };
export type RestartMutationVariables = Exact<{ [key: string]: never; }>;
export type RestartMutation = { __typename?: 'Mutation', restart: boolean };
export type StartAppMutationVariables = Exact<{
id: Scalars['String'];
}>;
export type StartAppMutation = { __typename?: 'Mutation'; startApp: { __typename: 'App'; id: string; status: AppStatusEnum } };
export type StartAppMutation = { __typename?: 'Mutation', startApp: { __typename: 'App', id: string, status: AppStatusEnum } };
export type StopAppMutationVariables = Exact<{
id: Scalars['String'];
}>;
export type StopAppMutation = { __typename?: 'Mutation'; stopApp: { __typename: 'App'; id: string; status: AppStatusEnum } };
export type StopAppMutation = { __typename?: 'Mutation', stopApp: { __typename: 'App', id: string, status: AppStatusEnum } };
export type UninstallAppMutationVariables = Exact<{
id: Scalars['String'];
}>;
export type UninstallAppMutation = { __typename?: 'Mutation'; uninstallApp: { __typename: 'App'; id: string; status: AppStatusEnum } };
export type UpdateMutationVariables = Exact<{ [key: string]: never }>;
export type UninstallAppMutation = { __typename?: 'Mutation', uninstallApp: { __typename: 'App', id: string, status: AppStatusEnum } };
export type UpdateMutation = { __typename?: 'Mutation'; update: boolean };
export type UpdateMutationVariables = Exact<{ [key: string]: never; }>;
export type UpdateMutation = { __typename?: 'Mutation', update: boolean };
export type UpdateAppMutationVariables = Exact<{
id: Scalars['String'];
}>;
export type UpdateAppMutation = { __typename?: 'Mutation'; updateApp: { __typename: 'App'; id: string; status: AppStatusEnum } };
export type UpdateAppMutation = { __typename?: 'Mutation', updateApp: { __typename: 'App', id: string, status: AppStatusEnum } };
export type UpdateAppConfigMutationVariables = Exact<{
input: AppInputType;
}>;
export type UpdateAppConfigMutation = { __typename?: 'Mutation'; updateAppConfig: { __typename: 'App'; id: string; status: AppStatusEnum } };
export type UpdateAppConfigMutation = { __typename?: 'Mutation', updateAppConfig: { __typename: 'App', id: string, status: AppStatusEnum } };
export type GetAppQueryVariables = Exact<{
appId: Scalars['String'];
}>;
export type GetAppQuery = {
__typename?: 'Query';
getApp: {
__typename?: 'App';
id: string;
status: AppStatusEnum;
config: any;
version?: number | null;
exposed: boolean;
domain?: string | null;
updateInfo?: { __typename?: 'UpdateInfo'; current: number; latest: number; dockerVersion?: string | null } | null;
info?: {
__typename?: 'AppInfo';
id: string;
port: number;
name: string;
description: string;
available: boolean;
version?: string | null;
tipi_version: number;
short_desc: string;
author: string;
source: string;
categories: Array<AppCategoriesEnum>;
url_suffix?: string | null;
https?: boolean | null;
exposable?: boolean | null;
form_fields: Array<{
__typename?: 'FormField';
type: FieldTypesEnum;
label: string;
max?: number | null;
min?: number | null;
hint?: string | null;
required?: boolean | null;
env_variable: string;
}>;
} | null;
};
};
export type InstalledAppsQueryVariables = Exact<{ [key: string]: never }>;
export type GetAppQuery = { __typename?: 'Query', getApp: { __typename?: 'App', id: string, status: AppStatusEnum, config: any, version?: number | null, exposed: boolean, domain?: string | null, updateInfo?: { __typename?: 'UpdateInfo', current: number, latest: number, dockerVersion?: string | null } | null, info?: { __typename?: 'AppInfo', id: string, port: number, name: string, description: string, available: boolean, version?: string | null, tipi_version: number, short_desc: string, author: string, source: string, categories: Array<AppCategoriesEnum>, url_suffix?: string | null, https?: boolean | null, exposable?: boolean | null, form_fields: Array<{ __typename?: 'FormField', type: FieldTypesEnum, label: string, max?: number | null, min?: number | null, hint?: string | null, required?: boolean | null, env_variable: string }> } | null } };
export type InstalledAppsQuery = {
__typename?: 'Query';
installedApps: Array<{
__typename?: 'App';
id: string;
status: AppStatusEnum;
config: any;
version?: number | null;
updateInfo?: { __typename?: 'UpdateInfo'; current: number; latest: number; dockerVersion?: string | null } | null;
info?: { __typename?: 'AppInfo'; id: string; name: string; description: string; tipi_version: number; short_desc: string; https?: boolean | null } | null;
}>;
};
export type InstalledAppsQueryVariables = Exact<{ [key: string]: never; }>;
export type ConfiguredQueryVariables = Exact<{ [key: string]: never }>;
export type ConfiguredQuery = { __typename?: 'Query'; isConfigured: boolean };
export type InstalledAppsQuery = { __typename?: 'Query', installedApps: Array<{ __typename?: 'App', id: string, status: AppStatusEnum, config: any, version?: number | null, updateInfo?: { __typename?: 'UpdateInfo', current: number, latest: number, dockerVersion?: string | null } | null, info?: { __typename?: 'AppInfo', id: string, name: string, description: string, tipi_version: number, short_desc: string, https?: boolean | null } | null }> };
export type ListAppsQueryVariables = Exact<{ [key: string]: never }>;
export type ConfiguredQueryVariables = Exact<{ [key: string]: never; }>;
export type ListAppsQuery = {
__typename?: 'Query';
listAppsInfo: {
__typename?: 'ListAppsResonse';
total: number;
apps: Array<{
__typename?: 'AppInfo';
id: string;
available: boolean;
tipi_version: number;
port: number;
name: string;
version?: string | null;
short_desc: string;
author: string;
categories: Array<AppCategoriesEnum>;
https?: boolean | null;
}>;
};
};
export type MeQueryVariables = Exact<{ [key: string]: never }>;
export type ConfiguredQuery = { __typename?: 'Query', isConfigured: boolean };
export type MeQuery = { __typename?: 'Query'; me?: { __typename?: 'User'; id: string } | null };
export type ListAppsQueryVariables = Exact<{ [key: string]: never; }>;
export type RefreshTokenQueryVariables = Exact<{ [key: string]: never }>;
export type RefreshTokenQuery = { __typename?: 'Query'; refreshToken?: { __typename?: 'TokenResponse'; token: string } | null };
export type ListAppsQuery = { __typename?: 'Query', listAppsInfo: { __typename?: 'ListAppsResonse', total: number, apps: Array<{ __typename?: 'AppInfo', id: string, available: boolean, tipi_version: number, port: number, name: string, version?: string | null, short_desc: string, author: string, categories: Array<AppCategoriesEnum>, https?: boolean | null }> } };
export type SystemInfoQueryVariables = Exact<{ [key: string]: never }>;
export type MeQueryVariables = Exact<{ [key: string]: never; }>;
export type SystemInfoQuery = {
__typename?: 'Query';
systemInfo?: {
__typename?: 'SystemInfoResponse';
cpu: { __typename?: 'Cpu'; load: number };
disk: { __typename?: 'DiskMemory'; available: number; used: number; total: number };
memory: { __typename?: 'DiskMemory'; available: number; used: number; total: number };
} | null;
};
export type VersionQueryVariables = Exact<{ [key: string]: never }>;
export type MeQuery = { __typename?: 'Query', me?: { __typename?: 'User', id: string } | null };
export type RefreshTokenQueryVariables = Exact<{ [key: string]: never; }>;
export type RefreshTokenQuery = { __typename?: 'Query', refreshToken?: { __typename?: 'TokenResponse', token: string } | null };
export type SystemInfoQueryVariables = Exact<{ [key: string]: never; }>;
export type SystemInfoQuery = { __typename?: 'Query', systemInfo?: { __typename?: 'SystemInfoResponse', cpu: { __typename?: 'Cpu', load: number }, disk: { __typename?: 'DiskMemory', available: number, used: number, total: number }, memory: { __typename?: 'DiskMemory', available: number, used: number, total: number } } | null };
export type VersionQueryVariables = Exact<{ [key: string]: never; }>;
export type VersionQuery = { __typename?: 'Query', version: { __typename?: 'VersionResponse', current: string, latest?: string | null } };
export type VersionQuery = { __typename?: 'Query'; version: { __typename?: 'VersionResponse'; current: string; latest?: string | null } };
export const InstallAppDocument = gql`
mutation InstallApp($input: AppInputType!) {
installApp(input: $input) {
id
status
__typename
}
mutation InstallApp($input: AppInputType!) {
installApp(input: $input) {
id
status
__typename
}
`;
}
`;
export type InstallAppMutationFn = Apollo.MutationFunction<InstallAppMutation, InstallAppMutationVariables>;
/**
@ -439,12 +398,12 @@ export type InstallAppMutationHookResult = ReturnType<typeof useInstallAppMutati
export type InstallAppMutationResult = Apollo.MutationResult<InstallAppMutation>;
export type InstallAppMutationOptions = Apollo.BaseMutationOptions<InstallAppMutation, InstallAppMutationVariables>;
export const LoginDocument = gql`
mutation Login($input: UsernamePasswordInput!) {
login(input: $input) {
token
}
mutation Login($input: UsernamePasswordInput!) {
login(input: $input) {
token
}
`;
}
`;
export type LoginMutationFn = Apollo.MutationFunction<LoginMutation, LoginMutationVariables>;
/**
@ -472,10 +431,10 @@ export type LoginMutationHookResult = ReturnType<typeof useLoginMutation>;
export type LoginMutationResult = Apollo.MutationResult<LoginMutation>;
export type LoginMutationOptions = Apollo.BaseMutationOptions<LoginMutation, LoginMutationVariables>;
export const LogoutDocument = gql`
mutation Logout {
logout
}
`;
mutation Logout {
logout
}
`;
export type LogoutMutationFn = Apollo.MutationFunction<LogoutMutation, LogoutMutationVariables>;
/**
@ -502,12 +461,12 @@ export type LogoutMutationHookResult = ReturnType<typeof useLogoutMutation>;
export type LogoutMutationResult = Apollo.MutationResult<LogoutMutation>;
export type LogoutMutationOptions = Apollo.BaseMutationOptions<LogoutMutation, LogoutMutationVariables>;
export const RegisterDocument = gql`
mutation Register($input: UsernamePasswordInput!) {
register(input: $input) {
token
}
mutation Register($input: UsernamePasswordInput!) {
register(input: $input) {
token
}
`;
}
`;
export type RegisterMutationFn = Apollo.MutationFunction<RegisterMutation, RegisterMutationVariables>;
/**
@ -535,10 +494,10 @@ export type RegisterMutationHookResult = ReturnType<typeof useRegisterMutation>;
export type RegisterMutationResult = Apollo.MutationResult<RegisterMutation>;
export type RegisterMutationOptions = Apollo.BaseMutationOptions<RegisterMutation, RegisterMutationVariables>;
export const RestartDocument = gql`
mutation Restart {
restart
}
`;
mutation Restart {
restart
}
`;
export type RestartMutationFn = Apollo.MutationFunction<RestartMutation, RestartMutationVariables>;
/**
@ -565,14 +524,14 @@ export type RestartMutationHookResult = ReturnType<typeof useRestartMutation>;
export type RestartMutationResult = Apollo.MutationResult<RestartMutation>;
export type RestartMutationOptions = Apollo.BaseMutationOptions<RestartMutation, RestartMutationVariables>;
export const StartAppDocument = gql`
mutation StartApp($id: String!) {
startApp(id: $id) {
id
status
__typename
}
mutation StartApp($id: String!) {
startApp(id: $id) {
id
status
__typename
}
`;
}
`;
export type StartAppMutationFn = Apollo.MutationFunction<StartAppMutation, StartAppMutationVariables>;
/**
@ -600,14 +559,14 @@ export type StartAppMutationHookResult = ReturnType<typeof useStartAppMutation>;
export type StartAppMutationResult = Apollo.MutationResult<StartAppMutation>;
export type StartAppMutationOptions = Apollo.BaseMutationOptions<StartAppMutation, StartAppMutationVariables>;
export const StopAppDocument = gql`
mutation StopApp($id: String!) {
stopApp(id: $id) {
id
status
__typename
}
mutation StopApp($id: String!) {
stopApp(id: $id) {
id
status
__typename
}
`;
}
`;
export type StopAppMutationFn = Apollo.MutationFunction<StopAppMutation, StopAppMutationVariables>;
/**
@ -635,14 +594,14 @@ export type StopAppMutationHookResult = ReturnType<typeof useStopAppMutation>;
export type StopAppMutationResult = Apollo.MutationResult<StopAppMutation>;
export type StopAppMutationOptions = Apollo.BaseMutationOptions<StopAppMutation, StopAppMutationVariables>;
export const UninstallAppDocument = gql`
mutation UninstallApp($id: String!) {
uninstallApp(id: $id) {
id
status
__typename
}
mutation UninstallApp($id: String!) {
uninstallApp(id: $id) {
id
status
__typename
}
`;
}
`;
export type UninstallAppMutationFn = Apollo.MutationFunction<UninstallAppMutation, UninstallAppMutationVariables>;
/**
@ -670,10 +629,10 @@ export type UninstallAppMutationHookResult = ReturnType<typeof useUninstallAppMu
export type UninstallAppMutationResult = Apollo.MutationResult<UninstallAppMutation>;
export type UninstallAppMutationOptions = Apollo.BaseMutationOptions<UninstallAppMutation, UninstallAppMutationVariables>;
export const UpdateDocument = gql`
mutation Update {
update
}
`;
mutation Update {
update
}
`;
export type UpdateMutationFn = Apollo.MutationFunction<UpdateMutation, UpdateMutationVariables>;
/**
@ -700,14 +659,14 @@ export type UpdateMutationHookResult = ReturnType<typeof useUpdateMutation>;
export type UpdateMutationResult = Apollo.MutationResult<UpdateMutation>;
export type UpdateMutationOptions = Apollo.BaseMutationOptions<UpdateMutation, UpdateMutationVariables>;
export const UpdateAppDocument = gql`
mutation UpdateApp($id: String!) {
updateApp(id: $id) {
id
status
__typename
}
mutation UpdateApp($id: String!) {
updateApp(id: $id) {
id
status
__typename
}
`;
}
`;
export type UpdateAppMutationFn = Apollo.MutationFunction<UpdateAppMutation, UpdateAppMutationVariables>;
/**
@ -735,14 +694,14 @@ export type UpdateAppMutationHookResult = ReturnType<typeof useUpdateAppMutation
export type UpdateAppMutationResult = Apollo.MutationResult<UpdateAppMutation>;
export type UpdateAppMutationOptions = Apollo.BaseMutationOptions<UpdateAppMutation, UpdateAppMutationVariables>;
export const UpdateAppConfigDocument = gql`
mutation UpdateAppConfig($input: AppInputType!) {
updateAppConfig(input: $input) {
id
status
__typename
}
mutation UpdateAppConfig($input: AppInputType!) {
updateAppConfig(input: $input) {
id
status
__typename
}
`;
}
`;
export type UpdateAppConfigMutationFn = Apollo.MutationFunction<UpdateAppConfigMutation, UpdateAppConfigMutationVariables>;
/**
@ -770,47 +729,47 @@ export type UpdateAppConfigMutationHookResult = ReturnType<typeof useUpdateAppCo
export type UpdateAppConfigMutationResult = Apollo.MutationResult<UpdateAppConfigMutation>;
export type UpdateAppConfigMutationOptions = Apollo.BaseMutationOptions<UpdateAppConfigMutation, UpdateAppConfigMutationVariables>;
export const GetAppDocument = gql`
query GetApp($appId: String!) {
getApp(id: $appId) {
query GetApp($appId: String!) {
getApp(id: $appId) {
id
status
config
version
exposed
domain
updateInfo {
current
latest
dockerVersion
}
info {
id
status
config
port
name
description
available
version
exposed
domain
updateInfo {
current
latest
dockerVersion
}
info {
id
port
name
description
available
version
tipi_version
short_desc
author
source
categories
url_suffix
https
exposable
form_fields {
type
label
max
min
hint
required
env_variable
}
tipi_version
short_desc
author
source
categories
url_suffix
https
exposable
form_fields {
type
label
max
min
hint
required
env_variable
}
}
}
`;
}
`;
/**
* __useGetAppQuery__
@ -840,28 +799,28 @@ export type GetAppQueryHookResult = ReturnType<typeof useGetAppQuery>;
export type GetAppLazyQueryHookResult = ReturnType<typeof useGetAppLazyQuery>;
export type GetAppQueryResult = Apollo.QueryResult<GetAppQuery, GetAppQueryVariables>;
export const InstalledAppsDocument = gql`
query InstalledApps {
installedApps {
query InstalledApps {
installedApps {
id
status
config
version
updateInfo {
current
latest
dockerVersion
}
info {
id
status
config
version
updateInfo {
current
latest
dockerVersion
}
info {
id
name
description
tipi_version
short_desc
https
}
name
description
tipi_version
short_desc
https
}
}
`;
}
`;
/**
* __useInstalledAppsQuery__
@ -890,10 +849,10 @@ export type InstalledAppsQueryHookResult = ReturnType<typeof useInstalledAppsQue
export type InstalledAppsLazyQueryHookResult = ReturnType<typeof useInstalledAppsLazyQuery>;
export type InstalledAppsQueryResult = Apollo.QueryResult<InstalledAppsQuery, InstalledAppsQueryVariables>;
export const ConfiguredDocument = gql`
query Configured {
isConfigured
}
`;
query Configured {
isConfigured
}
`;
/**
* __useConfiguredQuery__
@ -922,24 +881,24 @@ export type ConfiguredQueryHookResult = ReturnType<typeof useConfiguredQuery>;
export type ConfiguredLazyQueryHookResult = ReturnType<typeof useConfiguredLazyQuery>;
export type ConfiguredQueryResult = Apollo.QueryResult<ConfiguredQuery, ConfiguredQueryVariables>;
export const ListAppsDocument = gql`
query ListApps {
listAppsInfo {
apps {
id
available
tipi_version
port
name
version
short_desc
author
categories
https
}
total
query ListApps {
listAppsInfo {
apps {
id
available
tipi_version
port
name
version
short_desc
author
categories
https
}
total
}
`;
}
`;
/**
* __useListAppsQuery__
@ -968,12 +927,12 @@ export type ListAppsQueryHookResult = ReturnType<typeof useListAppsQuery>;
export type ListAppsLazyQueryHookResult = ReturnType<typeof useListAppsLazyQuery>;
export type ListAppsQueryResult = Apollo.QueryResult<ListAppsQuery, ListAppsQueryVariables>;
export const MeDocument = gql`
query Me {
me {
id
}
query Me {
me {
id
}
`;
}
`;
/**
* __useMeQuery__
@ -1002,12 +961,12 @@ export type MeQueryHookResult = ReturnType<typeof useMeQuery>;
export type MeLazyQueryHookResult = ReturnType<typeof useMeLazyQuery>;
export type MeQueryResult = Apollo.QueryResult<MeQuery, MeQueryVariables>;
export const RefreshTokenDocument = gql`
query RefreshToken {
refreshToken {
token
}
query RefreshToken {
refreshToken {
token
}
`;
}
`;
/**
* __useRefreshTokenQuery__
@ -1036,24 +995,24 @@ export type RefreshTokenQueryHookResult = ReturnType<typeof useRefreshTokenQuery
export type RefreshTokenLazyQueryHookResult = ReturnType<typeof useRefreshTokenLazyQuery>;
export type RefreshTokenQueryResult = Apollo.QueryResult<RefreshTokenQuery, RefreshTokenQueryVariables>;
export const SystemInfoDocument = gql`
query SystemInfo {
systemInfo {
cpu {
load
}
disk {
available
used
total
}
memory {
available
used
total
}
query SystemInfo {
systemInfo {
cpu {
load
}
disk {
available
used
total
}
memory {
available
used
total
}
}
`;
}
`;
/**
* __useSystemInfoQuery__
@ -1082,13 +1041,13 @@ export type SystemInfoQueryHookResult = ReturnType<typeof useSystemInfoQuery>;
export type SystemInfoLazyQueryHookResult = ReturnType<typeof useSystemInfoLazyQuery>;
export type SystemInfoQueryResult = Apollo.QueryResult<SystemInfoQuery, SystemInfoQueryVariables>;
export const VersionDocument = gql`
query Version {
version {
current
latest
}
query Version {
version {
current
latest
}
`;
}
`;
/**
* __useVersionQuery__
@ -1115,4 +1074,4 @@ export function useVersionLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<Ve
}
export type VersionQueryHookResult = ReturnType<typeof useVersionQuery>;
export type VersionLazyQueryHookResult = ReturnType<typeof useVersionLazyQuery>;
export type VersionQueryResult = Apollo.QueryResult<VersionQuery, VersionQueryVariables>;
export type VersionQueryResult = Apollo.QueryResult<VersionQuery, VersionQueryVariables>;

View file

@ -2,19 +2,14 @@ import { useEffect, useState } from 'react';
import { ApolloClient } from '@apollo/client';
import { createApolloClient } from '../core/apollo/client';
import { useSystemStore } from '../state/systemStore';
import useSWR, { Fetcher } from 'swr';
import { getUrl } from '../core/helpers/url-helpers';
interface IReturnProps {
client?: ApolloClient<unknown>;
isLoadingComplete?: boolean;
}
const fetcher: Fetcher<{ ip: string; domain: string; port: string }, string> = (...args) => fetch(...args).then((res) => res.json());
export default function useCachedResources(): IReturnProps {
const { data } = useSWR(getUrl('api/getenv'), fetcher);
const { baseUrl, setBaseUrl, setInternalIp, setDomain } = useSystemStore();
const { baseUrl, setBaseUrl } = useSystemStore();
const [isLoadingComplete, setLoadingComplete] = useState(false);
const [client, setClient] = useState<ApolloClient<unknown>>();
@ -32,23 +27,15 @@ export default function useCachedResources(): IReturnProps {
}
useEffect(() => {
const { ip, domain, port } = data || {};
const hostname = window.location.hostname;
const port = window.location.port;
if (ip && !baseUrl) {
setInternalIp(ip);
setDomain(domain);
if (!domain || domain === 'tipi.localhost') {
if (port === '80') {
setBaseUrl(`http://${ip}/api`);
} else {
setBaseUrl(`http://${ip}:${port}/api`);
}
} else {
setBaseUrl(`https://${domain}/api`);
}
if (!port) {
setBaseUrl(`http://${hostname}/api`);
} else {
setBaseUrl(`http://${hostname}:${port}/api`);
}
}, [baseUrl, setBaseUrl, setInternalIp, setDomain, data]);
}, [setBaseUrl]);
useEffect(() => {
if (baseUrl) {

View file

@ -1,7 +1,6 @@
import { SlideFade, Flex, Divider, useDisclosure, useToast } from '@chakra-ui/react';
import React from 'react';
import { FiExternalLink } from 'react-icons/fi';
import { useSystemStore } from '../../../state/systemStore';
import AppActions from '../components/AppActions';
import InstallModal from '../components/InstallModal';
import StopModal from '../components/StopModal';
@ -48,8 +47,6 @@ const AppDetails: React.FC<IProps> = ({ app, info }) => {
const updateAvailable = Number(app?.updateInfo?.current || 0) < Number(app?.updateInfo?.latest);
const { internalIp } = useSystemStore();
const handleError = (error: unknown) => {
if (error instanceof Error) {
toast({
@ -139,7 +136,9 @@ const AppDetails: React.FC<IProps> = ({ app, info }) => {
const protocol = https ? 'https' : 'http';
if (typeof window !== 'undefined') {
window.open(`${protocol}://${internalIp}:${info.port}${info.url_suffix || ''}`, '_blank', 'noreferrer');
// Current domain
const domain = window.location.hostname;
window.open(`${protocol}://${domain}:${info.port}${info.url_suffix || ''}`, '_blank', 'noreferrer');
}
};

View file

@ -1,7 +0,0 @@
export default function getEnv(_: any, res: any) {
const { INTERNAL_IP } = process.env;
const { NGINX_PORT } = process.env;
const { DOMAIN } = process.env;
res.status(200).json({ ip: INTERNAL_IP, domain: DOMAIN, port: NGINX_PORT });
}

View file

@ -3,6 +3,7 @@ import { AlertDialog, AlertDialogBody, AlertDialogContent, AlertDialogFooter, Al
import Layout from '../components/Layout';
import { useLogoutMutation, useRestartMutation, useUpdateMutation, useVersionQuery } from '../generated/graphql';
import { useRef, useState } from 'react';
import semver from 'semver';
const Settings: NextPage = () => {
const toast = useToast();
@ -15,7 +16,9 @@ const Settings: NextPage = () => {
const [restart] = useRestartMutation();
const [update] = useUpdateMutation();
const [logout] = useLogoutMutation({ refetchQueries: ['Me'] });
const isLatest = data?.version.latest === data?.version.current;
const defaultVersion = '0.0.0';
const isLatest = semver.gte(data?.version.latest || defaultVersion, data?.version.current || defaultVersion);
const handleError = (error: unknown) => {
if (error instanceof Error) {

View file

@ -8,22 +8,14 @@ export enum SystemStatus {
type Store = {
baseUrl: string;
internalIp: string;
domain: string;
status: SystemStatus;
setDomain: (domain?: string) => void;
setBaseUrl: (url: string) => void;
setInternalIp: (ip: string) => void;
setStatus: (status: SystemStatus) => void;
};
export const useSystemStore = create<Store>((set) => ({
baseUrl: '',
internalIp: '',
domain: '',
status: SystemStatus.RUNNING,
setDomain: (domain?: string) => set((state) => ({ ...state, domain: domain || '' })),
setBaseUrl: (url: string) => set((state) => ({ ...state, baseUrl: url })),
setInternalIp: (ip: string) => set((state) => ({ ...state, internalIp: ip })),
setStatus: (status: SystemStatus) => set((state) => ({ ...state, status })),
}));

View file

@ -1,4 +1,4 @@
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)[];
function nonNullable<T>(value: T): value is NonNullable<T> {
return value !== null && value !== undefined;

View file

@ -1,6 +1,6 @@
module.exports = {
env: { node: true, jest: true },
extends: ['airbnb-typescript', 'eslint:recommended', 'plugin:import/typescript'],
plugins: ['@typescript-eslint', 'import', 'react'],
extends: ['airbnb-base', 'airbnb-typescript/base', 'eslint:recommended', 'plugin:import/typescript', 'plugin:@typescript-eslint/recommended', 'prettier'],
parser: '@typescript-eslint/parser',
parserOptions: {
project: './tsconfig.json',
@ -8,18 +8,19 @@ module.exports = {
ecmaVersion: 'latest',
sourceType: 'module',
},
plugins: ['@typescript-eslint', 'import', 'react'],
rules: {
'arrow-body-style': 0,
'no-restricted-exports': 0,
'max-len': [1, { code: 200 }],
'import/extensions': ['error', 'ignorePackages', { js: 'never', jsx: 'never', ts: 'never', tsx: 'never' }],
indent: 'off',
'@typescript-eslint/indent': 0,
'no-unused-vars': [1, { argsIgnorePattern: '^_' }],
'@typescript-eslint/no-unused-vars': [1, { argsIgnorePattern: '^_' }],
'max-classes-per-file': 0,
'class-methods-use-this': 0,
'import/prefer-default-export': 0,
'no-underscore-dangle': 0,
'@typescript-eslint/ban-ts-comment': 0,
},
globals: {
NodeJS: true,
},
env: { node: true, jest: true },
};

View file

@ -1,6 +1,6 @@
const childProcess: { execFile: typeof execFile } = jest.genMockFromModule('child_process');
const execFile = (_path: string, _args: string[], _thing: any, callback: Function) => {
const execFile = (_path: string, _args: string[], _thing: unknown, callback: () => void) => {
callback();
};

View file

@ -1,4 +1,5 @@
import path from 'path';
const fs: {
__createMockFiles: typeof createMockFiles;
__resetAllMocks: typeof resetAllMocks;
@ -20,7 +21,7 @@ const createMockFiles = (newMockFiles: Record<string, string>) => {
mockFiles = Object.create(null);
// Create folder tree
for (const file in newMockFiles) {
Object.keys(newMockFiles).forEach((file) => {
const dir = path.dirname(file);
if (!mockFiles[dir]) {
@ -29,18 +30,14 @@ const createMockFiles = (newMockFiles: Record<string, string>) => {
mockFiles[dir].push(path.basename(file));
mockFiles[file] = newMockFiles[file];
}
});
};
const readFileSync = (p: string) => {
return mockFiles[p];
};
const readFileSync = (p: string) => mockFiles[p];
const existsSync = (p: string) => {
return mockFiles[p] !== undefined;
};
const existsSync = (p: string) => mockFiles[p] !== undefined;
const writeFileSync = (p: string, data: any) => {
const writeFileSync = (p: string, data: string | string[]) => {
mockFiles[p] = data;
};
@ -85,7 +82,7 @@ const copySync = (source: string, destination: string) => {
if (mockFiles[source] instanceof Array) {
mockFiles[source].forEach((file: string) => {
mockFiles[destination + '/' + file] = mockFiles[source + '/' + file];
mockFiles[`${destination}/${file}`] = mockFiles[`${source}/${file}`];
});
}
};
@ -120,4 +117,5 @@ fs.createFileSync = createFileSync;
fs.__createMockFiles = createMockFiles;
fs.__resetAllMocks = resetAllMocks;
module.exports = fs;
export default fs;
// module.exports = fs;

View file

@ -1,11 +0,0 @@
import { faker } from '@faker-js/faker';
const internalIp: { v4: typeof v4Mock } = jest.genMockFromModule('internal-ip');
const v4Mock = () => {
return faker.internet.ipv4();
};
internalIp.v4 = v4Mock;
module.exports = internalIp;

View file

@ -9,16 +9,10 @@ module.exports = {
values.set(key, value);
expirations.set(key, exp);
},
get: (key: string) => {
return values.get(key);
},
get: (key: string) => values.get(key),
quit: jest.fn(),
del: (key: string) => {
return values.delete(key);
},
ttl: (key: string) => {
return expirations.get(key);
},
del: (key: string) => values.delete(key),
ttl: (key: string) => expirations.get(key),
};
}),
};

View file

@ -1,9 +0,0 @@
import portUsed, { TcpPortUsedOptions } from 'tcp-port-used';
const internalIp: { check: typeof portUsed.check } = jest.genMockFromModule('tcp-port-used');
internalIp.check = async (_: number | TcpPortUsedOptions, __?: string | undefined) => {
return true;
};
module.exports = internalIp;

View file

@ -37,16 +37,13 @@
"graphql": "^15.3.0",
"graphql-type-json": "^0.3.2",
"http": "0.0.1-security",
"internal-ip": "^6.0.0",
"jsonwebtoken": "^8.5.1",
"node-cache": "^5.1.2",
"node-cron": "^3.0.1",
"node-port-scanner": "^3.0.1",
"pg": "^8.7.3",
"redis": "^4.3.1",
"reflect-metadata": "^0.1.13",
"semver": "^7.3.7",
"tcp-port-used": "^1.0.2",
"type-graphql": "^1.1.1",
"typeorm": "^0.3.6",
"uuid": "^9.0.0",
@ -67,7 +64,6 @@
"@types/node-cron": "^3.0.2",
"@types/pg": "^8.6.5",
"@types/semver": "^7.3.12",
"@types/tcp-port-used": "^1.0.1",
"@types/uuid": "^8.3.4",
"@types/validator": "^13.7.2",
"@typescript-eslint/eslint-plugin": "^5.18.0",

View file

@ -22,5 +22,5 @@ export default new DataSource({
logging: !__prod__,
synchronize: false,
entities: [App, User, Update],
migrations: [process.cwd() + '/dist/config/migrations/*.js'],
migrations: [`${process.cwd()}/dist/config/migrations/*.js`],
});

View file

@ -4,15 +4,13 @@ import { __prod__ } from '../constants/constants';
import logger from './logger';
const ApolloLogs: PluginDefinition = {
requestDidStart: async () => {
return {
requestDidStart: async () => ({
async didEncounterErrors(errors) {
if (!__prod__) {
logger.error(JSON.stringify(errors.errors));
}
},
};
},
}),
};
export { ApolloLogs };

View file

@ -1,3 +1,4 @@
/* eslint-disable max-len */
import { MigrationInterface, QueryRunner } from 'typeorm';
export class Initial1657299198975 implements MigrationInterface {

View file

@ -105,14 +105,14 @@ class Config {
this.config = parsed;
}
public setConfig<T extends keyof typeof configSchema.shape>(key: T, value: z.infer<typeof configSchema>[T], writeFile: boolean = false) {
public setConfig<T extends keyof typeof configSchema.shape>(key: T, value: z.infer<typeof configSchema>[T], writeFile = false) {
const newConf: z.infer<typeof configSchema> = { ...this.getConfig() };
newConf[key] = value;
this.config = configSchema.parse(newConf);
if (writeFile) {
const currentJsonConf = readJsonFile('/runtipi/state/settings.json') || {};
const currentJsonConf = readJsonFile<Partial<z.infer<typeof configSchema>>>('/runtipi/state/settings.json') || {};
currentJsonConf[key] = value;
const partialConfig = configSchema.partial();
const parsed = partialConfig.parse(currentJsonConf);
@ -122,7 +122,7 @@ class Config {
}
}
export const setConfig = <T extends keyof typeof configSchema.shape>(key: T, value: z.infer<typeof configSchema>[T], writeFile: boolean = false) => {
export const setConfig = <T extends keyof typeof configSchema.shape>(key: T, value: z.infer<typeof configSchema>[T], writeFile = false) => {
Config.getInstance().setConfig(key, value, writeFile);
};

View file

@ -5,6 +5,7 @@ const WATCH_FILE = '/runtipi/state/events';
jest.mock('fs-extra');
// eslint-disable-next-line no-promise-executor-return
const wait = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
beforeEach(() => {
@ -29,7 +30,7 @@ describe('EventDispatcher - dispatchEvent', () => {
eventDispatcher.dispatchEvent(EventTypes.APP, ['--help']);
// @ts-ignore
const queue = eventDispatcher.queue;
const { queue } = eventDispatcher;
expect(queue.length).toBe(2);
});
@ -39,12 +40,12 @@ describe('EventDispatcher - dispatchEvent', () => {
eventDispatcher.dispatchEvent(EventTypes.UPDATE, ['--help']);
// @ts-ignore
const queue = eventDispatcher.queue;
const { queue } = eventDispatcher;
await wait(1050);
// @ts-ignore
const lock = eventDispatcher.lock;
const { lock } = eventDispatcher;
expect(queue.length).toBe(2);
expect(lock).toBeDefined();
@ -59,7 +60,7 @@ describe('EventDispatcher - dispatchEvent', () => {
await wait(1050);
// @ts-ignore
const queue = eventDispatcher.queue;
const { queue } = eventDispatcher;
expect(queue.length).toBe(0);
});
@ -72,7 +73,7 @@ describe('EventDispatcher - dispatchEvent', () => {
await wait(1050);
// @ts-ignore
const queue = eventDispatcher.queue;
const { queue } = eventDispatcher;
expect(queue.length).toBe(0);
});
@ -161,7 +162,7 @@ describe('EventDispatcher - clearEvent', () => {
eventDispatcher.clearEvent(event);
// @ts-ignore
const queue = eventDispatcher.queue;
const { queue } = eventDispatcher;
expect(queue.length).toBe(0);
});
@ -174,7 +175,7 @@ describe('EventDispatcher - pollQueue', () => {
// @ts-ignore
const id = eventDispatcher.pollQueue();
// @ts-ignore
const interval = eventDispatcher.interval;
const { interval } = eventDispatcher;
expect(interval).toBe(123);
expect(id).toBe(123);
@ -192,7 +193,7 @@ describe('EventDispatcher - collectLockStatusAndClean', () => {
eventDispatcher.collectLockStatusAndClean();
// @ts-ignore
const lock = eventDispatcher.lock;
const { lock } = eventDispatcher;
expect(lock).toBeNull();
});

View file

@ -48,7 +48,7 @@ describe('Test: setConfig', () => {
expect(config).toBeDefined();
expect(config.appsRepoUrl).toBe(randomWord);
const settingsJson = readJsonFile('/runtipi/state/settings.json');
const settingsJson = readJsonFile<any>('/runtipi/state/settings.json');
expect(settingsJson).toBeDefined();
expect(settingsJson.appsRepoUrl).toBe(randomWord);

View file

@ -1,7 +1,7 @@
import cron from 'node-cron';
import { getConfig } from '../../config/TipiConfig';
import startJobs from '../jobs';
import { eventDispatcher, EventTypes } from '../../../core/config/EventDispatcher';
import { eventDispatcher, EventTypes } from '../../config/EventDispatcher';
jest.mock('node-cron');
jest.mock('child_process');

View file

@ -1,6 +1,6 @@
import cron from 'node-cron';
import logger from '../../config/logger/logger';
import { getConfig } from '../../core/config/TipiConfig';
import { getConfig } from '../config/TipiConfig';
import { eventDispatcher, EventTypes } from '../config/EventDispatcher';
const startJobs = () => {

View file

@ -1,5 +1,6 @@
import { faker } from '@faker-js/faker';
import jwt from 'jsonwebtoken';
import { Request, Response } from 'express';
import TipiCache from '../../../config/TipiCache';
import { getConfig } from '../../config/TipiConfig';
import getSessionMiddleware from '../sessionMiddleware';
@ -15,9 +16,9 @@ describe('SessionMiddleware', () => {
headers: {
authorization: `Bearer ${token}`,
},
} as any;
} as Request;
const next = jest.fn();
const res = {} as any;
const res = {} as Response;
// Act
await getSessionMiddleware(req, res, next);
@ -41,9 +42,9 @@ describe('SessionMiddleware', () => {
headers: {
authorization: `Bearer ${token}`,
},
} as any;
} as Request;
const next = jest.fn();
const res = {} as any;
const res = {} as Response;
// Act
await getSessionMiddleware(req, res, next);
@ -59,9 +60,9 @@ describe('SessionMiddleware', () => {
// Arrange
const req = {
headers: {},
} as any;
} as Request;
const next = jest.fn();
const res = {} as any;
const res = {} as Response;
// Act
await getSessionMiddleware(req, res, next);

View file

@ -30,9 +30,7 @@ afterAll(async () => {
await teardownConnection(TEST_SUITE);
});
const createState = (apps: string[]) => {
return JSON.stringify({ installed: apps.join(' ') });
};
const createState = (apps: string[]) => JSON.stringify({ installed: apps.join(' ') });
describe('No state/apps.json', () => {
it('Should do nothing and create the update with status SUCCES', async () => {
@ -41,7 +39,7 @@ describe('No state/apps.json', () => {
const update = await Update.findOne({ where: { name: 'v040' } });
expect(update).toBeDefined();
expect(update!.status).toBe(UpdateStatusEnum.SUCCESS);
expect(update?.status).toBe(UpdateStatusEnum.SUCCESS);
const apps = await App.find();

View file

@ -1,9 +1,21 @@
import { DataSource } from 'typeorm';
import { BaseEntity, DataSource, DeepPartial } from 'typeorm';
import logger from '../../config/logger/logger';
import App from '../../modules/apps/app.entity';
import User from '../../modules/auth/user.entity';
import Update from '../../modules/system/update.entity';
const createUser = async (user: DeepPartial<BaseEntity>): Promise<void> => {
await User.create(user).save();
};
const createApp = async (app: DeepPartial<BaseEntity>): Promise<void> => {
await App.create(app).save();
};
const createUpdate = async (update: DeepPartial<BaseEntity>): Promise<void> => {
await Update.create(update).save();
};
const recover = async (datasource: DataSource) => {
logger.info('Recovering broken database');
@ -18,20 +30,14 @@ const recover = async (datasource: DataSource) => {
logger.info('running migrations');
await datasource.runMigrations();
// create users
for (const user of users) {
await User.create(user).save();
}
// recreate users
await Promise.all(users.map(createUser));
// create apps
for (const app of apps) {
await App.create(app).save();
}
await Promise.all(apps.map(createApp));
// create updates
for (const update of updates) {
await Update.create(update).save();
}
await Promise.all(updates.map(createUpdate));
logger.info(`Users recovered ${users.length}`);
logger.info(`Apps recovered ${apps.length}`);

View file

@ -10,6 +10,41 @@ type AppsState = { installed: string };
const UPDATE_NAME = 'v040';
const migrateApp = async (appId: string): Promise<void> => {
const app = await App.findOne({ where: { id: appId } });
if (!app) {
const envFile = readFile(`/app/storage/app-data/${appId}/app.env`).toString();
const envVars = envFile.split('\n');
const envVarsMap = new Map<string, string>();
envVars.forEach((envVar) => {
const [key, value] = envVar.split('=');
envVarsMap.set(key, value);
});
const form: Record<string, string> = {};
const configFile: AppInfo | null = readJsonFile(`/runtipi/repos/${getConfig().appsRepoId}/apps/${appId}/config.json`);
configFile?.form_fields?.forEach((field) => {
const envVar = field.env_variable;
const envVarValue = envVarsMap.get(envVar);
if (envVarValue) {
form[field.env_variable] = envVarValue;
}
});
await App.create({ id: appId, status: AppStatusEnum.STOPPED, config: form }).save();
} else {
logger.info('App already migrated');
}
};
const migrateUser = async (user: { email: string; password: string }): Promise<void> => {
await User.create({ username: user.email.trim().toLowerCase(), password: user.password }).save();
};
export const updateV040 = async (): Promise<void> => {
try {
const update = await Update.findOne({ where: { name: UPDATE_NAME } });
@ -24,36 +59,7 @@ export const updateV040 = async (): Promise<void> => {
const state: AppsState = await readJsonFile('/runtipi/state/apps.json');
const installed: string[] = state.installed.split(' ').filter(Boolean);
for (const appId of installed) {
const app = await App.findOne({ where: { id: appId } });
if (!app) {
const envFile = readFile(`/app/storage/app-data/${appId}/app.env`).toString();
const envVars = envFile.split('\n');
const envVarsMap = new Map<string, string>();
envVars.forEach((envVar) => {
const [key, value] = envVar.split('=');
envVarsMap.set(key, value);
});
const form: Record<string, string> = {};
const configFile: AppInfo | null = readJsonFile(`/runtipi/repos/${getConfig().appsRepoId}/apps/${appId}/config.json`);
configFile?.form_fields?.forEach((field) => {
const envVar = field.env_variable;
const envVarValue = envVarsMap.get(envVar);
if (envVarValue) {
form[field.env_variable] = envVarValue;
}
});
await App.create({ id: appId, status: AppStatusEnum.STOPPED, config: form }).save();
} else {
logger.info('App already migrated');
}
}
await Promise.all(installed.map((appId) => migrateApp(appId)));
deleteFolder('/runtipi/state/apps.json');
}
@ -61,9 +67,7 @@ export const updateV040 = async (): Promise<void> => {
if (fileExists('/state/users.json')) {
const state: { email: string; password: string }[] = await readJsonFile('/runtipi/state/users.json');
for (const user of state) {
await User.create({ username: user.email.trim().toLowerCase(), password: user.password }).save();
}
await Promise.all(state.map((user) => migrateUser(user)));
deleteFolder('/runtipi/state/users.json');
}

View file

@ -4,6 +4,6 @@ declare namespace Express {
userId?: number;
id?: string;
};
[key: string]: any;
[key: string]: unknown;
}
}

View file

@ -30,7 +30,6 @@ const createApp = async (props: IProps) => {
env_variable: 'TEST_FIELD',
},
],
name: faker.random.word(),
description: faker.random.words(),
tipi_version: faker.datatype.number({ min: 1, max: 10 }),
@ -56,7 +55,7 @@ const createApp = async (props: IProps) => {
};
}
let MockFiles: any = {};
const MockFiles: Record<string, string | string[]> = {};
MockFiles['/runtipi/.env'] = 'TEST=test';
MockFiles['/runtipi/repos/repo-id'] = '';
MockFiles[`/runtipi/repos/repo-id/apps/${appInfo.id}/config.json`] = JSON.stringify(appInfo);
@ -71,6 +70,7 @@ const createApp = async (props: IProps) => {
status,
exposed,
domain,
version: 1,
}).save();
MockFiles[`/app/storage/app-data/${appInfo.id}`] = '';

View file

@ -4,22 +4,12 @@ import { DataSource } from 'typeorm';
import logger from '../../../config/logger/logger';
import { setupConnection, teardownConnection } from '../../../test/connection';
import App from '../app.entity';
import { checkAppRequirements, checkEnvFile, generateEnvFile, getAppInfo, getAvailableApps, getEnvMap, getUpdateInfo } from '../apps.helpers';
import { checkAppRequirements, checkEnvFile, ensureAppFolder, generateEnvFile, getAppInfo, getAvailableApps, getEnvMap, getUpdateInfo } from '../apps.helpers';
import { AppInfo } from '../apps.types';
import { createApp } from './apps.factory';
jest.mock('fs-extra');
jest.mock('child_process');
jest.mock('internal-ip');
jest.mock('tcp-port-used', () => ({
check: (port: number) => {
if (port === 53) {
return true;
}
return false;
},
}));
let db: DataSource | null = null;
const TEST_SUITE = 'appshelpers';
@ -51,15 +41,6 @@ describe('checkAppRequirements', () => {
it('Should throw an error if app does not exist', async () => {
await expect(checkAppRequirements('not-existing-app')).rejects.toThrow('App not-existing-app not found');
});
it('Should return false if a required port is in use', async () => {
const { appInfo, MockFiles } = await createApp({ requiredPort: 53 });
// @ts-ignore
fs.__createMockFiles(MockFiles);
const ivValid = await checkAppRequirements(appInfo.id);
expect(ivValid).toBe(false);
});
});
describe('getEnvMap', () => {
@ -100,9 +81,13 @@ describe('checkEnvFile', () => {
try {
checkEnvFile(app1.id);
expect(true).toBe(false);
} catch (e: any) {
expect(e).toBeDefined();
expect(e.message).toBe('New info needed. App config needs to be updated');
} catch (e: unknown) {
if (e instanceof Error) {
expect(e).toBeDefined();
expect(e.message).toBe('New info needed. App config needs to be updated');
} else {
fail('Should throw an error');
}
}
});
});
@ -161,9 +146,13 @@ describe('Test: generateEnvFile', () => {
try {
generateEnvFile(Object.assign(appEntity1, { config: { TEST_FIELD: undefined } }));
expect(true).toBe(false);
} catch (e: any) {
expect(e).toBeDefined();
expect(e.message).toBe('Variable TEST_FIELD is required');
} catch (e: unknown) {
if (e instanceof Error) {
expect(e).toBeDefined();
expect(e.message).toBe('Variable TEST_FIELD is required');
} else {
fail('Should throw an error');
}
}
});
@ -171,9 +160,13 @@ describe('Test: generateEnvFile', () => {
try {
generateEnvFile(Object.assign(appEntity1, { id: 'not-existing-app' }));
expect(true).toBe(false);
} catch (e: any) {
expect(e).toBeDefined();
expect(e.message).toBe('App not-existing-app not found');
} catch (e: unknown) {
if (e instanceof Error) {
expect(e).toBeDefined();
expect(e.message).toBe('App not-existing-app not found');
} else {
fail('Should throw an error');
}
}
});
@ -329,9 +322,13 @@ describe('Test: getAppInfo', () => {
try {
await getAppInfo(appInfo.id, appEntity.status);
expect(true).toBe(false);
} catch (e: any) {
expect(e.message).toBe(`Error loading app: ${appInfo.id}`);
expect(log).toBeCalledWith(`Error loading app: ${appInfo.id}`);
} catch (e: unknown) {
if (e instanceof Error) {
expect(e.message).toBe(`Error loading app: ${appInfo.id}`);
expect(log).toBeCalledWith(`Error loading app: ${appInfo.id}`);
} else {
expect(true).toBe(false);
}
}
spy.mockRestore();
@ -355,15 +352,89 @@ describe('getUpdateInfo', () => {
});
it('Should return update info', async () => {
const updateInfo = await getUpdateInfo(app1.id);
const updateInfo = await getUpdateInfo(app1.id, 1);
expect(updateInfo?.latest).toBe(app1.tipi_version);
expect(updateInfo?.current).toBe(1);
});
it('Should return null if app is not installed', async () => {
const updateInfo = await getUpdateInfo(faker.random.word());
const updateInfo = await getUpdateInfo(faker.random.word(), 1);
expect(updateInfo).toBeNull();
});
});
describe('Test: ensureAppFolder', () => {
beforeEach(() => {
const mockFiles = {
[`/runtipi/repos/repo-id/apps/test`]: ['test.yml'],
};
// @ts-ignore
fs.__createMockFiles(mockFiles);
});
it('should copy the folder from repo', () => {
// Act
ensureAppFolder('test');
// Assert
const files = fs.readdirSync('/runtipi/apps/test');
expect(files).toEqual(['test.yml']);
});
it('should not copy the folder if it already exists', () => {
const mockFiles = {
[`/runtipi/repos/repo-id/apps/test`]: ['test.yml'],
'/runtipi/apps/test': ['docker-compose.yml'],
'/runtipi/apps/test/docker-compose.yml': 'test',
};
// @ts-ignore
fs.__createMockFiles(mockFiles);
// Act
ensureAppFolder('test');
// Assert
const files = fs.readdirSync('/runtipi/apps/test');
expect(files).toEqual(['docker-compose.yml']);
});
it('Should overwrite the folder if clean up is true', () => {
const mockFiles = {
[`/runtipi/repos/repo-id/apps/test`]: ['test.yml'],
'/runtipi/apps/test': ['docker-compose.yml'],
'/runtipi/apps/test/docker-compose.yml': 'test',
};
// @ts-ignore
fs.__createMockFiles(mockFiles);
// Act
ensureAppFolder('test', true);
// Assert
const files = fs.readdirSync('/runtipi/apps/test');
expect(files).toEqual(['test.yml']);
});
it('Should delete folder if it exists but has no docker-compose.yml file', () => {
// Arrange
const randomFileName = `${faker.random.word()}.yml`;
const mockFiles = {
[`/runtipi/repos/repo-id/apps/test`]: [randomFileName],
'/runtipi/apps/test': ['test.yml'],
};
// @ts-ignore
fs.__createMockFiles(mockFiles);
// Act
ensureAppFolder('test');
// Assert
const files = fs.readdirSync('/runtipi/apps/test');
expect(files).toEqual([randomFileName]);
});
});

View file

@ -1,6 +1,7 @@
import { DataSource } from 'typeorm';
import { setupConnection, teardownConnection } from '../../../test/connection';
import fs from 'fs-extra';
import { faker } from '@faker-js/faker';
import { setupConnection, teardownConnection } from '../../../test/connection';
import { gcall } from '../../../test/gcall';
import App from '../app.entity';
import { getAppQuery, InstalledAppsQuery, listAppInfosQuery } from '../../../test/queries';
@ -9,13 +10,10 @@ import { AppInfo, AppStatusEnum, ListAppsResonse } from '../apps.types';
import { createUser } from '../../auth/__tests__/user.factory';
import User from '../../auth/user.entity';
import { installAppMutation, startAppMutation, stopAppMutation, uninstallAppMutation, updateAppConfigMutation, updateAppMutation } from '../../../test/mutations';
import { faker } from '@faker-js/faker';
import EventDispatcher from '../../../core/config/EventDispatcher';
jest.mock('fs');
jest.mock('child_process');
jest.mock('internal-ip');
jest.mock('tcp-port-used');
type TApp = App & {
info: AppInfo;
@ -220,23 +218,6 @@ describe('InstallApp', () => {
expect(errors?.[0].message).toBe(`Variable ${app1.form_fields?.[0].env_variable} is required`);
expect(data?.installApp).toBeUndefined();
});
it('Should throw an error if the requirements are not met', async () => {
const { appInfo, MockFiles } = await createApp({ requiredPort: 400 });
// @ts-ignore
fs.__createMockFiles(MockFiles);
const user = await createUser();
const { data, errors } = await gcall<{ installApp: TApp }>({
source: installAppMutation,
userId: user.id,
variableValues: { input: { id: appInfo.id, form: { TEST_FIELD: 'hello' }, exposed: false, domain: '' } },
});
expect(errors?.[0].message).toBe(`App ${appInfo.id} requirements not met`);
expect(data?.installApp).toBeUndefined();
});
});
describe('StartApp', () => {

View file

@ -1,10 +1,10 @@
import AppsService from '../apps.service';
import fs from 'fs-extra';
import { DataSource } from 'typeorm';
import AppsService from '../apps.service';
import { AppInfo, AppStatusEnum, AppSupportedArchitecturesEnum } from '../apps.types';
import App from '../app.entity';
import { createApp } from './apps.factory';
import { setupConnection, teardownConnection } from '../../../test/connection';
import { DataSource } from 'typeorm';
import { getEnvMap } from '../apps.helpers';
import EventDispatcher, { eventDispatcher, EventTypes } from '../../../core/config/EventDispatcher';
import { setConfig } from '../../../core/config/TipiConfig';
@ -56,9 +56,9 @@ describe('Install app', () => {
const app = await App.findOne({ where: { id: app1.id } });
expect(app).toBeDefined();
expect(app!.id).toBe(app1.id);
expect(app!.config).toStrictEqual({ TEST_FIELD: 'test' });
expect(app!.status).toBe(AppStatusEnum.RUNNING);
expect(app?.id).toBe(app1.id);
expect(app?.config).toStrictEqual({ TEST_FIELD: 'test' });
expect(app?.status).toBe(AppStatusEnum.RUNNING);
});
it('Should start app if already installed', async () => {
@ -147,7 +147,7 @@ describe('Install app', () => {
const app2 = await createApp({ exposable: true });
const app3 = await createApp({ exposable: true });
// @ts-ignore
fs.__createMockFiles(Object.assign({}, app2.MockFiles, app3.MockFiles));
fs.__createMockFiles({ ...app2.MockFiles, ...app3.MockFiles });
await AppsService.installApp(app2.appInfo.id, { TEST_FIELD: 'test' }, true, 'test.com');
@ -203,8 +203,8 @@ describe('Uninstall app', () => {
// Assert
expect(app).toBeDefined();
expect(app!.id).toBe(app1.id);
expect(app!.status).toBe(AppStatusEnum.RUNNING);
expect(app?.id).toBe(app1.id);
expect(app?.status).toBe(AppStatusEnum.RUNNING);
});
it('Should correctly remove app from database', async () => {
@ -244,7 +244,7 @@ describe('Uninstall app', () => {
// Act & Assert
await expect(AppsService.uninstallApp(app1.id)).rejects.toThrow(`App ${app1.id} failed to uninstall\nstdout: test`);
const app = await App.findOne({ where: { id: app1.id } });
expect(app!.status).toBe(AppStatusEnum.STOPPED);
expect(app?.status).toBe(AppStatusEnum.STOPPED);
});
});
@ -300,7 +300,7 @@ describe('Start app', () => {
// Act & Assert
await expect(AppsService.startApp(app1.id)).rejects.toThrow(`App ${app1.id} failed to start\nstdout: test`);
const app = await App.findOne({ where: { id: app1.id } });
expect(app!.status).toBe(AppStatusEnum.STOPPED);
expect(app?.status).toBe(AppStatusEnum.STOPPED);
});
});
@ -333,7 +333,7 @@ describe('Stop app', () => {
// Act & Assert
await expect(AppsService.stopApp(app1.id)).rejects.toThrow(`App ${app1.id} failed to stop\nstdout: test`);
const app = await App.findOne({ where: { id: app1.id } });
expect(app!.status).toBe(AppStatusEnum.RUNNING);
expect(app?.status).toBe(AppStatusEnum.RUNNING);
});
});
@ -378,17 +378,13 @@ describe('Update app config', () => {
expect(envMap.get('RANDOM_FIELD')).toBe('test');
});
it('Should throw if app is exposed and domain is not provided', () => {
return expect(AppsService.updateAppConfig(app1.id, { TEST_FIELD: 'test' }, true)).rejects.toThrowError('Domain is required');
});
it('Should throw if app is exposed and domain is not provided', () => expect(AppsService.updateAppConfig(app1.id, { TEST_FIELD: 'test' }, true)).rejects.toThrowError('Domain is required'));
it('Should throw if app is exposed and domain is not valid', () => {
return expect(AppsService.updateAppConfig(app1.id, { TEST_FIELD: 'test' }, true, 'test')).rejects.toThrowError('Domain test is not valid');
});
it('Should throw if app is exposed and domain is not valid', () =>
expect(AppsService.updateAppConfig(app1.id, { TEST_FIELD: 'test' }, true, 'test')).rejects.toThrowError('Domain test is not valid'));
it('Should throw if app is exposed and config does not allow it', () => {
return expect(AppsService.updateAppConfig(app1.id, { TEST_FIELD: 'test' }, true, 'test.com')).rejects.toThrowError(`App ${app1.id} is not exposable`);
});
it('Should throw if app is exposed and config does not allow it', () =>
expect(AppsService.updateAppConfig(app1.id, { TEST_FIELD: 'test' }, true, 'test.com')).rejects.toThrowError(`App ${app1.id} is not exposable`));
it('Should throw if app is exposed and domain is already used', async () => {
const app2 = await createApp({ exposable: true, installed: true });
@ -592,6 +588,6 @@ describe('Update app', () => {
await expect(AppsService.updateApp(app1.id)).rejects.toThrow(`App ${app1.id} failed to update\nstdout: error`);
const app = await App.findOne({ where: { id: app1.id } });
expect(app!.status).toBe(AppStatusEnum.STOPPED);
expect(app?.status).toBe(AppStatusEnum.STOPPED);
});
});

View file

@ -70,7 +70,7 @@ class App extends BaseEntity {
@Field(() => UpdateInfo, { nullable: true })
updateInfo(): Promise<UpdateInfo | null> {
return getUpdateInfo(this.id);
return getUpdateInfo(this.id, this.version);
}
}

View file

@ -0,0 +1,6 @@
export interface AppEntityType {
id: string;
config: Record<string, string>;
exposed: boolean;
domain?: string;
}

View file

@ -1,36 +1,23 @@
import portUsed from 'tcp-port-used';
import { fileExists, getSeed, readdirSync, readFile, readJsonFile, writeFile } from '../fs/fs.helpers';
import InternalIp from 'internal-ip';
import crypto from 'crypto';
import fs from 'fs-extra';
import { deleteFolder, fileExists, getSeed, readdirSync, readFile, readJsonFile, writeFile } from '../fs/fs.helpers';
import { AppInfo, AppStatusEnum } from './apps.types';
import logger from '../../config/logger/logger';
import App from './app.entity';
import { getConfig } from '../../core/config/TipiConfig';
import fs from 'fs-extra';
import { AppEntityType } from './app.types';
export const checkAppRequirements = async (appName: string) => {
let valid = true;
const configFile: AppInfo | null = readJsonFile(`/runtipi/repos/${getConfig().appsRepoId}/apps/${appName}/config.json`);
if (!configFile) {
throw new Error(`App ${appName} not found`);
}
if (configFile?.requirements?.ports) {
for (const port of configFile.requirements.ports) {
const ip = await InternalIp.v4();
const used = await portUsed.check(port, ip);
if (used) valid = false;
}
}
if (configFile?.supported_architectures && !configFile.supported_architectures.includes(getConfig().architecture)) {
throw new Error(`App ${appName} is not supported on this architecture`);
}
return valid;
return true;
};
export const getEnvMap = (appName: string): Map<string, string> => {
@ -66,7 +53,7 @@ const getEntropy = (name: string, length: number) => {
return hash.digest('hex').substring(0, length);
};
export const generateEnvFile = (app: App) => {
export const generateEnvFile = (app: AppEntityType) => {
const configFile: AppInfo | null = readJsonFile(`/runtipi/apps/${app.id}/config.json`);
if (!configFile) {
@ -120,9 +107,9 @@ export const getAvailableApps = async (): Promise<string[]> => {
appsDir.forEach((app) => {
if (fileExists(`/runtipi/repos/${getConfig().appsRepoId}/apps/${app}/config.json`)) {
const configFile: AppInfo = readJsonFile(`/runtipi/repos/${getConfig().appsRepoId}/apps/${app}/config.json`);
const configFile = readJsonFile<AppInfo>(`/runtipi/repos/${getConfig().appsRepoId}/apps/${app}/config.json`);
if (configFile.available) {
if (configFile?.available) {
apps.push(app);
}
}
@ -137,14 +124,22 @@ export const getAppInfo = (id: string, status?: AppStatusEnum): AppInfo | null =
const installed = typeof status !== 'undefined' && status !== AppStatusEnum.MISSING;
if (installed && fileExists(`/runtipi/apps/${id}/config.json`)) {
const configFile: AppInfo = readJsonFile(`/runtipi/apps/${id}/config.json`);
configFile.description = readFile(`/runtipi/apps/${id}/metadata/description.md`).toString();
return configFile;
} else if (fileExists(`/runtipi/repos/${getConfig().appsRepoId}/apps/${id}/config.json`)) {
const configFile: AppInfo = readJsonFile(`/runtipi/repos/${getConfig().appsRepoId}/apps/${id}/config.json`);
configFile.description = readFile(`/runtipi/repos/${getConfig().appsRepoId}/apps/${id}/metadata/description.md`);
const configFile = readJsonFile<AppInfo>(`/runtipi/apps/${id}/config.json`);
if (configFile.available) {
if (configFile) {
configFile.description = readFile(`/runtipi/apps/${id}/metadata/description.md`).toString();
}
return configFile;
}
if (fileExists(`/runtipi/repos/${getConfig().appsRepoId}/apps/${id}/config.json`)) {
const configFile = readJsonFile<AppInfo>(`/runtipi/repos/${getConfig().appsRepoId}/apps/${id}/config.json`);
if (configFile) {
configFile.description = readFile(`/runtipi/repos/${getConfig().appsRepoId}/apps/${id}/metadata/description.md`);
}
if (configFile?.available) {
return configFile;
}
}
@ -156,20 +151,36 @@ export const getAppInfo = (id: string, status?: AppStatusEnum): AppInfo | null =
}
};
export const getUpdateInfo = async (id: string) => {
const app = await App.findOne({ where: { id } });
export const getUpdateInfo = async (id: string, version: number) => {
const doesFileExist = fileExists(`/runtipi/repos/${getConfig().appsRepoId}/apps/${id}`);
if (!app || !doesFileExist) {
if (!doesFileExist) {
return null;
}
const repoConfig: AppInfo = readJsonFile(`/runtipi/repos/${getConfig().appsRepoId}/apps/${id}/config.json`);
const repoConfig = readJsonFile<AppInfo>(`/runtipi/repos/${getConfig().appsRepoId}/apps/${id}/config.json`);
if (!repoConfig?.tipi_version) {
return null;
}
return {
current: app.version,
latest: repoConfig.tipi_version,
dockerVersion: repoConfig.version,
current: version || 0,
latest: repoConfig?.tipi_version,
dockerVersion: repoConfig?.version,
};
};
export const ensureAppFolder = (appName: string, cleanup = false) => {
if (cleanup && fileExists(`/runtipi/apps/${appName}`)) {
deleteFolder(`/runtipi/apps/${appName}`);
}
if (!fileExists(`/runtipi/apps/${appName}/docker-compose.yml`)) {
if (fileExists(`/runtipi/apps/${appName}`)) {
deleteFolder(`/runtipi/apps/${appName}`);
}
// Copy from apps repo
fs.copySync(`/runtipi/repos/${getConfig().appsRepoId}/apps/${appName}`, `/runtipi/apps/${appName}`);
}
};

View file

@ -1,10 +1,10 @@
import validator from 'validator';
import { createFolder, ensureAppFolder, readFile, readJsonFile } from '../fs/fs.helpers';
import { checkAppRequirements, checkEnvFile, generateEnvFile, getAvailableApps } from './apps.helpers';
import { Not } from 'typeorm';
import { createFolder, readFile, readJsonFile } from '../fs/fs.helpers';
import { checkAppRequirements, checkEnvFile, generateEnvFile, getAvailableApps, ensureAppFolder } from './apps.helpers';
import { AppInfo, AppStatusEnum, ListAppsResonse } from './apps.types';
import App from './app.entity';
import logger from '../../config/logger/logger';
import { Not } from 'typeorm';
import { getConfig } from '../../core/config/TipiConfig';
import { eventDispatcher, EventTypes } from '../../core/config/EventDispatcher';
@ -18,9 +18,7 @@ const filterApp = (app: AppInfo): boolean => {
return app.supported_architectures.includes(arch);
};
const filterApps = (apps: AppInfo[]): AppInfo[] => {
return apps.sort(sortApps).filter(filterApp);
};
const filterApps = (apps: AppInfo[]): AppInfo[] => apps.sort(sortApps).filter(filterApp);
/**
* Start all apps which had the status RUNNING in the database
@ -157,11 +155,7 @@ const installApp = async (id: string, form: Record<string, string>, exposed?: bo
const listApps = async (): Promise<ListAppsResonse> => {
const folders: string[] = await getAvailableApps();
const apps: AppInfo[] = folders
.map((app) => {
return readJsonFile(`/runtipi/repos/${getConfig().appsRepoId}/apps/${app}/config.json`);
})
.filter(Boolean);
const apps: AppInfo[] = folders.map((app) => readJsonFile(`/runtipi/repos/${getConfig().appsRepoId}/apps/${app}/config.json`)).filter(Boolean);
const filteredApps = filterApps(apps).map((app) => {
const description = readFile(`/runtipi/repos/${getConfig().appsRepoId}/apps/${app.id}/metadata/description.md`);
@ -254,7 +248,7 @@ const stopApp = async (id: string): Promise<App> => {
* @returns - the app entity
*/
const uninstallApp = async (id: string): Promise<App> => {
let app = await App.findOne({ where: { id } });
const app = await App.findOne({ where: { id } });
if (!app) {
throw new Error(`App ${id} not found`);

View file

@ -7,7 +7,7 @@ import { setupConnection, teardownConnection } from '../../../test/connection';
import { gcall } from '../../../test/gcall';
import { loginMutation, registerMutation } from '../../../test/mutations';
import { isConfiguredQuery, MeQuery, refreshTokenQuery } from '../../../test/queries';
import User from '../../auth/user.entity';
import User from '../user.entity';
import { TokenResponse } from '../auth.types';
import { createUser } from './user.factory';
@ -214,7 +214,7 @@ describe('Test: refreshToken', () => {
const { data } = await gcall<{ refreshToken: TokenResponse }>({
source: refreshTokenQuery,
userId: user1.id,
session: session,
session,
});
const decoded = jwt.verify(data?.refreshToken?.token || '', getConfig().jwtSecret) as jwt.JwtPayload;

View file

@ -1,11 +1,11 @@
import * as argon2 from 'argon2';
import jwt from 'jsonwebtoken';
import { faker } from '@faker-js/faker';
import { DataSource } from 'typeorm';
import AuthService from '../auth.service';
import { createUser } from './user.factory';
import User from '../user.entity';
import { faker } from '@faker-js/faker';
import { setupConnection, teardownConnection } from '../../../test/connection';
import { DataSource } from 'typeorm';
import { setConfig } from '../../../core/config/TipiConfig';
import TipiCache from '../../../config/TipiCache';

View file

@ -1,6 +1,6 @@
import User from '../user.entity';
import * as argon2 from 'argon2';
import { faker } from '@faker-js/faker';
import User from '../user.entity';
const createUser = async (email?: string) => {
const hash = await argon2.hash('password');

View file

@ -1,5 +1,4 @@
import { Field, InputType, ObjectType } from 'type-graphql';
import User from './user.entity';
@InputType()
class UsernamePasswordInput {

View file

@ -1,7 +1,5 @@
import { readJsonFile, readFile, readdirSync, fileExists, writeFile, createFolder, deleteFolder, getSeed, ensureAppFolder } from '../fs.helpers';
import fs from 'fs-extra';
import { getConfig } from '../../../core/config/TipiConfig';
import { faker } from '@faker-js/faker';
import { readJsonFile, readFile, readdirSync, fileExists, writeFile, createFolder, deleteFolder, getSeed } from '../fs.helpers';
jest.mock('fs-extra');
@ -15,7 +13,7 @@ describe('Test: readJsonFile', () => {
// Arrange
const rawFile = '{"test": "test"}';
const mockFiles = {
['/runtipi/test-file.json']: rawFile,
'/runtipi/test-file.json': rawFile,
};
// @ts-ignore
fs.__createMockFiles(mockFiles);
@ -52,7 +50,7 @@ describe('Test: readFile', () => {
it('should return the file', () => {
const rawFile = 'test';
const mockFiles = {
['/runtipi/test-file.txt']: rawFile,
'/runtipi/test-file.txt': rawFile,
};
// @ts-ignore
@ -69,7 +67,7 @@ describe('Test: readFile', () => {
describe('Test: readdirSync', () => {
it('should return the files', () => {
const mockFiles = {
['/runtipi/test/test-file.txt']: 'test',
'/runtipi/test/test-file.txt': 'test',
};
// @ts-ignore
@ -86,7 +84,7 @@ describe('Test: readdirSync', () => {
describe('Test: fileExists', () => {
it('should return true if the file exists', () => {
const mockFiles = {
['/runtipi/test-file.txt']: 'test',
'/runtipi/test-file.txt': 'test',
};
// @ts-ignore
@ -133,7 +131,7 @@ describe('Test: deleteFolder', () => {
describe('Test: getSeed', () => {
it('should return the seed', () => {
const mockFiles = {
['/runtipi/state/seed']: 'test',
'/runtipi/state/seed': 'test',
};
// @ts-ignore
@ -142,77 +140,3 @@ describe('Test: getSeed', () => {
expect(getSeed()).toEqual('test');
});
});
describe('Test: ensureAppFolder', () => {
beforeEach(() => {
const mockFiles = {
[`/runtipi/repos/${getConfig().appsRepoId}/apps/test`]: ['test.yml'],
};
// @ts-ignore
fs.__createMockFiles(mockFiles);
});
it('should copy the folder from repo', () => {
// Act
ensureAppFolder('test');
// Assert
const files = fs.readdirSync('/runtipi/apps/test');
expect(files).toEqual(['test.yml']);
});
it('should not copy the folder if it already exists', () => {
const mockFiles = {
[`/runtipi/repos/${getConfig().appsRepoId}/apps/test`]: ['test.yml'],
['/runtipi/apps/test']: ['docker-compose.yml'],
['/runtipi/apps/test/docker-compose.yml']: 'test',
};
// @ts-ignore
fs.__createMockFiles(mockFiles);
// Act
ensureAppFolder('test');
// Assert
const files = fs.readdirSync('/runtipi/apps/test');
expect(files).toEqual(['docker-compose.yml']);
});
it('Should overwrite the folder if clean up is true', () => {
const mockFiles = {
[`/runtipi/repos/${getConfig().appsRepoId}/apps/test`]: ['test.yml'],
['/runtipi/apps/test']: ['docker-compose.yml'],
['/runtipi/apps/test/docker-compose.yml']: 'test',
};
// @ts-ignore
fs.__createMockFiles(mockFiles);
// Act
ensureAppFolder('test', true);
// Assert
const files = fs.readdirSync('/runtipi/apps/test');
expect(files).toEqual(['test.yml']);
});
it('Should delete folder if it exists but has no docker-compose.yml file', () => {
// Arrange
const randomFileName = `${faker.random.word()}.yml`;
const mockFiles = {
[`/runtipi/repos/${getConfig().appsRepoId}/apps/test`]: [randomFileName],
['/runtipi/apps/test']: ['test.yml'],
};
// @ts-ignore
fs.__createMockFiles(mockFiles);
// Act
ensureAppFolder('test');
// Assert
const files = fs.readdirSync('/runtipi/apps/test');
expect(files).toEqual([randomFileName]);
});
});

View file

@ -1,7 +1,6 @@
import fs from 'fs-extra';
import { getConfig } from '../../core/config/TipiConfig';
export const readJsonFile = (path: string): any => {
export const readJsonFile = <T>(path: string): T | null => {
try {
const rawFile = fs.readFileSync(path).toString();
@ -23,7 +22,7 @@ export const readdirSync = (path: string): string[] => fs.readdirSync(path);
export const fileExists = (path: string): boolean => fs.existsSync(path);
export const writeFile = (path: string, data: any) => fs.writeFileSync(path, data);
export const writeFile = (path: string, data: string) => fs.writeFileSync(path, data);
export const createFolder = (path: string) => {
if (!fileExists(path)) {
@ -36,17 +35,3 @@ export const getSeed = () => {
const seed = readFile('/runtipi/state/seed');
return seed.toString();
};
export const ensureAppFolder = (appName: string, cleanup = false) => {
if (cleanup && fileExists(`/runtipi/apps/${appName}`)) {
deleteFolder(`/runtipi/apps/${appName}`);
}
if (!fileExists(`/runtipi/apps/${appName}/docker-compose.yml`)) {
if (fileExists(`/runtipi/apps/${appName}`)) {
deleteFolder(`/runtipi/apps/${appName}`);
}
// Copy from apps repo
fs.copySync(`/runtipi/repos/${getConfig().appsRepoId}/apps/${appName}`, `/runtipi/apps/${appName}`);
}
};

View file

@ -1,8 +1,8 @@
import fs from 'fs-extra';
import semver from 'semver';
import axios from 'axios';
import SystemService from '../system.service';
import { faker } from '@faker-js/faker';
import SystemService from '../system.service';
import TipiCache from '../../../config/TipiCache';
import { setConfig } from '../../../core/config/TipiConfig';
import logger from '../../../config/logger/logger';
@ -21,9 +21,13 @@ describe('Test: systemInfo', () => {
it('Should throw if system-info.json does not exist', () => {
try {
SystemService.systemInfo();
} catch (e: any) {
expect(e).toBeDefined();
expect(e.message).toBe('Error parsing system info');
} catch (e: unknown) {
if (e instanceof Error) {
expect(e).toBeDefined();
expect(e.message).toBe('Error parsing system info');
} else {
fail('Should throw an error');
}
}
});

View file

@ -45,7 +45,7 @@ const getVersion = async (): Promise<{ current: string; latest?: string }> => {
version = data.name.replace('v', '');
}
await TipiCache.set('latestVersion', version?.replace('v', '') || '');
await TipiCache.set('latestVersion', version?.replace('v', '') || '', 60 * 60);
return { current: getConfig().version, latest: version?.replace('v', '') };
} catch (e) {

View file

@ -2,9 +2,11 @@ import 'reflect-metadata';
import express from 'express';
import { ApolloServerPluginLandingPageGraphQLPlayground as Playground } from 'apollo-server-core';
import { ApolloServer } from 'apollo-server-express';
import { createServer } from 'http';
import { ZodError } from 'zod';
import cors, { CorsOptions } from 'cors';
import { createSchema } from './schema';
import { ApolloLogs } from './config/logger/apollo.logger';
import { createServer } from 'http';
import logger from './config/logger/logger';
import getSessionMiddleware from './core/middlewares/sessionMiddleware';
import { MyContext } from './types';
@ -15,10 +17,8 @@ import { runUpdates } from './core/updates/run';
import recover from './core/updates/recover-migrations';
import startJobs from './core/jobs/jobs';
import { applyJsonConfig, getConfig, setConfig } from './core/config/TipiConfig';
import { ZodError } from 'zod';
import systemController from './modules/system/system.controller';
import { eventDispatcher, EventTypes } from './core/config/EventDispatcher';
import cors from 'cors';
const applyCustomConfig = () => {
try {
@ -33,6 +33,13 @@ const applyCustomConfig = () => {
}
};
const corsOptions: CorsOptions = {
credentials: false,
origin: (_, callback) => {
callback(null, true);
},
};
const main = async () => {
try {
eventDispatcher.clear();
@ -41,8 +48,8 @@ const main = async () => {
const app = express();
const port = 3001;
app.use(cors(corsOptions));
app.use(express.static(`${getConfig().rootFolder}/repos/${getConfig().appsRepoId}`));
app.use(cors());
app.use('/status', systemController.status);
app.use(getSessionMiddleware);

View file

@ -1,7 +1,7 @@
import { DataSource } from 'typeorm';
import pg from 'pg';
import App from '../modules/apps/app.entity';
import User from '../modules/auth/user.entity';
import pg from 'pg';
import Update from '../modules/system/update.entity';
const HOST = 'localhost';

View file

@ -5,7 +5,7 @@ import { createSchema } from '../schema';
interface Options {
source: string;
variableValues?: Maybe<{
[key: string]: any;
[key: string]: unknown;
}>;
userId?: number;
session?: string;
@ -13,7 +13,7 @@ interface Options {
let schema: GraphQLSchema | null = null;
export const gcall = async <T>({ source, variableValues, userId, session }: Options): Promise<ExecutionResult<T, { [key: string]: any }>> => {
export const gcall = async <T>({ source, variableValues, userId, session }: Options): Promise<ExecutionResult<T, { [key: string]: unknown }>> => {
if (!schema) {
schema = await createSchema();
}
@ -23,5 +23,5 @@ export const gcall = async <T>({ source, variableValues, userId, session }: Opti
source,
variableValues,
contextValue: { req: { session: { userId, id: session } } },
}) as any;
}) as unknown as ExecutionResult<T, { [key: string]: unknown }>;
};

View file

@ -35,6 +35,7 @@ importers:
'@types/react': 18.0.8
'@types/react-dom': 18.0.3
'@types/react-slick': ^0.23.8
'@types/semver': ^7.3.12
'@types/validator': ^13.7.2
'@typescript-eslint/eslint-plugin': ^5.18.0
'@typescript-eslint/parser': ^5.0.0
@ -60,6 +61,7 @@ importers:
remark-breaks: ^3.0.2
remark-gfm: ^3.0.1
remark-mdx: ^2.1.1
semver: ^7.3.7
swr: ^1.3.0
tailwindcss: ^3.0.23
ts-jest: ^28.0.2
@ -88,6 +90,7 @@ importers:
remark-breaks: 3.0.2
remark-gfm: 3.0.1
remark-mdx: 2.1.1
semver: 7.3.7
swr: 1.3.0_react@18.1.0
tslib: 2.4.0
validator: 13.7.0
@ -103,6 +106,7 @@ importers:
'@types/react': 18.0.8
'@types/react-dom': 18.0.3
'@types/react-slick': 0.23.8
'@types/semver': 7.3.12
'@types/validator': 13.7.2
'@typescript-eslint/eslint-plugin': 5.22.0_oztpoyrbzkyaikrhdkppp3gagu
'@typescript-eslint/parser': 5.22.0_uhoeudlwl7kc47h4kncsfowede
@ -131,7 +135,6 @@ importers:
'@types/node-cron': ^3.0.2
'@types/pg': ^8.6.5
'@types/semver': ^7.3.12
'@types/tcp-port-used': ^1.0.1
'@types/uuid': ^8.3.4
'@types/validator': ^13.7.2
'@typescript-eslint/eslint-plugin': ^5.18.0
@ -155,12 +158,10 @@ importers:
graphql-import-node: ^0.0.5
graphql-type-json: ^0.3.2
http: 0.0.1-security
internal-ip: ^6.0.0
jest: ^28.1.0
jsonwebtoken: ^8.5.1
node-cache: ^5.1.2
node-cron: ^3.0.1
node-port-scanner: ^3.0.1
nodemon: ^2.0.15
pg: ^8.7.3
prettier: 2.6.2
@ -168,7 +169,6 @@ importers:
reflect-metadata: ^0.1.13
rimraf: ^3.0.2
semver: ^7.3.7
tcp-port-used: ^1.0.2
ts-jest: ^28.0.2
ts-node: ^10.8.2
type-graphql: ^1.1.1
@ -191,16 +191,13 @@ importers:
graphql: 15.8.0
graphql-type-json: 0.3.2_graphql@15.8.0
http: 0.0.1-security
internal-ip: 6.2.0
jsonwebtoken: 8.5.1
node-cache: 5.1.2
node-cron: 3.0.1
node-port-scanner: 3.0.1
pg: 8.7.3
redis: 4.3.1
reflect-metadata: 0.1.13
semver: 7.3.7
tcp-port-used: 1.0.2
type-graphql: 1.1.1_v2revtygxcm7xrdg2oz3ssohfu
typeorm: 0.3.6_rymjtjxvmmxrsowl5wrmwxcyqa
uuid: 9.0.0
@ -220,7 +217,6 @@ importers:
'@types/node-cron': 3.0.2
'@types/pg': 8.6.5
'@types/semver': 7.3.12
'@types/tcp-port-used': 1.0.1
'@types/uuid': 8.3.4
'@types/validator': 13.7.2
'@typescript-eslint/eslint-plugin': 5.22.0_tal4xlmvnofklupd3hwjtzfb4q
@ -3961,10 +3957,6 @@ packages:
resolution: {integrity: sha512-Hl219/BT5fLAaz6NDkSuhzasy49dwQS/DSdu4MdggFB8zcXv7vflBI3xp7FEmkmdDkBUI2bPUNeMttp2knYdxw==}
dev: true
/@types/tcp-port-used/1.0.1:
resolution: {integrity: sha512-6pwWTx8oUtWvsiZUCrhrK/53MzKVLnuNSSaZILPy3uMes9QnTrLMar9BDlJArbMOjDcjb3QXFk6Rz8qmmuySZw==}
dev: true
/@types/unist/2.0.6:
resolution: {integrity: sha512-PBjIUxZHOuj0R15/xuwJYjFi+KZdNFrehocChv4g5hu6aFroHue8m0lBP0POdK2nKzbw0cgV1mws8+V/JAcEkQ==}
dev: false
@ -5628,6 +5620,7 @@ packages:
path-key: 3.1.1
shebang-command: 2.0.0
which: 2.0.2
dev: true
/cross-undici-fetch/0.4.7:
resolution: {integrity: sha512-e5KZdjHigxFECfw1B7cjmXLm3yT8eiffSJYUSyIWxy6c+f/MGiJsV1NHegZvG23ZgQ0o8rNaZxbtu5NdF5FmwQ==}
@ -5742,18 +5735,6 @@ packages:
supports-color: 5.5.0
dev: true
/debug/4.3.1:
resolution: {integrity: sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==}
engines: {node: '>=6.0'}
peerDependencies:
supports-color: '*'
peerDependenciesMeta:
supports-color:
optional: true
dependencies:
ms: 2.1.2
dev: false
/debug/4.3.4:
resolution: {integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==}
engines: {node: '>=6.0'}
@ -5802,19 +5783,13 @@ packages:
/deep-is/0.1.4:
resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==}
dev: true
/deepmerge/4.2.2:
resolution: {integrity: sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg==}
engines: {node: '>=0.10.0'}
dev: true
/default-gateway/6.0.3:
resolution: {integrity: sha512-fwSOJsbbNzZ/CUFpqFBqYfYNLj1NbMPm8MMCIzHjC83iSJRBEGmDUxU+WP661BaBQImeC2yHwXtz+P/O9o+XEg==}
engines: {node: '>= 10'}
dependencies:
execa: 5.1.1
dev: false
/defaults/1.0.3:
resolution: {integrity: sha512-s82itHOnYrN0Ib8r+z7laQz3sdE+4FP3d9Q7VLO7U+KRT+CR0GsWuyHxzdAY82I7cXv0G/twrqomTJLOssO5HA==}
dependencies:
@ -6668,6 +6643,7 @@ packages:
onetime: 5.1.2
signal-exit: 3.0.7
strip-final-newline: 2.0.0
dev: true
/exit/0.1.2:
resolution: {integrity: sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ==}
@ -7131,6 +7107,7 @@ packages:
/get-stream/6.0.1:
resolution: {integrity: sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==}
engines: {node: '>=10'}
dev: true
/get-symbol-description/1.0.0:
resolution: {integrity: sha512-2EmdH1YvIQiZpltCNgkuiUnyukzxM/R6NDJX31Ke3BG1Nq5b0S2PhX59UKi9vZpPDQVdqn+1IcaAwnzTT5vCjw==}
@ -7527,6 +7504,7 @@ packages:
/human-signals/2.1.0:
resolution: {integrity: sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==}
engines: {node: '>=10.17.0'}
dev: true
/husky/8.0.1:
resolution: {integrity: sha512-xs7/chUH/CKdOCs7Zy0Aev9e/dKOMZf3K1Az1nar3tzlv0jfqnYtu235bstsWTmXOR0EfINrPa97yy4Lz6RiKw==}
@ -7641,16 +7619,6 @@ packages:
wrap-ansi: 7.0.0
dev: true
/internal-ip/6.2.0:
resolution: {integrity: sha512-D8WGsR6yDt8uq7vDMu7mjcR+yRMm3dW8yufyChmszWRjcSHuxLBkR3GdS2HZAjodsaGuCvXeEJpueisXJULghg==}
engines: {node: '>=10'}
dependencies:
default-gateway: 6.0.3
ipaddr.js: 1.9.1
is-ip: 3.1.0
p-event: 4.2.0
dev: false
/internal-slot/1.0.3:
resolution: {integrity: sha512-O0DB1JC/sPyZl7cIo78n5dR7eUSwwpYPiXRhTzNxZVAMUuB8vlnRFyLxdrVToks6XPLVnFfbzaVd5WLjhgg+vA==}
engines: {node: '>= 0.4'}
@ -7665,11 +7633,6 @@ packages:
dependencies:
loose-envify: 1.4.0
/ip-regex/4.3.0:
resolution: {integrity: sha512-B9ZWJxHHOHUhUjCPrMpLD4xEq35bUTClHM1S6CBU5ixQnkZmwipwgc96vAd7AAGM9TGHvJR+Uss+/Ak6UphK+Q==}
engines: {node: '>=8'}
dev: false
/ipaddr.js/1.9.1:
resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==}
engines: {node: '>= 0.10'}
@ -7805,13 +7768,6 @@ packages:
engines: {node: '>=8'}
dev: true
/is-ip/3.1.0:
resolution: {integrity: sha512-35vd5necO7IitFPjd/YBeqwWnyDWbuLH9ZXQdMfDA8TEo7pv5X8yfrvVO3xbJbLUlERCMvf6X0hTUamQxCYJ9Q==}
engines: {node: '>=8'}
dependencies:
ip-regex: 4.3.0
dev: false
/is-lower-case/2.0.2:
resolution: {integrity: sha512-bVcMJy4X5Og6VZfdOZstSexlEy20Sr0k/p/b2IlQJlfdKAQuMpiv5w2Ccxb8sKdRUNAG1PnHVHjFSdRDVS6NlQ==}
dependencies:
@ -7944,10 +7900,6 @@ packages:
tslib: 2.4.0
dev: true
/is-url/1.2.4:
resolution: {integrity: sha512-ITvGim8FhRiYe4IQ5uHSkj7pVaPDrCTkNd3yq3cV7iZAcJdHTUMPMEHcqSOy9xZ9qFenQCvi+2wjH9a1nXqHww==}
dev: false
/is-utf8/0.2.1:
resolution: {integrity: sha512-rMYPYvCzsXywIsldgLaSoPlw5PfoB/ssr7hY4pLfcodrA5M/eArza1a9VmTiNIBNMjOGr1Ow9mTyU2o69U6U9Q==}
dev: true
@ -7967,17 +7919,9 @@ packages:
resolution: {integrity: sha512-VjSeb/lHmkoyd8ryPVIKvOCn4D1koMqY+vqyjjUfc3xyKtP4dYOxM44sZrnqQSzSds3xyOrUTLTC9LVCVgLngw==}
dev: true
/is2/2.0.7:
resolution: {integrity: sha512-4vBQoURAXC6hnLFxD4VW7uc04XiwTTl/8ydYJxKvPwkWQrSjInkuM5VZVg6BGr1/natq69zDuvO9lGpLClJqvA==}
engines: {node: '>=v0.10.0'}
dependencies:
deep-is: 0.1.4
ip-regex: 4.3.0
is-url: 1.2.4
dev: false
/isexe/2.0.0:
resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==}
dev: true
/isomorphic-fetch/3.0.0:
resolution: {integrity: sha512-qvUtwJ3j6qwsF3jLxkZ72qCgjMysPzDfeV240JHiGZsANBYd+EEuu35v7dfrJ9Up0Ak07D7GGSkGhCHTqg/5wA==}
@ -9203,6 +9147,7 @@ packages:
/merge-stream/2.0.0:
resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==}
dev: true
/merge/2.1.1:
resolution: {integrity: sha512-jz+Cfrg9GWOZbQAnDQ4hlVnQky+341Yk5ru8bZSe6sIDTCIg8n9i/u7hSQGSVOF3C7lH6mGtqjkiT9G4wFLL0w==}
@ -9594,6 +9539,7 @@ packages:
/mimic-fn/2.1.0:
resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==}
engines: {node: '>=6'}
dev: true
/mimic-response/1.0.1:
resolution: {integrity: sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==}
@ -9778,10 +9724,6 @@ packages:
resolution: {integrity: sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==}
dev: true
/node-port-scanner/3.0.1:
resolution: {integrity: sha512-TuFGEWfye+1atB74v0Vm6myEjpq0L5Jo3UaOG9xgtYHxnFZN0fF9CnwCxp7ENWDerGbI1UXAgdRMIPz8TM73Hg==}
dev: false
/node-releases/2.0.4:
resolution: {integrity: sha512-gbMzqQtTtDz/00jQzZ21PQzdI9PyLYqUSvD0p3naOhX4odFji0ZxYdnVwPTxmSwkmxhcFImpozceidSG+AgoPQ==}
dev: true
@ -9865,6 +9807,7 @@ packages:
engines: {node: '>=8'}
dependencies:
path-key: 3.1.1
dev: true
/npmlog/5.0.1:
resolution: {integrity: sha512-AqZtDUWOMKs1G/8lwylVjrdYgqA4d9nu8hc+0gzRxlDb1I10+FHBGMXs6aiQHFdCUUlqH99MUMuLfzWDNDtfxw==}
@ -9975,6 +9918,7 @@ packages:
engines: {node: '>=6'}
dependencies:
mimic-fn: 2.1.0
dev: true
/optimism/0.16.1:
resolution: {integrity: sha512-64i+Uw3otrndfq5kaoGNoY7pvOhSsjFEN4bdEFh80MWVk/dbgJfMv7VFDeCT8LxNAlEVhQmdVEbfE7X2nWNIIg==}
@ -10020,18 +9964,6 @@ packages:
engines: {node: '>=6'}
dev: true
/p-event/4.2.0:
resolution: {integrity: sha512-KXatOjCRXXkSePPb1Nbi0p0m+gQAwdlbhi4wQKJPI1HsMQS9g+Sqp2o+QHziPr7eYJyOZet836KoHEVM1mwOrQ==}
engines: {node: '>=8'}
dependencies:
p-timeout: 3.2.0
dev: false
/p-finally/1.0.0:
resolution: {integrity: sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4=}
engines: {node: '>=4'}
dev: false
/p-limit/1.3.0:
resolution: {integrity: sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q==}
engines: {node: '>=4'}
@ -10079,13 +10011,6 @@ packages:
engines: {node: '>=6'}
dev: true
/p-timeout/3.2.0:
resolution: {integrity: sha512-rhIwUycgwwKcP9yTOOFK/AKsAopjjCakVqLHePO3CC6Mir1Z99xT+R63jZxAT5lFZLa2inS5h+ZS2GvR99/FBg==}
engines: {node: '>=8'}
dependencies:
p-finally: 1.0.0
dev: false
/p-try/1.0.0:
resolution: {integrity: sha512-U1etNYuMJoIz3ZXSrrySFjsXQTWOx2/jdi86L+2pRvph/qMKL6sbcCYdH23fqsbm8TH2Gn0OybpT4eSFlCVHww==}
engines: {node: '>=4'}
@ -10209,6 +10134,7 @@ packages:
/path-key/3.1.1:
resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==}
engines: {node: '>=8'}
dev: true
/path-parse/1.0.7:
resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==}
@ -11150,10 +11076,12 @@ packages:
engines: {node: '>=8'}
dependencies:
shebang-regex: 3.0.0
dev: true
/shebang-regex/3.0.0:
resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==}
engines: {node: '>=8'}
dev: true
/side-channel/1.0.4:
resolution: {integrity: sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==}
@ -11412,6 +11340,7 @@ packages:
/strip-final-newline/2.0.0:
resolution: {integrity: sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==}
engines: {node: '>=6'}
dev: true
/strip-indent/3.0.0:
resolution: {integrity: sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==}
@ -11576,15 +11505,6 @@ packages:
yallist: 4.0.0
dev: false
/tcp-port-used/1.0.2:
resolution: {integrity: sha512-l7ar8lLUD3XS1V2lfoJlCBaeoaWo/2xfYt81hM7VlvR4RrMVFqfmzfhLVk40hAb368uitje5gPtBRL1m/DGvLA==}
dependencies:
debug: 4.3.1
is2: 2.0.7
transitivePeerDependencies:
- supports-color
dev: false
/terminal-link/2.1.1:
resolution: {integrity: sha512-un0FmiRUQNr5PJqy9kP7c40F5BOfpGlYTrxonDChEZB7pzZxRNp/bt+ymiy9/npwXya9KH99nJ/GXFIiUkYGFQ==}
engines: {node: '>=8'}
@ -12438,6 +12358,7 @@ packages:
hasBin: true
dependencies:
isexe: 2.0.0
dev: true
/wide-align/1.1.5:
resolution: {integrity: sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==}

View file

@ -14,7 +14,7 @@ source "${BASH_SOURCE%/*}/common.sh"
ROOT_FOLDER="${PWD}"
STATE_FOLDER="${ROOT_FOLDER}/state"
SED_ROOT_FOLDER="$(echo "$ROOT_FOLDER" | sed 's/\//\\\//g')"
NGINX_PORT=80
NGINX_PORT=3000
NGINX_PORT_SSL=443
DOMAIN=tipi.localhost
DNS_IP="9.9.9.9" # Default to Quad9 DNS