Merge pull request #246 from meienberger/feat/jwt-auth
feat: move from cookie base auth to jwt auth
This commit is contained in:
commit
1f8dbf9c91
39 changed files with 744 additions and 285 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -10,6 +10,7 @@ github.secrets
|
|||
node_modules/
|
||||
app-data/*
|
||||
data/postgres
|
||||
data/redis
|
||||
!app-data/.gitkeep
|
||||
repos/*
|
||||
!repos/.gitkeep
|
||||
|
|
|
@ -39,6 +39,15 @@ services:
|
|||
networks:
|
||||
- tipi_main_network
|
||||
|
||||
tipi-redis:
|
||||
container_name: tipi-redis
|
||||
image: redis:alpine
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
- ./data/redis:/data
|
||||
networks:
|
||||
- tipi_main_network
|
||||
|
||||
api:
|
||||
build:
|
||||
context: .
|
||||
|
@ -121,6 +130,8 @@ services:
|
|||
# Middlewares
|
||||
traefik.http.middlewares.redirect-middleware.redirectregex.regex: .*
|
||||
traefik.http.middlewares.redirect-middleware.redirectregex.replacement: /dashboard
|
||||
|
||||
|
||||
|
||||
networks:
|
||||
tipi_main_network:
|
||||
|
|
|
@ -36,6 +36,15 @@ services:
|
|||
networks:
|
||||
- tipi_main_network
|
||||
|
||||
tipi-redis:
|
||||
container_name: tipi-redis
|
||||
image: redis:alpine
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
- ./data/redis:/data
|
||||
networks:
|
||||
- tipi_main_network
|
||||
|
||||
api:
|
||||
image: meienberger/runtipi:rc-${TIPI_VERSION}
|
||||
command: /bin/sh -c "cd /api && npm run start"
|
||||
|
|
|
@ -35,6 +35,15 @@ services:
|
|||
networks:
|
||||
- tipi_main_network
|
||||
|
||||
tipi-redis:
|
||||
container_name: tipi-redis
|
||||
image: redis:alpine
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
- ./data/redis:/data
|
||||
networks:
|
||||
- tipi_main_network
|
||||
|
||||
api:
|
||||
image: meienberger/runtipi:${TIPI_VERSION}
|
||||
command: /bin/sh -c "cd /api && npm run start"
|
||||
|
|
|
@ -1,11 +1,12 @@
|
|||
import { Flex, useDisclosure, Spinner, Breadcrumb, BreadcrumbItem, useColorModeValue, Box } from '@chakra-ui/react';
|
||||
import Head from 'next/head';
|
||||
import Link from 'next/link';
|
||||
import React from 'react';
|
||||
import React, { useEffect } from 'react';
|
||||
import { FiChevronRight } from 'react-icons/fi';
|
||||
import Header from './Header';
|
||||
import Menu from './SideMenu';
|
||||
import MenuDrawer from './MenuDrawer';
|
||||
import { useRefreshTokenQuery } from '../../generated/graphql';
|
||||
|
||||
interface IProps {
|
||||
loading?: boolean;
|
||||
|
@ -15,6 +16,13 @@ interface IProps {
|
|||
|
||||
const Layout: React.FC<IProps> = ({ children, loading, breadcrumbs }) => {
|
||||
const { isOpen, onClose, onOpen } = useDisclosure();
|
||||
const { data } = useRefreshTokenQuery({ fetchPolicy: 'network-only' });
|
||||
|
||||
useEffect(() => {
|
||||
if (data?.refreshToken?.token) {
|
||||
localStorage.setItem('token', data.refreshToken.token);
|
||||
}
|
||||
}, [data]);
|
||||
|
||||
const menubg = useColorModeValue('#F1F3F4', '#202736');
|
||||
const bg = useColorModeValue('white', '#1a202c');
|
||||
|
|
|
@ -45,6 +45,11 @@ const SideMenu: React.FC = () => {
|
|||
setColorMode(checked ? 'dark' : 'light');
|
||||
};
|
||||
|
||||
const handleLogout = async () => {
|
||||
localStorage.removeItem('token');
|
||||
logout();
|
||||
};
|
||||
|
||||
return (
|
||||
<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} />
|
||||
|
@ -64,7 +69,7 @@ const SideMenu: React.FC = () => {
|
|||
<p className="flex-1 mb-1 text-md">Donate</p>
|
||||
</ListItem>
|
||||
</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" />
|
||||
<p className="flex-1">Log out</p>
|
||||
</ListItem>
|
||||
|
|
|
@ -2,7 +2,7 @@ import { ApolloClient, from, InMemoryCache } from '@apollo/client';
|
|||
import links from './links';
|
||||
|
||||
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({
|
||||
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) => {
|
||||
return new HttpLink({
|
||||
uri: `${url}/graphql`,
|
||||
credentials: 'include',
|
||||
});
|
||||
};
|
||||
|
||||
|
|
|
@ -1,9 +1,11 @@
|
|||
import errorLink from './errorLink';
|
||||
import httpLink from './httpLink';
|
||||
import authLink from './authLink';
|
||||
|
||||
const links = {
|
||||
errorLink,
|
||||
httpLink,
|
||||
authLink,
|
||||
};
|
||||
|
||||
export default links;
|
||||
|
|
|
@ -134,9 +134,9 @@ export type ListAppsResonse = {
|
|||
export type Mutation = {
|
||||
__typename?: 'Mutation';
|
||||
installApp: App;
|
||||
login: UserResponse;
|
||||
login: TokenResponse;
|
||||
logout: Scalars['Boolean'];
|
||||
register: UserResponse;
|
||||
register: TokenResponse;
|
||||
restart: Scalars['Boolean'];
|
||||
startApp: App;
|
||||
stopApp: App;
|
||||
|
@ -185,6 +185,7 @@ export type Query = {
|
|||
isConfigured: Scalars['Boolean'];
|
||||
listAppsInfo: ListAppsResonse;
|
||||
me?: Maybe<User>;
|
||||
refreshToken?: Maybe<TokenResponse>;
|
||||
systemInfo?: Maybe<SystemInfoResponse>;
|
||||
version: VersionResponse;
|
||||
};
|
||||
|
@ -200,6 +201,11 @@ export type SystemInfoResponse = {
|
|||
memory: DiskMemory;
|
||||
};
|
||||
|
||||
export type TokenResponse = {
|
||||
__typename?: 'TokenResponse';
|
||||
token: Scalars['String'];
|
||||
};
|
||||
|
||||
export type UpdateInfo = {
|
||||
__typename?: 'UpdateInfo';
|
||||
current: Scalars['Float'];
|
||||
|
@ -215,11 +221,6 @@ export type User = {
|
|||
username: Scalars['String'];
|
||||
};
|
||||
|
||||
export type UserResponse = {
|
||||
__typename?: 'UserResponse';
|
||||
user?: Maybe<User>;
|
||||
};
|
||||
|
||||
export type UsernamePasswordInput = {
|
||||
password: Scalars['String'];
|
||||
username: Scalars['String'];
|
||||
|
@ -241,7 +242,7 @@ export type LoginMutationVariables = Exact<{
|
|||
input: UsernamePasswordInput;
|
||||
}>;
|
||||
|
||||
export type LoginMutation = { __typename?: 'Mutation'; login: { __typename?: 'UserResponse'; user?: { __typename?: 'User'; id: string } | null } };
|
||||
export type LoginMutation = { __typename?: 'Mutation'; login: { __typename?: 'TokenResponse'; token: string } };
|
||||
|
||||
export type LogoutMutationVariables = Exact<{ [key: string]: never }>;
|
||||
|
||||
|
@ -251,7 +252,7 @@ export type RegisterMutationVariables = Exact<{
|
|||
input: UsernamePasswordInput;
|
||||
}>;
|
||||
|
||||
export type RegisterMutation = { __typename?: 'Mutation'; register: { __typename?: 'UserResponse'; user?: { __typename?: 'User'; id: string } | null } };
|
||||
export type RegisterMutation = { __typename?: 'Mutation'; register: { __typename?: 'TokenResponse'; token: string } };
|
||||
|
||||
export type RestartMutationVariables = Exact<{ [key: string]: never }>;
|
||||
|
||||
|
@ -382,6 +383,10 @@ export type MeQueryVariables = Exact<{ [key: string]: never }>;
|
|||
|
||||
export type MeQuery = { __typename?: 'Query'; me?: { __typename?: 'User'; id: string } | null };
|
||||
|
||||
export type RefreshTokenQueryVariables = Exact<{ [key: string]: never }>;
|
||||
|
||||
export type RefreshTokenQuery = { __typename?: 'Query'; refreshToken?: { __typename?: 'TokenResponse'; token: string } | null };
|
||||
|
||||
export type SystemInfoQueryVariables = Exact<{ [key: string]: never }>;
|
||||
|
||||
export type SystemInfoQuery = {
|
||||
|
@ -436,9 +441,7 @@ export type InstallAppMutationOptions = Apollo.BaseMutationOptions<InstallAppMut
|
|||
export const LoginDocument = gql`
|
||||
mutation Login($input: UsernamePasswordInput!) {
|
||||
login(input: $input) {
|
||||
user {
|
||||
id
|
||||
}
|
||||
token
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
@ -501,9 +504,7 @@ export type LogoutMutationOptions = Apollo.BaseMutationOptions<LogoutMutation, L
|
|||
export const RegisterDocument = gql`
|
||||
mutation Register($input: UsernamePasswordInput!) {
|
||||
register(input: $input) {
|
||||
user {
|
||||
id
|
||||
}
|
||||
token
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
@ -1000,6 +1001,40 @@ export function useMeLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<MeQuery
|
|||
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
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
/**
|
||||
* __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`
|
||||
query SystemInfo {
|
||||
systemInfo {
|
||||
|
|
|
@ -1,7 +1,5 @@
|
|||
mutation Login($input: UsernamePasswordInput!) {
|
||||
login(input: $input) {
|
||||
user {
|
||||
id
|
||||
}
|
||||
token
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,7 +1,5 @@
|
|||
mutation Register($input: UsernamePasswordInput!) {
|
||||
register(input: $input) {
|
||||
user {
|
||||
id
|
||||
}
|
||||
token
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
query RefreshToken {
|
||||
refreshToken {
|
||||
token
|
||||
}
|
||||
}
|
|
@ -1,3 +1,4 @@
|
|||
import { useApolloClient } from '@apollo/client';
|
||||
import { useToast } from '@chakra-ui/react';
|
||||
import React, { useState } from 'react';
|
||||
import { useLoginMutation } from '../../../generated/graphql';
|
||||
|
@ -7,11 +8,13 @@ import LoginForm from '../components/LoginForm';
|
|||
type FormValues = { email: string; password: string };
|
||||
|
||||
const Login: React.FC = () => {
|
||||
const [login] = useLoginMutation({ refetchQueries: ['Me'] });
|
||||
const client = useApolloClient();
|
||||
const [login] = useLoginMutation({});
|
||||
const [loading, setLoading] = useState(false);
|
||||
const toast = useToast();
|
||||
|
||||
const handleError = (error: unknown) => {
|
||||
localStorage.removeItem('token');
|
||||
if (error instanceof Error) {
|
||||
toast({
|
||||
title: 'Error',
|
||||
|
@ -26,7 +29,13 @@ const Login: React.FC = () => {
|
|||
const handleLogin = async (values: FormValues) => {
|
||||
try {
|
||||
setLoading(true);
|
||||
await login({ variables: { input: { username: values.email, password: values.password } } });
|
||||
const { data } = await login({ variables: { input: { username: values.email, password: values.password } } });
|
||||
|
||||
if (data?.login?.token) {
|
||||
localStorage.setItem('token', data.login.token);
|
||||
}
|
||||
|
||||
await client.refetchQueries({ include: ['Me'] });
|
||||
} catch (error) {
|
||||
handleError(error);
|
||||
} finally {
|
||||
|
|
19
packages/system-api/__mocks__/redis.ts
Normal file
19
packages/system-api/__mocks__/redis.ts
Normal file
|
@ -0,0 +1,19 @@
|
|||
module.exports = {
|
||||
createClient: jest.fn(() => {
|
||||
const values = new Map();
|
||||
return {
|
||||
isOpen: true,
|
||||
connect: jest.fn(),
|
||||
set: (key: string, value: string) => {
|
||||
values.set(key, value);
|
||||
},
|
||||
get: (key: string) => {
|
||||
return values.get(key);
|
||||
},
|
||||
quit: jest.fn(),
|
||||
del: (key: string) => {
|
||||
return values.delete(key);
|
||||
},
|
||||
};
|
||||
}),
|
||||
};
|
|
@ -33,22 +33,23 @@
|
|||
"cors": "^2.8.5",
|
||||
"dotenv": "^16.0.0",
|
||||
"express": "^4.17.3",
|
||||
"express-session": "^1.17.3",
|
||||
"fs-extra": "^10.1.0",
|
||||
"graphql": "^15.3.0",
|
||||
"graphql-type-json": "^0.3.2",
|
||||
"http": "0.0.1-security",
|
||||
"internal-ip": "^6.0.0",
|
||||
"jsonwebtoken": "^8.5.1",
|
||||
"node-cache": "^5.1.2",
|
||||
"node-cron": "^3.0.1",
|
||||
"node-port-scanner": "^3.0.1",
|
||||
"pg": "^8.7.3",
|
||||
"redis": "^4.3.1",
|
||||
"reflect-metadata": "^0.1.13",
|
||||
"semver": "^7.3.7",
|
||||
"session-file-store": "^1.5.0",
|
||||
"tcp-port-used": "^1.0.2",
|
||||
"type-graphql": "^1.1.1",
|
||||
"typeorm": "^0.3.6",
|
||||
"uuid": "^9.0.0",
|
||||
"validator": "^13.7.0",
|
||||
"winston": "^3.7.2",
|
||||
"zod": "^3.19.1"
|
||||
|
@ -59,15 +60,15 @@
|
|||
"@swc/core": "^1.2.210",
|
||||
"@types/cors": "^2.8.12",
|
||||
"@types/express": "^4.17.13",
|
||||
"@types/express-session": "^1.17.4",
|
||||
"@types/fs-extra": "^9.0.13",
|
||||
"@types/jest": "^27.5.0",
|
||||
"@types/jsonwebtoken": "^8.5.9",
|
||||
"@types/node": "17.0.31",
|
||||
"@types/node-cron": "^3.0.2",
|
||||
"@types/pg": "^8.6.5",
|
||||
"@types/semver": "^7.3.12",
|
||||
"@types/session-file-store": "^1.2.2",
|
||||
"@types/tcp-port-used": "^1.0.1",
|
||||
"@types/uuid": "^8.3.4",
|
||||
"@types/validator": "^13.7.2",
|
||||
"@typescript-eslint/eslint-plugin": "^5.18.0",
|
||||
"@typescript-eslint/parser": "^5.22.0",
|
||||
|
|
|
@ -1,5 +1,56 @@
|
|||
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();
|
||||
}
|
||||
}
|
||||
|
||||
export default TipiCache.getInstance();
|
||||
|
|
|
@ -21,10 +21,12 @@ const {
|
|||
APPS_REPO_URL = '',
|
||||
DOMAIN = '',
|
||||
STORAGE_PATH = '/runtipi',
|
||||
REDIS_HOST = 'tipi-redis',
|
||||
} = process.env;
|
||||
|
||||
const configSchema = z.object({
|
||||
NODE_ENV: z.union([z.literal('development'), z.literal('production'), z.literal('test')]),
|
||||
REDIS_HOST: z.string(),
|
||||
status: z.union([z.literal('RUNNING'), z.literal('UPDATING'), z.literal('RESTARTING')]),
|
||||
logs: z.object({
|
||||
LOGS_FOLDER: z.string(),
|
||||
|
@ -55,6 +57,7 @@ class Config {
|
|||
LOGS_APP,
|
||||
LOGS_ERROR,
|
||||
},
|
||||
REDIS_HOST,
|
||||
NODE_ENV: NODE_ENV as z.infer<typeof configSchema>['NODE_ENV'],
|
||||
rootFolder: '/runtipi',
|
||||
internalIp: INTERNAL_IP,
|
||||
|
|
|
@ -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 SessionFileStore from 'session-file-store';
|
||||
import { COOKIE_MAX_AGE, __prod__ } from '../../config/constants/constants';
|
||||
import { NextFunction, Request, Response } from 'express';
|
||||
import jwt from 'jsonwebtoken';
|
||||
import logger from '../../config/logger/logger';
|
||||
import TipiCache from '../../config/TipiCache';
|
||||
import { getConfig } from '../config/TipiConfig';
|
||||
|
||||
const getSessionMiddleware = () => {
|
||||
const FileStore = SessionFileStore(session);
|
||||
const getSessionMiddleware = async (req: Request, _: Response, next: NextFunction) => {
|
||||
req.session = {};
|
||||
|
||||
const sameSite = __prod__ ? 'lax' : 'none';
|
||||
const token = req.headers.authorization?.split(' ')[1];
|
||||
|
||||
return session({
|
||||
name: 'qid',
|
||||
store: new FileStore(),
|
||||
cookie: { maxAge: COOKIE_MAX_AGE, secure: false, sameSite, httpOnly: true },
|
||||
secret: getConfig().jwtSecret,
|
||||
resave: false,
|
||||
saveUninitialized: false,
|
||||
});
|
||||
if (token) {
|
||||
try {
|
||||
const decodedToken = jwt.verify(token, getConfig().jwtSecret) as { id: number; session: string };
|
||||
|
||||
const userId = await TipiCache.get(decodedToken.session);
|
||||
|
||||
if (userId === decodedToken.id.toString()) {
|
||||
req.session = {
|
||||
userId: decodedToken.id,
|
||||
id: decodedToken.session,
|
||||
};
|
||||
}
|
||||
} catch (err) {
|
||||
logger.error(err);
|
||||
}
|
||||
}
|
||||
|
||||
next();
|
||||
};
|
||||
|
||||
export default getSessionMiddleware;
|
||||
|
|
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 };
|
||||
|
|
|
@ -1,13 +1,18 @@
|
|||
import { faker } from '@faker-js/faker';
|
||||
import jwt from 'jsonwebtoken';
|
||||
import { DataSource } from 'typeorm';
|
||||
import TipiCache from '../../../config/TipiCache';
|
||||
import { getConfig } from '../../../core/config/TipiConfig';
|
||||
import { setupConnection, teardownConnection } from '../../../test/connection';
|
||||
import { gcall } from '../../../test/gcall';
|
||||
import { loginMutation, registerMutation } from '../../../test/mutations';
|
||||
import { isConfiguredQuery, MeQuery } from '../../../test/queries';
|
||||
import { isConfiguredQuery, MeQuery, refreshTokenQuery } from '../../../test/queries';
|
||||
import User from '../../auth/user.entity';
|
||||
import { UserResponse } from '../auth.types';
|
||||
import { TokenResponse } from '../auth.types';
|
||||
import { createUser } from './user.factory';
|
||||
|
||||
jest.mock('redis');
|
||||
|
||||
let db: DataSource | null = null;
|
||||
const TEST_SUITE = 'authresolver';
|
||||
|
||||
|
@ -58,20 +63,30 @@ describe('Test: register', () => {
|
|||
const password = faker.internet.password();
|
||||
|
||||
it('should register a user', async () => {
|
||||
const { data } = await gcall<{ register: UserResponse }>({
|
||||
const { data } = await gcall<{ register: TokenResponse }>({
|
||||
source: registerMutation,
|
||||
variableValues: {
|
||||
input: { username: email, password },
|
||||
},
|
||||
});
|
||||
|
||||
expect(data?.register.user?.username).toEqual(email.toLowerCase());
|
||||
expect(data?.register).toBeDefined();
|
||||
expect(data?.register?.token).toBeDefined();
|
||||
|
||||
const decoded = jwt.verify(data?.register?.token || '', getConfig().jwtSecret) as jwt.JwtPayload;
|
||||
|
||||
expect(decoded).toBeDefined();
|
||||
expect(decoded).not.toBeNull();
|
||||
expect(decoded).toHaveProperty('id');
|
||||
expect(decoded).toHaveProperty('iat');
|
||||
expect(decoded).toHaveProperty('exp');
|
||||
expect(decoded).toHaveProperty('session');
|
||||
});
|
||||
|
||||
it('should not register a user with an existing username', async () => {
|
||||
await createUser(email);
|
||||
|
||||
const { errors } = await gcall<{ register: UserResponse }>({
|
||||
const { errors } = await gcall<{ register: TokenResponse }>({
|
||||
source: registerMutation,
|
||||
variableValues: {
|
||||
input: { username: email, password },
|
||||
|
@ -82,7 +97,7 @@ describe('Test: register', () => {
|
|||
});
|
||||
|
||||
it('should not register a user with a malformed email', async () => {
|
||||
const { errors } = await gcall<{ register: UserResponse }>({
|
||||
const { errors } = await gcall<{ register: TokenResponse }>({
|
||||
source: registerMutation,
|
||||
variableValues: {
|
||||
input: { username: 'not an email', password },
|
||||
|
@ -101,18 +116,27 @@ describe('Test: login', () => {
|
|||
});
|
||||
|
||||
it('should login a user', async () => {
|
||||
const { data } = await gcall<{ login: UserResponse }>({
|
||||
const { data } = await gcall<{ login: TokenResponse }>({
|
||||
source: loginMutation,
|
||||
variableValues: {
|
||||
input: { username: email, password: 'password' },
|
||||
},
|
||||
});
|
||||
|
||||
expect(data?.login.user?.username).toEqual(email.toLowerCase());
|
||||
const token = data?.login.token as string;
|
||||
|
||||
expect(token).toBeDefined();
|
||||
|
||||
const decoded = jwt.verify(token, getConfig().jwtSecret) as { id: string; session: string };
|
||||
|
||||
const user = await User.findOne({ where: { username: email.toLowerCase().trim() } });
|
||||
|
||||
expect(decoded.id).toBeDefined();
|
||||
expect(user?.id).toEqual(decoded.id);
|
||||
});
|
||||
|
||||
it('should not login a user with an incorrect password', async () => {
|
||||
const { errors } = await gcall<{ login: UserResponse }>({
|
||||
const { errors } = await gcall<{ login: TokenResponse }>({
|
||||
source: loginMutation,
|
||||
variableValues: {
|
||||
input: { username: email, password: 'wrong password' },
|
||||
|
@ -123,7 +147,7 @@ describe('Test: login', () => {
|
|||
});
|
||||
|
||||
it('should not login a user with a malformed email', async () => {
|
||||
const { errors } = await gcall<{ login: UserResponse }>({
|
||||
const { errors } = await gcall<{ login: TokenResponse }>({
|
||||
source: loginMutation,
|
||||
variableValues: {
|
||||
input: { username: 'not an email', password: 'password' },
|
||||
|
@ -146,6 +170,7 @@ describe('Test: logout', () => {
|
|||
const { data } = await gcall<{ logout: boolean }>({
|
||||
source: 'mutation { logout }',
|
||||
userId: user1.id,
|
||||
session: 'session',
|
||||
});
|
||||
|
||||
expect(data?.logout).toBeTruthy();
|
||||
|
@ -171,3 +196,39 @@ describe('Test: isConfigured', () => {
|
|||
expect(data?.isConfigured).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Test: refreshToken', () => {
|
||||
const email = faker.internet.email();
|
||||
let user1: User;
|
||||
|
||||
beforeEach(async () => {
|
||||
user1 = await createUser(email);
|
||||
});
|
||||
|
||||
it('should return a new token', async () => {
|
||||
// Arrange
|
||||
const session = faker.datatype.uuid();
|
||||
await TipiCache.set(session, user1.id.toString());
|
||||
|
||||
// Act
|
||||
const { data } = await gcall<{ refreshToken: TokenResponse }>({
|
||||
source: refreshTokenQuery,
|
||||
userId: user1.id,
|
||||
session: session,
|
||||
});
|
||||
const decoded = jwt.verify(data?.refreshToken?.token || '', getConfig().jwtSecret) as jwt.JwtPayload;
|
||||
|
||||
// Assert
|
||||
expect(data?.refreshToken).toBeDefined();
|
||||
expect(data?.refreshToken?.token).toBeDefined();
|
||||
expect(decoded).toBeDefined();
|
||||
expect(decoded).not.toBeNull();
|
||||
expect(decoded).toHaveProperty('id');
|
||||
expect(decoded).toHaveProperty('iat');
|
||||
expect(decoded).toHaveProperty('exp');
|
||||
expect(decoded).toHaveProperty('session');
|
||||
|
||||
expect(decoded.id).toEqual(user1.id.toString());
|
||||
expect(decoded.session).not.toEqual(session);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,15 +1,21 @@
|
|||
import * as argon2 from 'argon2';
|
||||
import jwt from 'jsonwebtoken';
|
||||
import AuthService from '../auth.service';
|
||||
import { createUser } from './user.factory';
|
||||
import User from '../user.entity';
|
||||
import { faker } from '@faker-js/faker';
|
||||
import { setupConnection, teardownConnection } from '../../../test/connection';
|
||||
import { DataSource } from 'typeorm';
|
||||
import { setConfig } from '../../../core/config/TipiConfig';
|
||||
import TipiCache from '../../../config/TipiCache';
|
||||
|
||||
let db: DataSource | null = null;
|
||||
const TEST_SUITE = 'authservice';
|
||||
|
||||
jest.mock('redis');
|
||||
|
||||
beforeAll(async () => {
|
||||
setConfig('jwtSecret', 'test');
|
||||
db = await setupConnection(TEST_SUITE);
|
||||
});
|
||||
|
||||
|
@ -23,14 +29,24 @@ afterAll(async () => {
|
|||
});
|
||||
|
||||
describe('Login', () => {
|
||||
it('Should return user after login', async () => {
|
||||
it('Should return a valid jsonwebtoken containing a user id', async () => {
|
||||
// Arrange
|
||||
const email = faker.internet.email();
|
||||
await createUser(email);
|
||||
const user = await createUser(email);
|
||||
|
||||
const { user } = await AuthService.login({ username: email, password: 'password' });
|
||||
// Act
|
||||
const { token } = await AuthService.login({ username: email, password: 'password' });
|
||||
const decoded = jwt.verify(token, 'test') as jwt.JwtPayload;
|
||||
|
||||
expect(user).toBeDefined();
|
||||
expect(user?.id).toBe(1);
|
||||
// Assert
|
||||
expect(decoded).toBeDefined();
|
||||
expect(decoded).toBeDefined();
|
||||
expect(decoded).not.toBeNull();
|
||||
expect(decoded).toHaveProperty('id');
|
||||
expect(decoded.id).toBe(user.id);
|
||||
expect(decoded).toHaveProperty('iat');
|
||||
expect(decoded).toHaveProperty('exp');
|
||||
expect(decoded).toHaveProperty('session');
|
||||
});
|
||||
|
||||
it('Should throw if user does not exist', async () => {
|
||||
|
@ -45,26 +61,41 @@ describe('Login', () => {
|
|||
});
|
||||
|
||||
describe('Register', () => {
|
||||
it('Should return new user after register', async () => {
|
||||
it('Should return valid jsonwebtoken after register', async () => {
|
||||
// Arrange
|
||||
const email = faker.internet.email();
|
||||
const { user } = await AuthService.register({ username: email, password: 'test' });
|
||||
|
||||
expect(user).toBeDefined();
|
||||
// Act
|
||||
const { token } = await AuthService.register({ username: email, password: 'password' });
|
||||
const decoded = jwt.verify(token, 'test') as jwt.JwtPayload;
|
||||
|
||||
// Assert
|
||||
expect(decoded).toBeDefined();
|
||||
expect(decoded).not.toBeNull();
|
||||
expect(decoded).toHaveProperty('id');
|
||||
expect(decoded).toHaveProperty('iat');
|
||||
expect(decoded).toHaveProperty('exp');
|
||||
expect(decoded).toHaveProperty('session');
|
||||
});
|
||||
|
||||
it('Should correctly trim and lowercase email', async () => {
|
||||
// Arrange
|
||||
const email = faker.internet.email();
|
||||
await AuthService.register({ username: email, password: 'test' });
|
||||
|
||||
// Act
|
||||
await AuthService.register({ username: email, password: 'test' });
|
||||
const user = await User.findOne({ where: { username: email.toLowerCase().trim() } });
|
||||
|
||||
// Assert
|
||||
expect(user).toBeDefined();
|
||||
expect(user?.username).toBe(email.toLowerCase().trim());
|
||||
});
|
||||
|
||||
it('Should throw if user already exists', async () => {
|
||||
// Arrange
|
||||
const email = faker.internet.email();
|
||||
|
||||
// Act & Assert
|
||||
await createUser(email);
|
||||
await expect(AuthService.register({ username: email, password: 'test' })).rejects.toThrowError('User already exists');
|
||||
});
|
||||
|
@ -78,11 +109,125 @@ describe('Register', () => {
|
|||
});
|
||||
|
||||
it('Password is correctly hashed', async () => {
|
||||
const email = faker.internet.email();
|
||||
const { user } = await AuthService.register({ username: email, password: 'test' });
|
||||
// Arrange
|
||||
const email = faker.internet.email().toLowerCase().trim();
|
||||
|
||||
// Act
|
||||
await AuthService.register({ username: email, password: 'test' });
|
||||
const user = await User.findOne({ where: { username: email } });
|
||||
const isPasswordValid = await argon2.verify(user?.password || '', 'test');
|
||||
|
||||
// Assert
|
||||
expect(isPasswordValid).toBe(true);
|
||||
});
|
||||
|
||||
it('Should throw if email is invalid', async () => {
|
||||
await expect(AuthService.register({ username: 'test', password: 'test' })).rejects.toThrowError('Invalid username');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Test: logout', () => {
|
||||
it('Should return true if there is no session to delete', async () => {
|
||||
// Act
|
||||
// @ts-ignore
|
||||
const result = await AuthService.logout();
|
||||
|
||||
// Assert
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('Should delete session from cache', async () => {
|
||||
// Arrange
|
||||
const session = faker.random.alphaNumeric(32);
|
||||
await TipiCache.set(session, 'test');
|
||||
expect(await TipiCache.get(session)).toBe('test');
|
||||
|
||||
// Act
|
||||
const result = await AuthService.logout(session);
|
||||
|
||||
// Assert
|
||||
expect(result).toBe(true);
|
||||
expect(await TipiCache.get('session')).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Test: refreshToken', () => {
|
||||
it('Should return null if session is not provided', async () => {
|
||||
// Act
|
||||
const result = await AuthService.refreshToken();
|
||||
|
||||
// Assert
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it('Should return null if session is not found in cache', async () => {
|
||||
// Act
|
||||
const result = await AuthService.refreshToken('test');
|
||||
|
||||
// Assert
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it('Should return a new token if session is found in cache', async () => {
|
||||
// Arrange
|
||||
const session = faker.random.alphaNumeric(32);
|
||||
await TipiCache.set(session, 'test');
|
||||
|
||||
// Act
|
||||
const result = await AuthService.refreshToken(session);
|
||||
|
||||
// Assert
|
||||
expect(result).not.toBeNull();
|
||||
expect(result).toHaveProperty('token');
|
||||
expect(result?.token).not.toBe(session);
|
||||
});
|
||||
|
||||
it('Should delete old session from cache', async () => {
|
||||
// Arrange
|
||||
const session = faker.random.alphaNumeric(32);
|
||||
await TipiCache.set(session, '1');
|
||||
|
||||
// Act
|
||||
const result = await AuthService.refreshToken(session);
|
||||
|
||||
// Assert
|
||||
expect(result).not.toBeNull();
|
||||
expect(result).toHaveProperty('token');
|
||||
expect(result?.token).not.toBe(session);
|
||||
expect(await TipiCache.get(session)).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
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 { MyContext } from '../../types';
|
||||
import { UsernamePasswordInput, UserResponse } from './auth.types';
|
||||
import { TokenResponse, UsernamePasswordInput } from './auth.types';
|
||||
|
||||
import AuthService from './auth.service';
|
||||
import User from './user.entity';
|
||||
|
@ -9,34 +9,31 @@ import User from './user.entity';
|
|||
export default class AuthResolver {
|
||||
@Query(() => User, { nullable: true })
|
||||
async me(@Ctx() ctx: MyContext): Promise<User | null> {
|
||||
return AuthService.me(ctx.req.session.userId);
|
||||
return AuthService.me(ctx.req?.session?.userId);
|
||||
}
|
||||
|
||||
@Mutation(() => UserResponse)
|
||||
async register(@Arg('input', () => UsernamePasswordInput) input: UsernamePasswordInput, @Ctx() { req }: MyContext): Promise<UserResponse> {
|
||||
const { user } = await AuthService.register(input);
|
||||
@Mutation(() => TokenResponse)
|
||||
async register(@Arg('input', () => UsernamePasswordInput) input: UsernamePasswordInput): Promise<TokenResponse> {
|
||||
const { token } = await AuthService.register(input);
|
||||
|
||||
if (user) {
|
||||
req.session.userId = user.id;
|
||||
}
|
||||
|
||||
return { user };
|
||||
return { token };
|
||||
}
|
||||
|
||||
@Mutation(() => UserResponse)
|
||||
async login(@Arg('input', () => UsernamePasswordInput) input: UsernamePasswordInput, @Ctx() { req }: MyContext): Promise<UserResponse> {
|
||||
const { user } = await AuthService.login(input);
|
||||
@Mutation(() => TokenResponse)
|
||||
async login(@Arg('input', () => UsernamePasswordInput) input: UsernamePasswordInput): Promise<TokenResponse> {
|
||||
const { token } = await AuthService.login(input);
|
||||
|
||||
if (user) {
|
||||
req.session.userId = user.id;
|
||||
}
|
||||
|
||||
return { user };
|
||||
return { token };
|
||||
}
|
||||
|
||||
@Mutation(() => Boolean)
|
||||
logout(@Ctx() { req }: MyContext): boolean {
|
||||
async logout(@Ctx() { req }: MyContext): Promise<boolean> {
|
||||
if (req.session.id) {
|
||||
await AuthService.logout(req.session?.id);
|
||||
}
|
||||
|
||||
req.session.userId = undefined;
|
||||
req.session.id = undefined;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
@ -47,4 +44,11 @@ export default class AuthResolver {
|
|||
|
||||
return users.length > 0;
|
||||
}
|
||||
|
||||
@Query(() => TokenResponse, { nullable: true })
|
||||
async refreshToken(@Ctx() { req }: MyContext): Promise<TokenResponse | null> {
|
||||
const res = await AuthService.refreshToken(req.session?.id);
|
||||
|
||||
return res;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,9 +1,13 @@
|
|||
import * as argon2 from 'argon2';
|
||||
import { v4 } from 'uuid';
|
||||
import jwt from 'jsonwebtoken';
|
||||
import validator from 'validator';
|
||||
import { UsernamePasswordInput, UserResponse } from './auth.types';
|
||||
import { getConfig } from '../../core/config/TipiConfig';
|
||||
import { TokenResponse, UsernamePasswordInput } from './auth.types';
|
||||
import User from './user.entity';
|
||||
import TipiCache from '../../config/TipiCache';
|
||||
|
||||
const login = async (input: UsernamePasswordInput): Promise<UserResponse> => {
|
||||
const login = async (input: UsernamePasswordInput): Promise<TokenResponse> => {
|
||||
const { password, username } = input;
|
||||
|
||||
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');
|
||||
}
|
||||
|
||||
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 email = username.trim().toLowerCase();
|
||||
|
||||
|
@ -42,7 +51,12 @@ const register = async (input: UsernamePasswordInput): Promise<UserResponse> =>
|
|||
const hash = await argon2.hash(password);
|
||||
const newUser = await User.create({ username: email, password: hash }).save();
|
||||
|
||||
return { user: newUser };
|
||||
const session = v4();
|
||||
const token = jwt.sign({ id: newUser.id, session }, getConfig().jwtSecret, { expiresIn: '1d' });
|
||||
|
||||
await TipiCache.set(session, newUser.id.toString());
|
||||
|
||||
return { token };
|
||||
};
|
||||
|
||||
const me = async (userId?: number): Promise<User | null> => {
|
||||
|
@ -55,10 +69,33 @@ const me = async (userId?: number): Promise<User | null> => {
|
|||
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;
|
||||
|
||||
await TipiCache.del(session);
|
||||
|
||||
const newSession = v4();
|
||||
const token = jwt.sign({ id: userId, session: newSession }, getConfig().jwtSecret, { expiresIn: '1d' });
|
||||
await TipiCache.set(newSession, userId);
|
||||
|
||||
return { token };
|
||||
};
|
||||
|
||||
const AuthService = {
|
||||
login,
|
||||
register,
|
||||
me,
|
||||
logout,
|
||||
refreshToken,
|
||||
};
|
||||
|
||||
export default AuthService;
|
||||
|
|
|
@ -11,9 +11,9 @@ class UsernamePasswordInput {
|
|||
}
|
||||
|
||||
@ObjectType()
|
||||
class UserResponse {
|
||||
@Field(() => User, { nullable: true })
|
||||
user?: User;
|
||||
class TokenResponse {
|
||||
@Field(() => String, { nullable: false })
|
||||
token!: string;
|
||||
}
|
||||
|
||||
export { UsernamePasswordInput, UserResponse };
|
||||
export { UsernamePasswordInput, TokenResponse };
|
||||
|
|
|
@ -16,6 +16,7 @@ import EventDispatcher from '../../../core/config/EventDispatcher';
|
|||
|
||||
jest.mock('fs-extra');
|
||||
jest.mock('axios');
|
||||
jest.mock('redis');
|
||||
|
||||
beforeEach(async () => {
|
||||
jest.resetModules();
|
||||
|
@ -42,8 +43,6 @@ beforeEach(async () => {
|
|||
});
|
||||
|
||||
describe('Test: systemInfo', () => {
|
||||
beforeEach(async () => {});
|
||||
|
||||
it('Should return correct system info from file', async () => {
|
||||
const systemInfo = {
|
||||
cpu: { load: 10 },
|
||||
|
|
|
@ -10,6 +10,7 @@ import EventDispatcher from '../../../core/config/EventDispatcher';
|
|||
|
||||
jest.mock('fs-extra');
|
||||
jest.mock('axios');
|
||||
jest.mock('redis');
|
||||
|
||||
beforeEach(async () => {
|
||||
jest.resetModules();
|
||||
|
|
|
@ -37,16 +37,15 @@ const systemInfo = (): z.infer<typeof systemInfoSchema> => {
|
|||
|
||||
const getVersion = async (): Promise<{ current: string; latest?: string }> => {
|
||||
try {
|
||||
let version = TipiCache.get<string>('latestVersion');
|
||||
let version = await TipiCache.get('latestVersion');
|
||||
|
||||
if (!version) {
|
||||
const { data } = await axios.get('https://api.github.com/repos/meienberger/runtipi/releases/latest');
|
||||
|
||||
TipiCache.set('latestVersion', data.name);
|
||||
version = data.name.replace('v', '');
|
||||
}
|
||||
|
||||
TipiCache.set('latestVersion', version?.replace('v', ''));
|
||||
await TipiCache.set('latestVersion', version?.replace('v', '') || '');
|
||||
|
||||
return { current: getConfig().version, latest: version?.replace('v', '') };
|
||||
} catch (e) {
|
||||
|
|
|
@ -9,7 +9,6 @@ import logger from './config/logger/logger';
|
|||
import getSessionMiddleware from './core/middlewares/sessionMiddleware';
|
||||
import { MyContext } from './types';
|
||||
import { __prod__ } from './config/constants/constants';
|
||||
import cors from 'cors';
|
||||
import datasource from './config/datasource';
|
||||
import appsService from './modules/apps/apps.service';
|
||||
import { runUpdates } from './core/updates/run';
|
||||
|
@ -19,24 +18,7 @@ import { applyJsonConfig, getConfig, setConfig } from './core/config/TipiConfig'
|
|||
import { ZodError } from 'zod';
|
||||
import systemController from './modules/system/system.controller';
|
||||
import { eventDispatcher, EventTypes } from './core/config/EventDispatcher';
|
||||
|
||||
let corsOptions = {
|
||||
credentials: true,
|
||||
origin: function (origin: any, callback: any) {
|
||||
if (!__prod__) {
|
||||
return callback(null, true);
|
||||
}
|
||||
// disallow requests with no origin
|
||||
if (!origin) return callback(new Error('Not allowed by CORS'), false);
|
||||
|
||||
if (getConfig().clientUrls.includes(origin)) {
|
||||
return callback(null, true);
|
||||
}
|
||||
|
||||
const message = "The CORS policy for this origin doesn't allow access from the particular origin.";
|
||||
return callback(new Error(message), false);
|
||||
},
|
||||
};
|
||||
import cors from 'cors';
|
||||
|
||||
const applyCustomConfig = () => {
|
||||
try {
|
||||
|
@ -60,9 +42,9 @@ const main = async () => {
|
|||
const port = 3001;
|
||||
|
||||
app.use(express.static(`${getConfig().rootFolder}/repos/${getConfig().appsRepoId}`));
|
||||
app.use(cors());
|
||||
app.use('/status', systemController.status);
|
||||
app.use(cors(corsOptions));
|
||||
app.use(getSessionMiddleware());
|
||||
app.use(getSessionMiddleware);
|
||||
|
||||
await datasource.initialize();
|
||||
|
||||
|
@ -71,7 +53,7 @@ const main = async () => {
|
|||
const plugins = [ApolloLogs];
|
||||
|
||||
if (!__prod__) {
|
||||
plugins.push(Playground({ settings: { 'request.credentials': 'include' } }));
|
||||
plugins.push(Playground());
|
||||
}
|
||||
|
||||
const apolloServer = new ApolloServer({
|
||||
|
@ -81,7 +63,7 @@ const main = async () => {
|
|||
});
|
||||
|
||||
await apolloServer.start();
|
||||
apolloServer.applyMiddleware({ app, cors: corsOptions });
|
||||
apolloServer.applyMiddleware({ app });
|
||||
|
||||
try {
|
||||
await datasource.runMigrations();
|
||||
|
|
|
@ -8,11 +8,12 @@ interface Options {
|
|||
[key: string]: any;
|
||||
}>;
|
||||
userId?: number;
|
||||
session?: string;
|
||||
}
|
||||
|
||||
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) {
|
||||
schema = await createSchema();
|
||||
}
|
||||
|
@ -21,6 +22,6 @@ export const gcall = async <T>({ source, variableValues, userId }: Options): Pro
|
|||
schema,
|
||||
source,
|
||||
variableValues,
|
||||
contextValue: { req: { session: { userId } } },
|
||||
contextValue: { req: { session: { userId, id: session } } },
|
||||
}) as any;
|
||||
};
|
||||
|
|
|
@ -1,9 +1,6 @@
|
|||
# Write your query or mutation here
|
||||
mutation Login($input: UsernamePasswordInput!) {
|
||||
login(input: $input) {
|
||||
user {
|
||||
id
|
||||
username
|
||||
}
|
||||
token
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,9 +1,6 @@
|
|||
# Write your query or mutation here
|
||||
mutation Register($input: UsernamePasswordInput!) {
|
||||
register(input: $input) {
|
||||
user {
|
||||
id
|
||||
username
|
||||
}
|
||||
token
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,6 +9,7 @@ import * as Me from './me.graphql';
|
|||
import * as isConfigured from './isConfigured.graphql';
|
||||
import * as systemInfo from './systemInfo.graphql';
|
||||
import * as version from './version.graphql';
|
||||
import * as refreshToken from './refreshToken.graphql';
|
||||
|
||||
export const listAppInfosQuery = print(listAppInfos);
|
||||
export const getAppQuery = print(getApp);
|
||||
|
@ -17,3 +18,4 @@ export const MeQuery = print(Me);
|
|||
export const isConfiguredQuery = print(isConfigured);
|
||||
export const systemInfoQuery = print(systemInfo);
|
||||
export const versionQuery = print(version);
|
||||
export const refreshTokenQuery = print(refreshToken);
|
||||
|
|
|
@ -0,0 +1,6 @@
|
|||
# Write your query or mutation here
|
||||
query RefreshToken {
|
||||
refreshToken {
|
||||
token
|
||||
}
|
||||
}
|
|
@ -1,11 +1,4 @@
|
|||
import { Request, Response } from 'express';
|
||||
import 'express-session';
|
||||
|
||||
declare module 'express-session' {
|
||||
interface SessionData {
|
||||
userId: number;
|
||||
}
|
||||
}
|
||||
|
||||
export type MyContext = {
|
||||
req: Request;
|
||||
|
|
247
pnpm-lock.yaml
generated
247
pnpm-lock.yaml
generated
|
@ -124,15 +124,15 @@ importers:
|
|||
'@swc/core': ^1.2.210
|
||||
'@types/cors': ^2.8.12
|
||||
'@types/express': ^4.17.13
|
||||
'@types/express-session': ^1.17.4
|
||||
'@types/fs-extra': ^9.0.13
|
||||
'@types/jest': ^27.5.0
|
||||
'@types/jsonwebtoken': ^8.5.9
|
||||
'@types/node': 17.0.31
|
||||
'@types/node-cron': ^3.0.2
|
||||
'@types/pg': ^8.6.5
|
||||
'@types/semver': ^7.3.12
|
||||
'@types/session-file-store': ^1.2.2
|
||||
'@types/tcp-port-used': ^1.0.1
|
||||
'@types/uuid': ^8.3.4
|
||||
'@types/validator': ^13.7.2
|
||||
'@typescript-eslint/eslint-plugin': ^5.18.0
|
||||
'@typescript-eslint/parser': ^5.22.0
|
||||
|
@ -150,7 +150,6 @@ importers:
|
|||
eslint-plugin-import: ^2.26.0
|
||||
eslint-plugin-prettier: ^4.0.0
|
||||
express: ^4.17.3
|
||||
express-session: ^1.17.3
|
||||
fs-extra: ^10.1.0
|
||||
graphql: ^15.3.0
|
||||
graphql-import-node: ^0.0.5
|
||||
|
@ -158,22 +157,24 @@ importers:
|
|||
http: 0.0.1-security
|
||||
internal-ip: ^6.0.0
|
||||
jest: ^28.1.0
|
||||
jsonwebtoken: ^8.5.1
|
||||
node-cache: ^5.1.2
|
||||
node-cron: ^3.0.1
|
||||
node-port-scanner: ^3.0.1
|
||||
nodemon: ^2.0.15
|
||||
pg: ^8.7.3
|
||||
prettier: 2.6.2
|
||||
redis: ^4.3.1
|
||||
reflect-metadata: ^0.1.13
|
||||
rimraf: ^3.0.2
|
||||
semver: ^7.3.7
|
||||
session-file-store: ^1.5.0
|
||||
tcp-port-used: ^1.0.2
|
||||
ts-jest: ^28.0.2
|
||||
ts-node: ^10.8.2
|
||||
type-graphql: ^1.1.1
|
||||
typeorm: ^0.3.6
|
||||
typescript: 4.6.4
|
||||
uuid: ^9.0.0
|
||||
validator: ^13.7.0
|
||||
winston: ^3.7.2
|
||||
zod: ^3.19.1
|
||||
|
@ -186,22 +187,23 @@ importers:
|
|||
cors: 2.8.5
|
||||
dotenv: 16.0.0
|
||||
express: 4.18.1
|
||||
express-session: 1.17.3
|
||||
fs-extra: 10.1.0
|
||||
graphql: 15.8.0
|
||||
graphql-type-json: 0.3.2_graphql@15.8.0
|
||||
http: 0.0.1-security
|
||||
internal-ip: 6.2.0
|
||||
jsonwebtoken: 8.5.1
|
||||
node-cache: 5.1.2
|
||||
node-cron: 3.0.1
|
||||
node-port-scanner: 3.0.1
|
||||
pg: 8.7.3
|
||||
redis: 4.3.1
|
||||
reflect-metadata: 0.1.13
|
||||
semver: 7.3.7
|
||||
session-file-store: 1.5.0
|
||||
tcp-port-used: 1.0.2
|
||||
type-graphql: 1.1.1_v2revtygxcm7xrdg2oz3ssohfu
|
||||
typeorm: 0.3.6_pg@8.7.3+ts-node@10.8.2
|
||||
typeorm: 0.3.6_rymjtjxvmmxrsowl5wrmwxcyqa
|
||||
uuid: 9.0.0
|
||||
validator: 13.7.0
|
||||
winston: 3.7.2
|
||||
zod: 3.19.1
|
||||
|
@ -211,15 +213,15 @@ importers:
|
|||
'@swc/core': 1.2.210
|
||||
'@types/cors': 2.8.12
|
||||
'@types/express': 4.17.13
|
||||
'@types/express-session': 1.17.4
|
||||
'@types/fs-extra': 9.0.13
|
||||
'@types/jest': 27.5.0
|
||||
'@types/jsonwebtoken': 8.5.9
|
||||
'@types/node': 17.0.31
|
||||
'@types/node-cron': 3.0.2
|
||||
'@types/pg': 8.6.5
|
||||
'@types/semver': 7.3.12
|
||||
'@types/session-file-store': 1.2.2
|
||||
'@types/tcp-port-used': 1.0.1
|
||||
'@types/uuid': 8.3.4
|
||||
'@types/validator': 13.7.2
|
||||
'@typescript-eslint/eslint-plugin': 5.22.0_tal4xlmvnofklupd3hwjtzfb4q
|
||||
'@typescript-eslint/parser': 5.22.0_hcfsmds2fshutdssjqluwm76uu
|
||||
|
@ -3360,6 +3362,55 @@ packages:
|
|||
resolution: {integrity: sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==}
|
||||
dev: false
|
||||
|
||||
/@redis/bloom/1.0.2_@redis+client@1.3.0:
|
||||
resolution: {integrity: sha512-EBw7Ag1hPgFzdznK2PBblc1kdlj5B5Cw3XwI9/oG7tSn85/HKy3X9xHy/8tm/eNXJYHLXHJL/pkwBpFMVVefkw==}
|
||||
peerDependencies:
|
||||
'@redis/client': ^1.0.0
|
||||
dependencies:
|
||||
'@redis/client': 1.3.0
|
||||
dev: false
|
||||
|
||||
/@redis/client/1.3.0:
|
||||
resolution: {integrity: sha512-XCFV60nloXAefDsPnYMjHGtvbtHR8fV5Om8cQ0JYqTNbWcQo/4AryzJ2luRj4blveWazRK/j40gES8M7Cp6cfQ==}
|
||||
engines: {node: '>=14'}
|
||||
dependencies:
|
||||
cluster-key-slot: 1.1.0
|
||||
generic-pool: 3.8.2
|
||||
yallist: 4.0.0
|
||||
dev: false
|
||||
|
||||
/@redis/graph/1.0.1_@redis+client@1.3.0:
|
||||
resolution: {integrity: sha512-oDE4myMCJOCVKYMygEMWuriBgqlS5FqdWerikMoJxzmmTUErnTRRgmIDa2VcgytACZMFqpAOWDzops4DOlnkfQ==}
|
||||
peerDependencies:
|
||||
'@redis/client': ^1.0.0
|
||||
dependencies:
|
||||
'@redis/client': 1.3.0
|
||||
dev: false
|
||||
|
||||
/@redis/json/1.0.4_@redis+client@1.3.0:
|
||||
resolution: {integrity: sha512-LUZE2Gdrhg0Rx7AN+cZkb1e6HjoSKaeeW8rYnt89Tly13GBI5eP4CwDVr+MY8BAYfCg4/N15OUrtLoona9uSgw==}
|
||||
peerDependencies:
|
||||
'@redis/client': ^1.0.0
|
||||
dependencies:
|
||||
'@redis/client': 1.3.0
|
||||
dev: false
|
||||
|
||||
/@redis/search/1.1.0_@redis+client@1.3.0:
|
||||
resolution: {integrity: sha512-NyFZEVnxIJEybpy+YskjgOJRNsfTYqaPbK/Buv6W2kmFNaRk85JiqjJZA5QkRmWvGbyQYwoO5QfDi2wHskKrQQ==}
|
||||
peerDependencies:
|
||||
'@redis/client': ^1.0.0
|
||||
dependencies:
|
||||
'@redis/client': 1.3.0
|
||||
dev: false
|
||||
|
||||
/@redis/time-series/1.0.3_@redis+client@1.3.0:
|
||||
resolution: {integrity: sha512-OFp0q4SGrTH0Mruf6oFsHGea58u8vS/iI5+NpYdicaM+7BgqBZH8FFvNZ8rYYLrUO/QRqMq72NpXmxLVNcdmjA==}
|
||||
peerDependencies:
|
||||
'@redis/client': ^1.0.0
|
||||
dependencies:
|
||||
'@redis/client': 1.3.0
|
||||
dev: false
|
||||
|
||||
/@rushstack/eslint-patch/1.0.8:
|
||||
resolution: {integrity: sha512-ZK5v4bJwgXldAUA8r3q9YKfCwOqoHTK/ZqRjSeRXQrBXWouoPnS4MQtgC4AXGiiBuUu5wxrRgTlv0ktmM4P1Aw==}
|
||||
dev: true
|
||||
|
@ -3676,12 +3727,6 @@ packages:
|
|||
'@types/range-parser': 1.2.4
|
||||
dev: false
|
||||
|
||||
/@types/express-session/1.17.4:
|
||||
resolution: {integrity: sha512-7cNlSI8+oOBUHTfPXMwDxF/Lchx5aJ3ho7+p9jJZYVg9dVDJFh3qdMXmJtRsysnvS+C6x46k9DRYmrmCkE+MVg==}
|
||||
dependencies:
|
||||
'@types/express': 4.17.13
|
||||
dev: true
|
||||
|
||||
/@types/express/4.17.13:
|
||||
resolution: {integrity: sha512-6bSZTPaTIACxn48l50SR+axgrqm6qXFIxrdAKaG6PaJk3+zuUr35hBlgT7vOmJcum+OEaIBLtHV/qloEAFITeA==}
|
||||
dependencies:
|
||||
|
@ -3764,6 +3809,12 @@ packages:
|
|||
'@types/node': 17.0.31
|
||||
dev: true
|
||||
|
||||
/@types/jsonwebtoken/8.5.9:
|
||||
resolution: {integrity: sha512-272FMnFGzAVMGtu9tkr29hRL6bZj4Zs1KZNeHLnKqAvp06tAIcarTMwOh8/8bz4FmKRcMxZhZNeUAQsNLoiPhg==}
|
||||
dependencies:
|
||||
'@types/node': 17.0.31
|
||||
dev: true
|
||||
|
||||
/@types/keyv/3.1.4:
|
||||
resolution: {integrity: sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg==}
|
||||
dependencies:
|
||||
|
@ -3891,13 +3942,6 @@ packages:
|
|||
'@types/mime': 1.3.2
|
||||
'@types/node': 17.0.31
|
||||
|
||||
/@types/session-file-store/1.2.2:
|
||||
resolution: {integrity: sha512-l9yZ+PQ8vaXhch03MrV+25BIbhKpeWfZB++3njPIm6lKeDGRS2qF2elLuVa4XrhfJbObqW0puhB3A6FCbkraZg==}
|
||||
dependencies:
|
||||
'@types/express': 4.17.13
|
||||
'@types/express-session': 1.17.4
|
||||
dev: true
|
||||
|
||||
/@types/stack-utils/2.0.1:
|
||||
resolution: {integrity: sha512-Hl219/BT5fLAaz6NDkSuhzasy49dwQS/DSdu4MdggFB8zcXv7vflBI3xp7FEmkmdDkBUI2bPUNeMttp2knYdxw==}
|
||||
dev: true
|
||||
|
@ -3910,6 +3954,10 @@ packages:
|
|||
resolution: {integrity: sha512-PBjIUxZHOuj0R15/xuwJYjFi+KZdNFrehocChv4g5hu6aFroHue8m0lBP0POdK2nKzbw0cgV1mws8+V/JAcEkQ==}
|
||||
dev: false
|
||||
|
||||
/@types/uuid/8.3.4:
|
||||
resolution: {integrity: sha512-c/I8ZRb51j+pYGAu5CrFMRxqZ2ke4y2grEBO5AUjgSkSk+qT2Ea+OdWElz/OiMf5MNpn2b17kuVBwZLQJXzihw==}
|
||||
dev: true
|
||||
|
||||
/@types/validator/13.7.2:
|
||||
resolution: {integrity: sha512-KFcchQ3h0OPQgFirBRPZr5F/sVjxZsOrQHedj3zi8AH3Zv/hOLx2OLR4hxR5HcfoU+33n69ZuOfzthKVdMoTiw==}
|
||||
dev: true
|
||||
|
@ -4647,15 +4695,6 @@ packages:
|
|||
resolution: {integrity: sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==}
|
||||
dev: true
|
||||
|
||||
/asn1.js/5.4.1:
|
||||
resolution: {integrity: sha512-+I//4cYPccV8LdmBLiX8CYvf9Sp3vQsrqu2QNXRcrbiWvcx/UdlFiqUJJzxRQxgsZmvhXhn4cSKeSmoFjVdupA==}
|
||||
dependencies:
|
||||
bn.js: 4.12.0
|
||||
inherits: 2.0.4
|
||||
minimalistic-assert: 1.0.1
|
||||
safer-buffer: 2.1.2
|
||||
dev: false
|
||||
|
||||
/ast-types-flow/0.0.7:
|
||||
resolution: {integrity: sha1-9wtzXGvKGlycItmCw+Oef+ujva0=}
|
||||
dev: true
|
||||
|
@ -4844,10 +4883,6 @@ packages:
|
|||
babel-preset-current-node-syntax: 1.0.1_@babel+core@7.17.10
|
||||
dev: true
|
||||
|
||||
/bagpipe/0.3.5:
|
||||
resolution: {integrity: sha512-42sAlmPDKes1nLm/aly+0VdaopSU9br+jkRELedhQxI5uXHgtk47I83Mpmf4zoNTRMASdLFtUkimlu/Z9zQ8+g==}
|
||||
dev: false
|
||||
|
||||
/bail/2.0.2:
|
||||
resolution: {integrity: sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==}
|
||||
dev: false
|
||||
|
@ -4871,10 +4906,6 @@ packages:
|
|||
readable-stream: 3.6.0
|
||||
dev: true
|
||||
|
||||
/bn.js/4.12.0:
|
||||
resolution: {integrity: sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==}
|
||||
dev: false
|
||||
|
||||
/body-parser/1.20.0:
|
||||
resolution: {integrity: sha512-DfJ+q6EPcGKZD1QWUjSpqp+Q7bDQTsQIF4zfUAtZ6qk+H/3/QRhg9CEp39ss+/T2vw0+HaidC0ecJj/DRLIaKg==}
|
||||
engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16}
|
||||
|
@ -4948,8 +4979,7 @@ packages:
|
|||
dev: true
|
||||
|
||||
/buffer-equal-constant-time/1.0.1:
|
||||
resolution: {integrity: sha1-+OcRMvf/5uAaXJaXpMbz5I1cyBk=}
|
||||
dev: true
|
||||
resolution: {integrity: sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==}
|
||||
|
||||
/buffer-from/1.1.2:
|
||||
resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==}
|
||||
|
@ -5268,6 +5298,11 @@ packages:
|
|||
engines: {node: '>=6'}
|
||||
dev: false
|
||||
|
||||
/cluster-key-slot/1.1.0:
|
||||
resolution: {integrity: sha512-2Nii8p3RwAPiFwsnZvukotvow2rIHM+yQ6ZcBXGHdniadkYGZYiGmkHJIbZPIV9nfv7m/U1IPMVVcAhoWFeklw==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
dev: false
|
||||
|
||||
/co/4.6.0:
|
||||
resolution: {integrity: sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==}
|
||||
engines: {iojs: '>= 1.0.0', node: '>= 0.12.0'}
|
||||
|
@ -5486,11 +5521,6 @@ packages:
|
|||
resolution: {integrity: sha1-4wOogrNCzD7oylE6eZmXNNqzriw=}
|
||||
dev: false
|
||||
|
||||
/cookie/0.4.2:
|
||||
resolution: {integrity: sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA==}
|
||||
engines: {node: '>= 0.6'}
|
||||
dev: false
|
||||
|
||||
/cookie/0.5.0:
|
||||
resolution: {integrity: sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==}
|
||||
engines: {node: '>= 0.6'}
|
||||
|
@ -5937,7 +5967,6 @@ packages:
|
|||
resolution: {integrity: sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==}
|
||||
dependencies:
|
||||
safe-buffer: 5.2.1
|
||||
dev: true
|
||||
|
||||
/ee-first/1.1.1:
|
||||
resolution: {integrity: sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=}
|
||||
|
@ -6236,7 +6265,7 @@ packages:
|
|||
eslint-import-resolver-webpack:
|
||||
optional: true
|
||||
dependencies:
|
||||
'@typescript-eslint/parser': 5.22.0_uhoeudlwl7kc47h4kncsfowede
|
||||
'@typescript-eslint/parser': 5.22.0_hcfsmds2fshutdssjqluwm76uu
|
||||
debug: 3.2.7
|
||||
eslint-import-resolver-node: 0.3.6
|
||||
find-up: 2.1.0
|
||||
|
@ -6643,22 +6672,6 @@ packages:
|
|||
jest-util: 28.1.0
|
||||
dev: true
|
||||
|
||||
/express-session/1.17.3:
|
||||
resolution: {integrity: sha512-4+otWXlShYlG1Ma+2Jnn+xgKUZTMJ5QD3YvfilX3AcocOAbIkVylSWEklzALe/+Pu4qV6TYBj5GwOBFfdKqLBw==}
|
||||
engines: {node: '>= 0.8.0'}
|
||||
dependencies:
|
||||
cookie: 0.4.2
|
||||
cookie-signature: 1.0.6
|
||||
debug: 2.6.9
|
||||
depd: 2.0.0
|
||||
on-headers: 1.0.2
|
||||
parseurl: 1.3.3
|
||||
safe-buffer: 5.2.1
|
||||
uid-safe: 2.1.5
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
dev: false
|
||||
|
||||
/express/4.18.1:
|
||||
resolution: {integrity: sha512-zZBcOX9TfehHQhtupq57OF8lFZ3UZi08Y97dwFCkD8p9d/d2Y3M+ykKcwaMDEL+4qyUolgBDX6AblpR3fL212Q==}
|
||||
engines: {node: '>= 0.10.0'}
|
||||
|
@ -6986,15 +6999,6 @@ packages:
|
|||
jsonfile: 6.1.0
|
||||
universalify: 2.0.0
|
||||
|
||||
/fs-extra/8.1.0:
|
||||
resolution: {integrity: sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==}
|
||||
engines: {node: '>=6 <7 || >=8'}
|
||||
dependencies:
|
||||
graceful-fs: 4.2.10
|
||||
jsonfile: 4.0.0
|
||||
universalify: 0.1.2
|
||||
dev: false
|
||||
|
||||
/fs-extra/9.1.0:
|
||||
resolution: {integrity: sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==}
|
||||
engines: {node: '>=10'}
|
||||
|
@ -7059,6 +7063,11 @@ packages:
|
|||
wide-align: 1.1.5
|
||||
dev: false
|
||||
|
||||
/generic-pool/3.8.2:
|
||||
resolution: {integrity: sha512-nGToKy6p3PAbYQ7p1UlWl6vSPwfwU6TMSWK7TTu+WUY4ZjyZQGniGGt2oNVvyNSpyZYSB43zMXVLcBm08MTMkg==}
|
||||
engines: {node: '>= 4'}
|
||||
dev: false
|
||||
|
||||
/gensync/1.0.0-beta.2:
|
||||
resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
|
@ -7557,6 +7566,7 @@ packages:
|
|||
/imurmurhash/0.1.4:
|
||||
resolution: {integrity: sha1-khi5srkoojixPcT7a21XbyMUU+o=}
|
||||
engines: {node: '>=0.8.19'}
|
||||
dev: true
|
||||
|
||||
/indent-string/3.2.0:
|
||||
resolution: {integrity: sha512-BYqTHXTGUIvg7t1r4sJNKcbDZkL92nkXA8YtRpbjFHRHGDL/NtUeiBJMeE60kIFN/Mg8ESaWQvftaYMGJzQZCQ==}
|
||||
|
@ -7894,6 +7904,7 @@ packages:
|
|||
|
||||
/is-typedarray/1.0.0:
|
||||
resolution: {integrity: sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==}
|
||||
dev: true
|
||||
|
||||
/is-unc-path/1.0.0:
|
||||
resolution: {integrity: sha512-mrGpVd0fs7WWLfVsStvgF6iEJnbjDFZh9/emhRDcGWTduTfNHd9CHeUwH3gYIjdbwo4On6hunkztwOaAw0yllQ==}
|
||||
|
@ -8601,12 +8612,6 @@ packages:
|
|||
hasBin: true
|
||||
dev: true
|
||||
|
||||
/jsonfile/4.0.0:
|
||||
resolution: {integrity: sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==}
|
||||
optionalDependencies:
|
||||
graceful-fs: 4.2.10
|
||||
dev: false
|
||||
|
||||
/jsonfile/6.1.0:
|
||||
resolution: {integrity: sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==}
|
||||
dependencies:
|
||||
|
@ -8637,7 +8642,6 @@ packages:
|
|||
lodash.once: 4.1.1
|
||||
ms: 2.1.3
|
||||
semver: 5.7.1
|
||||
dev: true
|
||||
|
||||
/jsx-ast-utils/3.3.0:
|
||||
resolution: {integrity: sha512-XzO9luP6L0xkxwhIJMTJQpZo/eeN60K08jHdexfD569AGxeNug6UketeHXEhROoM8aR7EcUoOQmIhcJQjcuq8Q==}
|
||||
|
@ -8653,14 +8657,12 @@ packages:
|
|||
buffer-equal-constant-time: 1.0.1
|
||||
ecdsa-sig-formatter: 1.0.11
|
||||
safe-buffer: 5.2.1
|
||||
dev: true
|
||||
|
||||
/jws/3.2.2:
|
||||
resolution: {integrity: sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==}
|
||||
dependencies:
|
||||
jwa: 1.4.1
|
||||
safe-buffer: 5.2.1
|
||||
dev: true
|
||||
|
||||
/keyv/3.1.0:
|
||||
resolution: {integrity: sha512-9ykJ/46SN/9KPM/sichzQ7OvXyGDYKGTaDlKMGCAlg2UK8KRy4jb0d8sFc+0Tt0YYnThq8X2RZgCg74RPxgcVA==}
|
||||
|
@ -8683,13 +8685,6 @@ packages:
|
|||
engines: {node: '>=6'}
|
||||
dev: false
|
||||
|
||||
/kruptein/2.2.3:
|
||||
resolution: {integrity: sha512-BTwprBPTzkFT9oTugxKd3WnWrX630MqUDsnmBuoa98eQs12oD4n4TeI0GbpdGcYn/73Xueg2rfnw+oK4dovnJg==}
|
||||
engines: {node: '>6'}
|
||||
dependencies:
|
||||
asn1.js: 5.4.1
|
||||
dev: false
|
||||
|
||||
/kuler/2.0.0:
|
||||
resolution: {integrity: sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A==}
|
||||
dev: false
|
||||
|
@ -8813,27 +8808,21 @@ packages:
|
|||
|
||||
/lodash.includes/4.3.0:
|
||||
resolution: {integrity: sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==}
|
||||
dev: true
|
||||
|
||||
/lodash.isboolean/3.0.3:
|
||||
resolution: {integrity: sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==}
|
||||
dev: true
|
||||
|
||||
/lodash.isinteger/4.0.4:
|
||||
resolution: {integrity: sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==}
|
||||
dev: true
|
||||
|
||||
/lodash.isnumber/3.0.3:
|
||||
resolution: {integrity: sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==}
|
||||
dev: true
|
||||
|
||||
/lodash.isplainobject/4.0.6:
|
||||
resolution: {integrity: sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==}
|
||||
dev: true
|
||||
|
||||
/lodash.isstring/4.0.1:
|
||||
resolution: {integrity: sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==}
|
||||
dev: true
|
||||
|
||||
/lodash.map/4.6.0:
|
||||
resolution: {integrity: sha512-worNHGKLDetmcEYDvh2stPCrrQRkP20E4l0iIS7F8EvzMqBBi7ltvFN5m1HvTf1P7Jk1txKhvFcmYsCr8O2F1Q==}
|
||||
|
@ -8853,7 +8842,6 @@ packages:
|
|||
|
||||
/lodash.once/4.1.1:
|
||||
resolution: {integrity: sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==}
|
||||
dev: true
|
||||
|
||||
/lodash.sortby/4.7.0:
|
||||
resolution: {integrity: sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA==}
|
||||
|
@ -9597,10 +9585,6 @@ packages:
|
|||
engines: {node: '>=4'}
|
||||
dev: true
|
||||
|
||||
/minimalistic-assert/1.0.1:
|
||||
resolution: {integrity: sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==}
|
||||
dev: false
|
||||
|
||||
/minimatch/3.1.2:
|
||||
resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==}
|
||||
dependencies:
|
||||
|
@ -9945,11 +9929,6 @@ packages:
|
|||
ee-first: 1.1.1
|
||||
dev: false
|
||||
|
||||
/on-headers/1.0.2:
|
||||
resolution: {integrity: sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==}
|
||||
engines: {node: '>= 0.8'}
|
||||
dev: false
|
||||
|
||||
/once/1.4.0:
|
||||
resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==}
|
||||
dependencies:
|
||||
|
@ -10522,11 +10501,6 @@ packages:
|
|||
engines: {node: '>=10'}
|
||||
dev: true
|
||||
|
||||
/random-bytes/1.0.0:
|
||||
resolution: {integrity: sha512-iv7LhNVO047HzYR3InF6pUcUsPQiHTM1Qal51DcGSuZFBil1aBBWG5eHPNek7bvILMaYJ/8RU1e8w1AMdHmLQQ==}
|
||||
engines: {node: '>= 0.8'}
|
||||
dev: false
|
||||
|
||||
/range-parser/1.2.1:
|
||||
resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==}
|
||||
engines: {node: '>= 0.6'}
|
||||
|
@ -10785,6 +10759,17 @@ packages:
|
|||
strip-indent: 3.0.0
|
||||
dev: true
|
||||
|
||||
/redis/4.3.1:
|
||||
resolution: {integrity: sha512-cM7yFU5CA6zyCF7N/+SSTcSJQSRMEKN0k0Whhu6J7n9mmXRoXugfWDBo5iOzGwABmsWKSwGPTU5J4Bxbl+0mrA==}
|
||||
dependencies:
|
||||
'@redis/bloom': 1.0.2_@redis+client@1.3.0
|
||||
'@redis/client': 1.3.0
|
||||
'@redis/graph': 1.0.1_@redis+client@1.3.0
|
||||
'@redis/json': 1.0.4_@redis+client@1.3.0
|
||||
'@redis/search': 1.1.0_@redis+client@1.3.0
|
||||
'@redis/time-series': 1.0.3_@redis+client@1.3.0
|
||||
dev: false
|
||||
|
||||
/reflect-metadata/0.1.13:
|
||||
resolution: {integrity: sha512-Ts1Y/anZELhSsjMcU605fU9RE4Oi3p5ORujwbIKXfWa+0Zxs510Qrmrce5/Jowq3cHSZSJqBjypxmHarc+vEWg==}
|
||||
dev: false
|
||||
|
@ -10980,11 +10965,6 @@ packages:
|
|||
signal-exit: 3.0.7
|
||||
dev: true
|
||||
|
||||
/retry/0.12.0:
|
||||
resolution: {integrity: sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==}
|
||||
engines: {node: '>= 4'}
|
||||
dev: false
|
||||
|
||||
/retry/0.13.1:
|
||||
resolution: {integrity: sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==}
|
||||
engines: {node: '>= 4'}
|
||||
|
@ -11070,7 +11050,6 @@ packages:
|
|||
/semver/5.7.1:
|
||||
resolution: {integrity: sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==}
|
||||
hasBin: true
|
||||
dev: true
|
||||
|
||||
/semver/6.3.0:
|
||||
resolution: {integrity: sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==}
|
||||
|
@ -11124,18 +11103,6 @@ packages:
|
|||
- supports-color
|
||||
dev: false
|
||||
|
||||
/session-file-store/1.5.0:
|
||||
resolution: {integrity: sha512-60IZaJNzyu2tIeHutkYE8RiXVx3KRvacOxfLr2Mj92SIsRIroDsH0IlUUR6fJAjoTW4RQISbaOApa2IZpIwFdQ==}
|
||||
engines: {node: '>= 6'}
|
||||
dependencies:
|
||||
bagpipe: 0.3.5
|
||||
fs-extra: 8.1.0
|
||||
kruptein: 2.2.3
|
||||
object-assign: 4.1.1
|
||||
retry: 0.12.0
|
||||
write-file-atomic: 3.0.3
|
||||
dev: false
|
||||
|
||||
/set-blocking/2.0.0:
|
||||
resolution: {integrity: sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==}
|
||||
|
||||
|
@ -11982,8 +11949,9 @@ packages:
|
|||
resolution: {integrity: sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==}
|
||||
dependencies:
|
||||
is-typedarray: 1.0.0
|
||||
dev: true
|
||||
|
||||
/typeorm/0.3.6_pg@8.7.3+ts-node@10.8.2:
|
||||
/typeorm/0.3.6_rymjtjxvmmxrsowl5wrmwxcyqa:
|
||||
resolution: {integrity: sha512-DRqgfqcelMiGgWSMbBmVoJNFN2nPNA3EeY2gC324ndr2DZoGRTb9ILtp2oGVGnlA+cu5zgQ6it5oqKFNkte7Aw==}
|
||||
engines: {node: '>= 12.9.0'}
|
||||
hasBin: true
|
||||
|
@ -12053,6 +12021,7 @@ packages:
|
|||
js-yaml: 4.1.0
|
||||
mkdirp: 1.0.4
|
||||
pg: 8.7.3
|
||||
redis: 4.3.1
|
||||
reflect-metadata: 0.1.13
|
||||
sha.js: 2.4.11
|
||||
ts-node: 10.8.2_uva6s4l7h33czpzezvop6ux5pe
|
||||
|
@ -12074,13 +12043,6 @@ packages:
|
|||
resolution: {integrity: sha512-qLK/Xe9E2uzmYI3qLeOmI0tEOt+TBBQyUIAh4aAgU05FVYzeZrKUdkAZfBNVGRaHVgV0TDkdEngJSw/SyQchkQ==}
|
||||
dev: true
|
||||
|
||||
/uid-safe/2.1.5:
|
||||
resolution: {integrity: sha512-KPHm4VL5dDXKz01UuEd88Df+KzynaohSL9fBh096KWAxSKZQDI2uBrVqtvRM4rwrIrRRKsdLNML/lnaaVSRioA==}
|
||||
engines: {node: '>= 0.8'}
|
||||
dependencies:
|
||||
random-bytes: 1.0.0
|
||||
dev: false
|
||||
|
||||
/unbox-primitive/1.0.2:
|
||||
resolution: {integrity: sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==}
|
||||
dependencies:
|
||||
|
@ -12192,11 +12154,6 @@ packages:
|
|||
unist-util-visit-parents: 5.1.0
|
||||
dev: false
|
||||
|
||||
/universalify/0.1.2:
|
||||
resolution: {integrity: sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==}
|
||||
engines: {node: '>= 4.0.0'}
|
||||
dev: false
|
||||
|
||||
/universalify/2.0.0:
|
||||
resolution: {integrity: sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==}
|
||||
engines: {node: '>= 10.0.0'}
|
||||
|
@ -12302,6 +12259,11 @@ packages:
|
|||
hasBin: true
|
||||
dev: false
|
||||
|
||||
/uuid/9.0.0:
|
||||
resolution: {integrity: sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg==}
|
||||
hasBin: true
|
||||
dev: false
|
||||
|
||||
/uvu/0.5.3:
|
||||
resolution: {integrity: sha512-brFwqA3FXzilmtnIyJ+CxdkInkY/i4ErvP7uV0DnUVxQcQ55reuHphorpF+tZoVHK2MniZ/VJzI7zJQoc9T9Yw==}
|
||||
engines: {node: '>=8'}
|
||||
|
@ -12351,7 +12313,7 @@ packages:
|
|||
engines: {node: '>=12'}
|
||||
|
||||
/vary/1.1.2:
|
||||
resolution: {integrity: sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=}
|
||||
resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==}
|
||||
engines: {node: '>= 0.8'}
|
||||
dev: false
|
||||
|
||||
|
@ -12524,6 +12486,7 @@ packages:
|
|||
is-typedarray: 1.0.0
|
||||
signal-exit: 3.0.7
|
||||
typedarray-to-buffer: 3.1.5
|
||||
dev: true
|
||||
|
||||
/write-file-atomic/4.0.1:
|
||||
resolution: {integrity: sha512-nSKUxgAbyioruk6hU87QzVbY279oYT6uiwgDoujth2ju4mJ+TZau7SQBhtbTmUyuNYTuXnSyRn66FV0+eCgcrQ==}
|
||||
|
|
Loading…
Add table
Reference in a new issue