WIP: Postgres + Redis + Typeorm + Type-GraphQL
This commit is contained in:
parent
6715880c01
commit
16f3e3313d
19 changed files with 16543 additions and 163 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -3,6 +3,7 @@
|
|||
github.secrets
|
||||
node_modules/
|
||||
app-data/*
|
||||
data/
|
||||
traefik/ssl/*
|
||||
!traefik/ssl/.gitkeep
|
||||
!app-data/.gitkeep
|
||||
|
|
|
@ -45,7 +45,6 @@ services:
|
|||
ANONADDY_DOMAIN: ${ANONADDY_DOMAIN}
|
||||
ANONADDY_SECRET: ${ANONADDY_SECRET}
|
||||
ANONADDY_ADMIN_USERNAME: ${ANONADDY_USERNAME}
|
||||
POSTFIX_DEBUG: true
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
- tipi_main_network
|
||||
|
|
|
@ -1,11 +1,37 @@
|
|||
version: "3.7"
|
||||
|
||||
services:
|
||||
postgres:
|
||||
container_name: postgres
|
||||
image: postgres:latest
|
||||
restart: on-failure
|
||||
stop_grace_period: 1m
|
||||
volumes:
|
||||
- ./data/postgres:/var/lib/postgresql/data
|
||||
environment:
|
||||
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
|
||||
POSTGRES_USER: ${POSTGRES_USERNAME}
|
||||
POSTGRES_DB: ${POSTGRES_DBNAME}
|
||||
networks:
|
||||
- tipi_main_network
|
||||
|
||||
redis:
|
||||
container_name: redis
|
||||
image: redis:latest
|
||||
restart: on-failure
|
||||
volumes:
|
||||
- ./data/redis:/data
|
||||
networks:
|
||||
- tipi_main_network
|
||||
|
||||
api:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile.dev
|
||||
command: bash -c "cd /api && npm run dev"
|
||||
depends_on:
|
||||
- postgres
|
||||
- redis
|
||||
container_name: api
|
||||
ports:
|
||||
- 3001:3001
|
||||
|
@ -16,11 +42,14 @@ services:
|
|||
- ${PWD}/packages/system-api:/api
|
||||
- /api/node_modules
|
||||
environment:
|
||||
- INTERNAL_IP=${INTERNAL_IP}
|
||||
- TIPI_VERSION=${TIPI_VERSION}
|
||||
- JWT_SECRET=${JWT_SECRET}
|
||||
- ROOT_FOLDER_HOST=${ROOT_FOLDER_HOST}
|
||||
- NGINX_PORT=${NGINX_PORT}
|
||||
INTERNAL_IP: ${INTERNAL_IP}
|
||||
TIPI_VERSION: ${TIPI_VERSION}
|
||||
JWT_SECRET: ${JWT_SECRET}
|
||||
ROOT_FOLDER_HOST: ${ROOT_FOLDER_HOST}
|
||||
NGINX_PORT: ${NGINX_PORT}
|
||||
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
|
||||
POSTGRES_USER: ${POSTGRES_USERNAME}
|
||||
POSTGRES_DB: ${POSTGRES_DBNAME}
|
||||
networks:
|
||||
- tipi_main_network
|
||||
|
||||
|
|
3
packages/system-api/.gitignore
vendored
3
packages/system-api/.gitignore
vendored
|
@ -2,4 +2,5 @@ node_modules/
|
|||
dist/
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
coverage/
|
||||
logs/
|
||||
|
|
15168
packages/system-api/package-lock.json
generated
Normal file
15168
packages/system-api/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
|
@ -22,15 +22,21 @@
|
|||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@runtipi/common": "file:../common",
|
||||
"apollo-server-core": "^3.9.0",
|
||||
"apollo-server-express": "^3.9.0",
|
||||
"argon2": "^0.28.5",
|
||||
"axios": "^0.26.1",
|
||||
"class-validator": "^0.13.2",
|
||||
"compression": "^1.7.4",
|
||||
"connect-redis": "^6.1.3",
|
||||
"cookie-parser": "^1.4.6",
|
||||
"cors": "^2.8.5",
|
||||
"dotenv": "^16.0.0",
|
||||
"express": "^4.17.3",
|
||||
"graphql": "^16.5.0",
|
||||
"express-session": "^1.17.3",
|
||||
"graphql": "^15.3.0",
|
||||
"helmet": "^5.0.2",
|
||||
"http": "0.0.1-security",
|
||||
"internal-ip": "^6.0.0",
|
||||
"jsonwebtoken": "^8.5.1",
|
||||
"mock-fs": "^5.1.2",
|
||||
|
@ -40,16 +46,23 @@
|
|||
"passport": "^0.5.2",
|
||||
"passport-cookie": "^1.0.9",
|
||||
"passport-http-bearer": "^1.0.1",
|
||||
"pg": "^8.7.3",
|
||||
"public-ip": "^5.0.0",
|
||||
"redis": "^4.1.0",
|
||||
"reflect-metadata": "^0.1.13",
|
||||
"systeminformation": "^5.11.9",
|
||||
"tcp-port-used": "^1.0.2",
|
||||
"type-graphql": "^1.1.1"
|
||||
"type-graphql": "^1.1.1",
|
||||
"typeorm": "^0.3.6",
|
||||
"winston": "^3.7.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/compression": "^1.7.2",
|
||||
"@types/connect-redis": "^0.0.18",
|
||||
"@types/cookie-parser": "^1.4.3",
|
||||
"@types/cors": "^2.8.12",
|
||||
"@types/express": "^4.17.13",
|
||||
"@types/express-session": "^1.17.4",
|
||||
"@types/jest": "^27.5.0",
|
||||
"@types/jsonwebtoken": "^8.5.8",
|
||||
"@types/mock-fs": "^4.13.1",
|
||||
|
|
|
@ -1,6 +1,16 @@
|
|||
import * as dotenv from 'dotenv';
|
||||
import redis from 'redis';
|
||||
import { DataSourceOptions } from 'typeorm';
|
||||
import { __prod__ } from './constants/constants';
|
||||
|
||||
interface IConfig {
|
||||
logs: {
|
||||
LOGS_FOLDER: string;
|
||||
LOGS_APP: string;
|
||||
LOGS_ERROR: string;
|
||||
};
|
||||
typeorm: DataSourceOptions;
|
||||
redis: Parameters<typeof redis.createClient>[0];
|
||||
NODE_ENV: string;
|
||||
ROOT_FOLDER: string;
|
||||
JWT_SECRET: string;
|
||||
|
@ -11,9 +21,42 @@ interface IConfig {
|
|||
|
||||
dotenv.config();
|
||||
|
||||
const { NODE_ENV = 'development', JWT_SECRET = '', INTERNAL_IP = '', TIPI_VERSION = '', ROOT_FOLDER_HOST = '', NGINX_PORT = '80' } = process.env;
|
||||
const {
|
||||
LOGS_FOLDER = 'logs',
|
||||
LOGS_APP = 'app.log',
|
||||
LOGS_ERROR = 'error.log',
|
||||
NODE_ENV = 'development',
|
||||
JWT_SECRET = '',
|
||||
INTERNAL_IP = '',
|
||||
TIPI_VERSION = '',
|
||||
ROOT_FOLDER_HOST = '',
|
||||
NGINX_PORT = '80',
|
||||
POSTGRES_DB = '',
|
||||
POSTGRES_USER = '',
|
||||
POSTGRES_PASSWORD = '',
|
||||
} = process.env;
|
||||
|
||||
const config: IConfig = {
|
||||
logs: {
|
||||
LOGS_FOLDER,
|
||||
LOGS_APP,
|
||||
LOGS_ERROR,
|
||||
},
|
||||
typeorm: {
|
||||
type: 'postgres',
|
||||
host: 'postgres',
|
||||
database: POSTGRES_DB,
|
||||
username: POSTGRES_USER,
|
||||
password: POSTGRES_PASSWORD,
|
||||
port: 5432,
|
||||
logging: !__prod__,
|
||||
synchronize: true,
|
||||
entities: [],
|
||||
},
|
||||
redis: {
|
||||
url: 'redis://redis:6379',
|
||||
legacyMode: true,
|
||||
},
|
||||
NODE_ENV,
|
||||
ROOT_FOLDER: '/tipi',
|
||||
JWT_SECRET,
|
||||
|
|
6
packages/system-api/src/config/constants/constants.ts
Executable file
6
packages/system-api/src/config/constants/constants.ts
Executable file
|
@ -0,0 +1,6 @@
|
|||
/* eslint-disable @typescript-eslint/naming-convention */
|
||||
const __prod__ = process.env.NODE_ENV === 'production';
|
||||
|
||||
const COOKIE_MAX_AGE = 1000 * 60 * 60 * 24 * 365 * 10;
|
||||
|
||||
export { __prod__, COOKIE_MAX_AGE };
|
18
packages/system-api/src/config/logger/apollo.logger.ts
Normal file
18
packages/system-api/src/config/logger/apollo.logger.ts
Normal file
|
@ -0,0 +1,18 @@
|
|||
/* eslint-disable require-await */
|
||||
import { PluginDefinition } from 'apollo-server-core';
|
||||
import { __prod__ } from '../constants/constants';
|
||||
import logger from './logger';
|
||||
|
||||
const ApolloLogs: PluginDefinition = {
|
||||
requestDidStart: async () => {
|
||||
return {
|
||||
async didEncounterErrors(errors) {
|
||||
if (!__prod__) {
|
||||
logger.error(JSON.stringify(errors.errors));
|
||||
}
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
export { ApolloLogs };
|
62
packages/system-api/src/config/logger/logger.ts
Normal file
62
packages/system-api/src/config/logger/logger.ts
Normal file
|
@ -0,0 +1,62 @@
|
|||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { createLogger, format, transports } from 'winston';
|
||||
import config from '..';
|
||||
|
||||
const { align, printf, timestamp, combine, colorize } = format;
|
||||
|
||||
// Create the logs directory if it does not exist
|
||||
if (!fs.existsSync(config.logs.LOGS_FOLDER)) {
|
||||
fs.mkdirSync(config.logs.LOGS_FOLDER);
|
||||
}
|
||||
|
||||
/**
|
||||
* Production logger format
|
||||
*/
|
||||
const combinedLogFormat = combine(
|
||||
timestamp(),
|
||||
printf((info) => `${info.timestamp} > ${info.message}`),
|
||||
);
|
||||
|
||||
/**
|
||||
* Development logger format
|
||||
*/
|
||||
const combinedLogFormatDev = combine(
|
||||
colorize(),
|
||||
align(),
|
||||
printf((info) => `${info.level}: ${info.message}`),
|
||||
);
|
||||
|
||||
const Logger = createLogger({
|
||||
level: 'info',
|
||||
format: combinedLogFormat,
|
||||
transports: [
|
||||
//
|
||||
// - Write to all logs with level `info` and below to `app.log`
|
||||
// - Write all logs error (and below) to `error.log`.
|
||||
//
|
||||
new transports.File({
|
||||
filename: path.join(config.logs.LOGS_FOLDER, config.logs.LOGS_ERROR),
|
||||
level: 'error',
|
||||
}),
|
||||
new transports.File({
|
||||
filename: path.join(config.logs.LOGS_FOLDER, config.logs.LOGS_APP),
|
||||
}),
|
||||
],
|
||||
exceptionHandlers: [new transports.File({ filename: path.join(config.logs.LOGS_FOLDER, config.logs.LOGS_ERROR) })],
|
||||
});
|
||||
|
||||
//
|
||||
// If we're not in production then log to the `console
|
||||
//
|
||||
const LoggerDev = createLogger({
|
||||
level: 'debug',
|
||||
format: combinedLogFormatDev,
|
||||
transports: [
|
||||
new transports.Console({
|
||||
level: 'debug',
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
export default config.NODE_ENV === 'production' ? Logger : LoggerDev;
|
13
packages/system-api/src/core/middlewares/authChecker.ts
Normal file
13
packages/system-api/src/core/middlewares/authChecker.ts
Normal file
|
@ -0,0 +1,13 @@
|
|||
import { AuthChecker } from 'type-graphql';
|
||||
import { MyContext } from '../../types';
|
||||
|
||||
export const customAuthChecker: AuthChecker<MyContext> = ({ context }) => {
|
||||
// here we can read the user from context
|
||||
// and check his permission in the db against the `roles` argument
|
||||
// that comes from the `@Authorized` decorator, eg. ["ADMIN", "MODERATOR"]
|
||||
if (!context.req?.session?.userId) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
|
@ -0,0 +1,24 @@
|
|||
import connectRedis from 'connect-redis';
|
||||
import session from 'express-session';
|
||||
import { createClient } from 'redis';
|
||||
import config from '../../config';
|
||||
import { COOKIE_MAX_AGE, __prod__ } from '../../config/constants/constants';
|
||||
|
||||
const getSessionMiddleware = async (): Promise<any> => {
|
||||
const RedisStore = connectRedis(session);
|
||||
|
||||
const redisClient = createClient(config.redis);
|
||||
|
||||
await redisClient.connect();
|
||||
|
||||
return session({
|
||||
name: 'qid',
|
||||
store: new RedisStore({ client: redisClient as any, disableTouch: true }),
|
||||
cookie: { maxAge: COOKIE_MAX_AGE, secure: __prod__, sameSite: 'lax', httpOnly: true },
|
||||
secret: config.JWT_SECRET,
|
||||
resave: false,
|
||||
saveUninitialized: false,
|
||||
});
|
||||
};
|
||||
|
||||
export default getSessionMiddleware;
|
29
packages/system-api/src/modules/apps/app.entity.ts
Normal file
29
packages/system-api/src/modules/apps/app.entity.ts
Normal file
|
@ -0,0 +1,29 @@
|
|||
import { AppStatusEnum } from '@runtipi/common';
|
||||
import { Field, ObjectType } from 'type-graphql';
|
||||
import { BaseEntity, Column, CreateDateColumn, Entity, UpdateDateColumn } from 'typeorm';
|
||||
|
||||
@ObjectType()
|
||||
@Entity()
|
||||
class App extends BaseEntity {
|
||||
@Field(() => String)
|
||||
@Column({ primary: true, unique: true })
|
||||
id!: string;
|
||||
|
||||
@Field(() => Boolean)
|
||||
@Column({ type: 'boolean', default: false })
|
||||
installed!: boolean;
|
||||
|
||||
@Field(() => AppStatusEnum)
|
||||
@Column({ type: 'enum', enum: AppStatusEnum, default: AppStatusEnum.STOPPED, nullable: false })
|
||||
status!: AppStatusEnum;
|
||||
|
||||
@Field(() => Date)
|
||||
@CreateDateColumn()
|
||||
createdAt!: Date;
|
||||
|
||||
@Field(() => Date)
|
||||
@UpdateDateColumn()
|
||||
updatedAt!: Date;
|
||||
}
|
||||
|
||||
export default App;
|
|
@ -1,29 +1,29 @@
|
|||
import { AppCategoriesEnum, AppStatusEnum, FieldTypes } from '@runtipi/common';
|
||||
import { AppCategoriesEnum, AppStatusEnum } from '@runtipi/common';
|
||||
import { Field, ObjectType } from 'type-graphql';
|
||||
|
||||
@ObjectType()
|
||||
class FormField {
|
||||
@Field()
|
||||
type!: FieldTypes;
|
||||
// @ObjectType(() => FormField)
|
||||
// class FormField {
|
||||
// @Field()
|
||||
// type!: FieldTypes;
|
||||
|
||||
@Field()
|
||||
label!: string;
|
||||
// @Field()
|
||||
// label!: string;
|
||||
|
||||
@Field({ nullable: true })
|
||||
max?: number;
|
||||
// @Field({ nullable: true })
|
||||
// max?: number;
|
||||
|
||||
@Field({ nullable: true })
|
||||
min?: number;
|
||||
// @Field({ nullable: true })
|
||||
// min?: number;
|
||||
|
||||
@Field({ nullable: true })
|
||||
hint?: string;
|
||||
// @Field({ nullable: true })
|
||||
// hint?: string;
|
||||
|
||||
@Field({ nullable: true })
|
||||
required?: boolean;
|
||||
// @Field({ nullable: true })
|
||||
// required?: boolean;
|
||||
|
||||
@Field()
|
||||
env_variable!: string;
|
||||
}
|
||||
// @Field()
|
||||
// env_variable!: string;
|
||||
// }
|
||||
|
||||
@ObjectType()
|
||||
class App {
|
||||
|
@ -69,8 +69,8 @@ class App {
|
|||
@Field(() => String, { nullable: true })
|
||||
url_suffix?: string;
|
||||
|
||||
@Field(() => [FormField])
|
||||
form_fields?: FormField[];
|
||||
// @Field(() => [FormField])
|
||||
// form_fields?: FormField[];
|
||||
}
|
||||
|
||||
@ObjectType()
|
||||
|
|
|
@ -1,11 +1,13 @@
|
|||
import { GraphQLSchema } from 'graphql';
|
||||
import { buildSchema } from 'type-graphql';
|
||||
import AppsResolver from './modules/apps/apps.resolver.ts';
|
||||
import { customAuthChecker } from './core/middlewares/authChecker';
|
||||
import AppsResolver from './modules/apps/apps.resolver';
|
||||
|
||||
const createSchema = (): Promise<GraphQLSchema> =>
|
||||
buildSchema({
|
||||
resolvers: [AppsResolver],
|
||||
validate: true,
|
||||
authChecker: customAuthChecker,
|
||||
});
|
||||
|
||||
export { createSchema };
|
||||
|
|
|
@ -1,73 +1,44 @@
|
|||
/* eslint-disable no-unused-vars */
|
||||
import express, { NextFunction, Request, Response } from 'express';
|
||||
import compression from 'compression';
|
||||
import helmet from 'helmet';
|
||||
import cors from 'cors';
|
||||
import { isProd } from './constants/constants';
|
||||
import appsRoutes from './modules/apps/apps.routes';
|
||||
import systemRoutes from './modules/system/system.routes';
|
||||
import authRoutes from './modules/auth/auth.routes';
|
||||
import AuthHelpers from './modules/auth/auth.helpers';
|
||||
import cookieParser from 'cookie-parser';
|
||||
import 'reflect-metadata';
|
||||
import express from 'express';
|
||||
import { ApolloServerPluginLandingPageGraphQLPlayground as Playground } from 'apollo-server-core';
|
||||
import config from './config';
|
||||
import { DataSource } from 'typeorm';
|
||||
import { ApolloServer } from 'apollo-server-express';
|
||||
import { createSchema } from './schema';
|
||||
import { ApolloLogs } from './config/logger/apollo.logger';
|
||||
import { createServer } from 'http';
|
||||
import logger from './config/logger/logger';
|
||||
import getSessionMiddleware from './core/middlewares/sessionMiddleware';
|
||||
|
||||
const app = express();
|
||||
const port = 3001;
|
||||
const main = async () => {
|
||||
try {
|
||||
const app = express();
|
||||
const port = 3001;
|
||||
|
||||
app.use(express.json());
|
||||
app.use(cookieParser());
|
||||
const sessionMiddleware = await getSessionMiddleware();
|
||||
app.use(sessionMiddleware);
|
||||
|
||||
if (isProd) {
|
||||
app.use(compression());
|
||||
app.use(helmet());
|
||||
}
|
||||
const AppDataSource = new DataSource(config.typeorm);
|
||||
await AppDataSource.initialize();
|
||||
|
||||
app.use(
|
||||
cors({
|
||||
credentials: true,
|
||||
origin: function (origin, callback) {
|
||||
// allow requests with no origin
|
||||
if (!origin) return callback(null, true);
|
||||
const schema = await createSchema();
|
||||
|
||||
if (config.CLIENT_URLS.indexOf(origin) === -1) {
|
||||
const message = "The CORS policy for this origin doesn't allow access from the particular origin.";
|
||||
return callback(new Error(message), false);
|
||||
}
|
||||
const httpServer = createServer(app);
|
||||
|
||||
return callback(null, true);
|
||||
},
|
||||
}),
|
||||
);
|
||||
const apolloServer = new ApolloServer({
|
||||
schema,
|
||||
plugins: [Playground({ settings: { 'request.credentials': 'include' } }), ApolloLogs],
|
||||
});
|
||||
await apolloServer.start();
|
||||
apolloServer.applyMiddleware({ app });
|
||||
|
||||
// Get user from token
|
||||
app.use((req, _res, next) => {
|
||||
let user = null;
|
||||
|
||||
if (req?.cookies?.tipi_token) {
|
||||
user = AuthHelpers.tradeTokenForUser(req.cookies.tipi_token);
|
||||
if (user) req.user = user;
|
||||
}
|
||||
|
||||
next();
|
||||
});
|
||||
|
||||
const restrict = (req: Request, res: Response, next: NextFunction) => {
|
||||
if (!req.user) {
|
||||
res.status(401).json({ error: 'Unauthorized' });
|
||||
} else {
|
||||
next();
|
||||
httpServer.listen(port, () => {
|
||||
logger.info(`Server running on port ${port}`);
|
||||
});
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
logger.error(error);
|
||||
}
|
||||
};
|
||||
|
||||
app.use('/auth', authRoutes);
|
||||
app.use('/system', restrict, systemRoutes);
|
||||
app.use('/apps', restrict, appsRoutes);
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
app.use((err: Error, _req: Request, res: Response, _: NextFunction) => {
|
||||
res.status(200).json({ error: err.message });
|
||||
});
|
||||
|
||||
app.listen(port, () => {
|
||||
console.log(`System API listening on port ${port}`);
|
||||
});
|
||||
main();
|
||||
|
|
13
packages/system-api/src/types.ts
Normal file
13
packages/system-api/src/types.ts
Normal file
|
@ -0,0 +1,13 @@
|
|||
import { Request, Response } from 'express';
|
||||
import 'express-session';
|
||||
|
||||
declare module 'express-session' {
|
||||
interface SessionData {
|
||||
userId: number;
|
||||
}
|
||||
}
|
||||
|
||||
export type MyContext = {
|
||||
req: Request;
|
||||
res: Response;
|
||||
};
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"target": "es6",
|
||||
"lib": ["dom", "dom.iterable", "esnext"],
|
||||
"target": "es2018",
|
||||
"lib": ["es2018", "esnext.asynciterable"],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
|
@ -14,6 +14,7 @@
|
|||
"isolatedModules": false,
|
||||
"jsx": "preserve",
|
||||
"incremental": true,
|
||||
"emitDecoratorMetadata": true,
|
||||
"experimentalDecorators": true
|
||||
},
|
||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", "**/*.cjs", "jest.config.cjs"],
|
||||
|
|
1123
pnpm-lock.yaml
generated
1123
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load diff
Loading…
Add table
Reference in a new issue