feat: move from cookie base auth to jwt auth

test: mock redis
This commit is contained in:
Nicolas Meienberger 2022-10-20 23:10:50 +02:00
parent 5091bc4f6b
commit 86f29b5754
35 changed files with 505 additions and 282 deletions

1
.gitignore vendored
View file

@ -10,6 +10,7 @@ github.secrets
node_modules/
app-data/*
data/postgres
data/redis
!app-data/.gitkeep
repos/*
!repos/.gitkeep

View file

@ -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:

View file

@ -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"

View file

@ -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"

View file

@ -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');

View file

@ -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>

View file

@ -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,

View 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;

View file

@ -3,7 +3,6 @@ import { HttpLink } from '@apollo/client';
const httpLink = (url: string) => {
return new HttpLink({
uri: `${url}/graphql`,
credentials: 'include',
});
};

View file

@ -1,9 +1,11 @@
import errorLink from './errorLink';
import httpLink from './httpLink';
import authLink from './authLink';
const links = {
errorLink,
httpLink,
authLink,
};
export default links;

View file

@ -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 {

View file

@ -1,7 +1,5 @@
mutation Login($input: UsernamePasswordInput!) {
login(input: $input) {
user {
id
}
token
}
}

View file

@ -1,7 +1,5 @@
mutation Register($input: UsernamePasswordInput!) {
register(input: $input) {
user {
id
}
token
}
}

View file

@ -0,0 +1,5 @@
query RefreshToken {
refreshToken {
token
}
}

View file

@ -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) {
await localStorage.setItem('token', data.login.token);
}
await client.refetchQueries({ include: ['Me'] });
} catch (error) {
handleError(error);
} finally {

View 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);
},
};
}),
};

View file

@ -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",

View file

@ -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();

View file

@ -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,

View file

@ -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;

View file

@ -0,0 +1,9 @@
declare namespace Express {
interface Request {
session: {
userId?: number;
id?: string;
};
[key: string]: any;
}
}

View file

@ -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 };

View file

@ -1,13 +1,17 @@
import { faker } from '@faker-js/faker';
import jwt from 'jsonwebtoken';
import { DataSource } from 'typeorm';
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 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 +62,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 +96,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 +115,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 +146,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' },

View file

@ -1,15 +1,18 @@
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';
let db: DataSource | null = null;
const TEST_SUITE = 'authservice';
beforeAll(async () => {
setConfig('jwtSecret', 'test');
db = await setupConnection(TEST_SUITE);
});
@ -23,14 +26,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 +58,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 +106,15 @@ 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);
});
});

View file

@ -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,28 @@ 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> {
await AuthService.logout(req.session?.id);
req.session.userId = undefined;
req.session.id = undefined;
return true;
}
@ -47,4 +41,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;
}
}

View file

@ -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,35 @@ const me = async (userId?: number): Promise<User | null> => {
return user;
};
const logout = async (session?: string): Promise<boolean> => {
if (!session) return false;
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;

View file

@ -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 };

View file

@ -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 },

View file

@ -10,6 +10,7 @@ import EventDispatcher from '../../../core/config/EventDispatcher';
jest.mock('fs-extra');
jest.mock('axios');
jest.mock('redis');
beforeEach(async () => {
jest.resetModules();

View file

@ -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) {

View file

@ -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();

View file

@ -1,9 +1,6 @@
# Write your query or mutation here
mutation Login($input: UsernamePasswordInput!) {
login(input: $input) {
user {
id
username
}
token
}
}

View file

@ -1,9 +1,6 @@
# Write your query or mutation here
mutation Register($input: UsernamePasswordInput!) {
register(input: $input) {
user {
id
username
}
token
}
}

View file

@ -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
View file

@ -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==}