WIP - App Service with GraphQL
This commit is contained in:
parent
ac712013da
commit
ce615a40f1
22 changed files with 292 additions and 15442 deletions
|
@ -1,13 +1,15 @@
|
|||
version: "3.7"
|
||||
|
||||
services:
|
||||
postgres:
|
||||
container_name: postgres
|
||||
tipi-db:
|
||||
container_name: tipi-db
|
||||
image: postgres:latest
|
||||
restart: on-failure
|
||||
stop_grace_period: 1m
|
||||
volumes:
|
||||
- ./data/postgres:/var/lib/postgresql/data
|
||||
ports:
|
||||
- 5432:5432
|
||||
environment:
|
||||
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
|
||||
POSTGRES_USER: ${POSTGRES_USERNAME}
|
||||
|
@ -21,7 +23,7 @@ services:
|
|||
dockerfile: Dockerfile.dev
|
||||
command: bash -c "cd /api && npm run dev"
|
||||
depends_on:
|
||||
- postgres
|
||||
- tipi-db
|
||||
container_name: api
|
||||
ports:
|
||||
- 3001:3001
|
||||
|
|
1
packages/system-api/.gitignore
vendored
1
packages/system-api/.gitignore
vendored
|
@ -4,3 +4,4 @@ dist/
|
|||
# testing
|
||||
coverage/
|
||||
logs/
|
||||
sessions/
|
||||
|
|
15298
packages/system-api/package-lock.json
generated
15298
packages/system-api/package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
@ -34,6 +34,7 @@
|
|||
"express": "^4.17.3",
|
||||
"express-session": "^1.17.3",
|
||||
"graphql": "^15.3.0",
|
||||
"graphql-type-json": "^0.3.2",
|
||||
"helmet": "^5.0.2",
|
||||
"http": "0.0.1-security",
|
||||
"internal-ip": "^6.0.0",
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import * as dotenv from 'dotenv';
|
||||
import { DataSourceOptions } from 'typeorm';
|
||||
import App from '../modules/apps/app.entity';
|
||||
import User from '../modules/auth/user.entity';
|
||||
import { __prod__ } from './constants/constants';
|
||||
|
||||
interface IConfig {
|
||||
|
@ -43,14 +44,14 @@ const config: IConfig = {
|
|||
},
|
||||
typeorm: {
|
||||
type: 'postgres',
|
||||
host: 'postgres',
|
||||
host: 'tipi-db',
|
||||
database: POSTGRES_DB,
|
||||
username: POSTGRES_USER,
|
||||
password: POSTGRES_PASSWORD,
|
||||
port: 5432,
|
||||
logging: !__prod__,
|
||||
synchronize: true,
|
||||
entities: [App],
|
||||
entities: [App, User],
|
||||
},
|
||||
NODE_ENV,
|
||||
ROOT_FOLDER: '/tipi',
|
||||
|
|
21
packages/system-api/src/helpers/error.types.ts
Normal file
21
packages/system-api/src/helpers/error.types.ts
Normal file
|
@ -0,0 +1,21 @@
|
|||
import { ObjectType, Field } from 'type-graphql';
|
||||
|
||||
@ObjectType()
|
||||
class FieldError {
|
||||
@Field()
|
||||
code!: number;
|
||||
|
||||
@Field()
|
||||
message!: string;
|
||||
|
||||
@Field({ nullable: true })
|
||||
field?: string;
|
||||
}
|
||||
|
||||
@ObjectType()
|
||||
class ErrorResponse {
|
||||
@Field(() => [FieldError], { nullable: true })
|
||||
errors?: FieldError[];
|
||||
}
|
||||
|
||||
export { FieldError, ErrorResponse };
|
|
@ -9,16 +9,12 @@ class App extends BaseEntity {
|
|||
@Column({ type: 'varchar', primary: true, unique: true })
|
||||
id!: string;
|
||||
|
||||
@Field(() => Boolean)
|
||||
@Column({ type: 'boolean', default: false })
|
||||
installed!: boolean;
|
||||
|
||||
@Field(() => String)
|
||||
@Column({ type: 'enum', enum: AppStatusEnum, default: AppStatusEnum.STOPPED, nullable: false })
|
||||
status!: AppStatusEnum;
|
||||
|
||||
@Field(() => Date)
|
||||
@Column({ type: 'date', nullable: true })
|
||||
@Column({ type: 'timestamptz', nullable: true, default: () => 'CURRENT_TIMESTAMP' })
|
||||
lastOpened!: Date;
|
||||
|
||||
@Field(() => Number)
|
||||
|
|
|
@ -40,11 +40,11 @@ export const checkEnvFile = (appName: string) => {
|
|||
const configFile: AppConfig = readJsonFile(`/apps/${appName}/config.json`);
|
||||
const envMap = getEnvMap(appName);
|
||||
|
||||
Object.keys(configFile.form_fields).forEach((key) => {
|
||||
const envVar = configFile.form_fields[key].env_variable;
|
||||
configFile.form_fields.forEach((field) => {
|
||||
const envVar = field.env_variable;
|
||||
const envVarValue = envMap.get(envVar);
|
||||
|
||||
if (!envVarValue && configFile.form_fields[key].required) {
|
||||
if (!envVarValue && field.required) {
|
||||
throw new Error('New info needed. App config needs to be updated');
|
||||
}
|
||||
});
|
||||
|
@ -109,14 +109,14 @@ export const generateEnvFile = (appName: string, form: Record<string, string>) =
|
|||
const baseEnvFile = readFile('/.env').toString();
|
||||
let envFile = `${baseEnvFile}\nAPP_PORT=${configFile.port}\n`;
|
||||
|
||||
Object.keys(configFile.form_fields).forEach((key) => {
|
||||
const value = form[key];
|
||||
configFile.form_fields.forEach((field) => {
|
||||
const formValue = form[field.env_variable];
|
||||
|
||||
if (value) {
|
||||
const envVar = configFile.form_fields[key].env_variable;
|
||||
envFile += `${envVar}=${value}\n`;
|
||||
} else if (configFile.form_fields[key].required) {
|
||||
throw new Error(`Variable ${key} is required`);
|
||||
if (formValue) {
|
||||
const envVar = field.env_variable;
|
||||
envFile += `${envVar}=${formValue}\n`;
|
||||
} else if (field.required) {
|
||||
throw new Error(`Variable ${field.env_variable} is required`);
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { Arg, Authorized, Query, Resolver } from 'type-graphql';
|
||||
import { Arg, Authorized, Mutation, Query, Resolver } from 'type-graphql';
|
||||
import AppsService from './apps.service';
|
||||
import { AppConfig, ListAppsResonse } from './apps.types';
|
||||
import { AppConfig, AppInputType, ListAppsResonse } from './apps.types';
|
||||
import App from './app.entity';
|
||||
|
||||
@Resolver()
|
||||
|
@ -20,4 +20,38 @@ export default class AppsResolver {
|
|||
async installedApps(): Promise<App[]> {
|
||||
return App.find();
|
||||
}
|
||||
|
||||
@Authorized()
|
||||
@Mutation(() => App)
|
||||
async installApp(@Arg('input', () => AppInputType) input: AppInputType): Promise<App> {
|
||||
const { id, form } = input;
|
||||
|
||||
return AppsService.installApp(id, form);
|
||||
}
|
||||
|
||||
@Authorized()
|
||||
@Mutation(() => App)
|
||||
async startApp(@Arg('id', () => String) id: string): Promise<App> {
|
||||
return AppsService.startApp(id);
|
||||
}
|
||||
|
||||
@Authorized()
|
||||
@Mutation(() => App)
|
||||
async stopApp(@Arg('id', () => String) id: string): Promise<App> {
|
||||
return AppsService.stopApp(id);
|
||||
}
|
||||
|
||||
@Authorized()
|
||||
@Mutation(() => App)
|
||||
async uninstallApp(@Arg('id', () => String) id: string): Promise<boolean> {
|
||||
return AppsService.uninstallApp(id);
|
||||
}
|
||||
|
||||
@Authorized()
|
||||
@Mutation(() => App)
|
||||
async updateAppConfig(@Arg('input', () => AppInputType) input: AppInputType): Promise<App> {
|
||||
const { id, form } = input;
|
||||
|
||||
return AppsService.updateAppConfig(id, form);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,27 +1,37 @@
|
|||
import si from 'systeminformation';
|
||||
import { AppStatusEnum } from '@runtipi/common';
|
||||
import { createFolder, fileExists, readFile, readJsonFile } from '../fs/fs.helpers';
|
||||
import { checkAppExists, checkAppRequirements, checkEnvFile, ensureAppState, generateEnvFile, getAvailableApps, getInitalFormValues, getStateFile, runAppScript } from './apps.helpers';
|
||||
import { createFolder, readFile, readJsonFile } from '../fs/fs.helpers';
|
||||
import { checkAppRequirements, checkEnvFile, generateEnvFile, getAvailableApps, getInitalFormValues, getStateFile, runAppScript } from './apps.helpers';
|
||||
import { AppConfig, ListAppsResonse } from './apps.types';
|
||||
import App from './app.entity';
|
||||
|
||||
const startApp = async (appName: string): Promise<App> => {
|
||||
let app = await App.findOne({ where: { id: appName } });
|
||||
|
||||
if (!app) {
|
||||
throw new Error(`App ${appName} not found`);
|
||||
}
|
||||
|
||||
const startApp = async (appName: string): Promise<void> => {
|
||||
checkAppExists(appName);
|
||||
checkEnvFile(appName);
|
||||
|
||||
// Regenerate env file
|
||||
const form = getInitalFormValues(appName);
|
||||
generateEnvFile(appName, form);
|
||||
|
||||
await App.update({ id: appName }, { status: AppStatusEnum.STARTING });
|
||||
// Run script
|
||||
await runAppScript(['start', appName]);
|
||||
await App.update({ id: appName }, { status: AppStatusEnum.RUNNING });
|
||||
|
||||
ensureAppState(appName, true);
|
||||
app = (await App.findOne({ where: { id: appName } })) as App;
|
||||
|
||||
return app;
|
||||
};
|
||||
|
||||
const installApp = async (id: string, form: Record<string, string>): Promise<void> => {
|
||||
const appExists = fileExists(`/app-data/${id}`);
|
||||
const installApp = async (id: string, form: Record<string, string>): Promise<App> => {
|
||||
let app = await App.findOne({ where: { id } });
|
||||
|
||||
if (appExists) {
|
||||
if (app) {
|
||||
await startApp(id);
|
||||
} else {
|
||||
const appIsValid = await checkAppRequirements(id);
|
||||
|
@ -35,13 +45,16 @@ const installApp = async (id: string, form: Record<string, string>): Promise<voi
|
|||
|
||||
// Create env file
|
||||
generateEnvFile(id, form);
|
||||
ensureAppState(id, true);
|
||||
|
||||
await App.create({ id, status: AppStatusEnum.INSTALLING }).save();
|
||||
|
||||
// Run script
|
||||
await runAppScript(['install', id]);
|
||||
}
|
||||
|
||||
return Promise.resolve();
|
||||
await App.update({ id }, { status: AppStatusEnum.RUNNING });
|
||||
app = (await App.findOne({ where: { id } })) as App;
|
||||
return app;
|
||||
};
|
||||
|
||||
const listApps = async (): Promise<ListAppsResonse> => {
|
||||
|
@ -82,23 +95,45 @@ const getAppInfo = async (id: string): Promise<AppConfig> => {
|
|||
return configFile;
|
||||
};
|
||||
|
||||
const updateAppConfig = async (id: string, form: Record<string, string>): Promise<void> => {
|
||||
checkAppExists(id);
|
||||
const updateAppConfig = async (id: string, form: Record<string, string>): Promise<App> => {
|
||||
const app = await App.findOne({ where: { id } });
|
||||
|
||||
if (!app) {
|
||||
throw new Error(`App ${id} not found`);
|
||||
}
|
||||
|
||||
generateEnvFile(id, form);
|
||||
|
||||
return app;
|
||||
};
|
||||
|
||||
const stopApp = async (id: string): Promise<void> => {
|
||||
checkAppExists(id);
|
||||
const stopApp = async (id: string): Promise<App> => {
|
||||
let app = await App.findOne({ where: { id } });
|
||||
// Run script
|
||||
await App.update({ id }, { status: AppStatusEnum.STOPPING });
|
||||
await runAppScript(['stop', id]);
|
||||
await App.update({ id }, { status: AppStatusEnum.STOPPED });
|
||||
app = (await App.findOne({ where: { id } })) as App;
|
||||
|
||||
return app;
|
||||
};
|
||||
|
||||
const uninstallApp = async (id: string): Promise<void> => {
|
||||
checkAppExists(id);
|
||||
ensureAppState(id, false);
|
||||
const uninstallApp = async (id: string): Promise<boolean> => {
|
||||
let app = await App.findOne({ where: { id } });
|
||||
|
||||
if (!app) {
|
||||
throw new Error(`App ${id} not found`);
|
||||
}
|
||||
if (app.status === AppStatusEnum.RUNNING) {
|
||||
await stopApp(id);
|
||||
}
|
||||
|
||||
await App.update({ id }, { status: AppStatusEnum.UNINSTALLING });
|
||||
// Run script
|
||||
await runAppScript(['uninstall', id]);
|
||||
await App.delete({ id });
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
export default { installApp, startApp, listApps, getAppInfo, updateAppConfig, stopApp, uninstallApp };
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import { AppCategoriesEnum, AppStatusEnum, FieldTypes } from '@runtipi/common';
|
||||
import { Field, ObjectType } from 'type-graphql';
|
||||
import { Field, InputType, ObjectType } from 'type-graphql';
|
||||
import { GraphQLJSONObject } from 'graphql-type-json';
|
||||
|
||||
@ObjectType()
|
||||
class FormField {
|
||||
|
@ -82,4 +83,13 @@ class ListAppsResonse {
|
|||
total!: number;
|
||||
}
|
||||
|
||||
export { ListAppsResonse, AppConfig };
|
||||
@InputType()
|
||||
class AppInputType {
|
||||
@Field(() => String)
|
||||
id!: string;
|
||||
|
||||
@Field(() => GraphQLJSONObject)
|
||||
form!: Record<string, string>;
|
||||
}
|
||||
|
||||
export { ListAppsResonse, AppConfig, AppInputType };
|
||||
|
|
|
@ -1,70 +0,0 @@
|
|||
import { NextFunction, Request, Response } from 'express';
|
||||
import { IUser } from '../../config/types';
|
||||
import { readJsonFile } from '../fs/fs.helpers';
|
||||
import AuthService from './auth.service';
|
||||
|
||||
const login = async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const { email, password } = req.body;
|
||||
|
||||
if (!email || !password) {
|
||||
throw new Error('Missing id or password');
|
||||
}
|
||||
|
||||
const token = await AuthService.login(email, password);
|
||||
|
||||
res.cookie('tipi_token', token, {
|
||||
httpOnly: false,
|
||||
secure: false,
|
||||
maxAge: 1000 * 60 * 60 * 24 * 7,
|
||||
});
|
||||
|
||||
res.status(200).json({ token });
|
||||
} catch (e) {
|
||||
next(e);
|
||||
}
|
||||
};
|
||||
|
||||
const register = async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const { email, password, name } = req.body;
|
||||
|
||||
const token = await AuthService.register(email, password, name);
|
||||
|
||||
res.cookie('tipi_token', token, {
|
||||
httpOnly: false,
|
||||
secure: false,
|
||||
maxAge: 1000 * 60 * 60 * 24 * 7,
|
||||
});
|
||||
|
||||
res.status(200).json({ token });
|
||||
} catch (e) {
|
||||
next(e);
|
||||
}
|
||||
};
|
||||
|
||||
const me = async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const { user } = req;
|
||||
|
||||
if (user) {
|
||||
res.status(200).json({ user });
|
||||
} else {
|
||||
res.status(200).json({ user: null });
|
||||
}
|
||||
} catch (e) {
|
||||
next(e);
|
||||
}
|
||||
};
|
||||
|
||||
const isConfigured = async (_req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const users: IUser[] = readJsonFile('/state/users.json');
|
||||
|
||||
res.status(200).json({ configured: users.length > 0 });
|
||||
} catch (e) {
|
||||
next(e);
|
||||
}
|
||||
};
|
||||
|
||||
export default { login, me, register, isConfigured };
|
|
@ -3,6 +3,7 @@ import * as argon2 from 'argon2';
|
|||
import { IUser, Maybe } from '../../config/types';
|
||||
import { readJsonFile } from '../fs/fs.helpers';
|
||||
import config from '../../config';
|
||||
import User from './user.entity';
|
||||
|
||||
const getUser = (email: string): Maybe<IUser> => {
|
||||
const savedUser: IUser[] = readJsonFile('/state/users.json');
|
||||
|
@ -14,12 +15,12 @@ const compareHashPassword = (password: string, hash = ''): Promise<boolean> => {
|
|||
return argon2.verify(hash, password);
|
||||
};
|
||||
|
||||
const getJwtToken = async (user: IUser, password: string) => {
|
||||
const getJwtToken = async (user: User, password: string) => {
|
||||
const validPassword = await compareHashPassword(password, user.password);
|
||||
|
||||
if (validPassword) {
|
||||
if (config.JWT_SECRET) {
|
||||
return jsonwebtoken.sign({ email: user.email }, config.JWT_SECRET, {
|
||||
return jsonwebtoken.sign({ email: user.username }, config.JWT_SECRET, {
|
||||
expiresIn: '7d',
|
||||
});
|
||||
} else {
|
||||
|
|
53
packages/system-api/src/modules/auth/auth.resolver.ts
Normal file
53
packages/system-api/src/modules/auth/auth.resolver.ts
Normal file
|
@ -0,0 +1,53 @@
|
|||
import { Arg, Authorized, Ctx, Mutation, Query, Resolver } from 'type-graphql';
|
||||
import { MyContext } from '../../types';
|
||||
import { UsernamePasswordInput, UserResponse } from './auth.types';
|
||||
|
||||
import AuthService from './auth.service';
|
||||
import User from './user.entity';
|
||||
|
||||
@Resolver()
|
||||
export default class AuthResolver {
|
||||
@Query(() => User, { nullable: true })
|
||||
async me(@Ctx() ctx: MyContext): Promise<User | null> {
|
||||
const user = await AuthService.me(ctx.req.session.userId);
|
||||
|
||||
return user;
|
||||
}
|
||||
|
||||
@Mutation(() => UserResponse)
|
||||
async register(@Arg('input', () => UsernamePasswordInput) input: UsernamePasswordInput, @Ctx() { req }: MyContext): Promise<UserResponse> {
|
||||
const { user } = await AuthService.register(input);
|
||||
|
||||
if (user) {
|
||||
req.session.userId = user.id;
|
||||
}
|
||||
|
||||
return { user };
|
||||
}
|
||||
|
||||
@Mutation(() => UserResponse)
|
||||
async login(@Arg('input', () => UsernamePasswordInput) input: UsernamePasswordInput, @Ctx() { req }: MyContext): Promise<UserResponse> {
|
||||
const { user } = await AuthService.login(input);
|
||||
|
||||
if (user) {
|
||||
req.session.userId = user.id;
|
||||
}
|
||||
|
||||
return { user };
|
||||
}
|
||||
|
||||
@Authorized()
|
||||
@Mutation(() => Boolean)
|
||||
logout(@Ctx() { req }: MyContext): boolean {
|
||||
req.session.userId = undefined;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@Query(() => Boolean)
|
||||
async isConfigured(): Promise<boolean> {
|
||||
const users = await User.find();
|
||||
|
||||
return users.length > 0;
|
||||
}
|
||||
}
|
|
@ -1,11 +0,0 @@
|
|||
import { Router } from 'express';
|
||||
import AuthController from './auth.controller';
|
||||
|
||||
const router = Router();
|
||||
|
||||
router.route('/login').post(AuthController.login);
|
||||
router.route('/me').get(AuthController.me);
|
||||
router.route('/configured').get(AuthController.isConfigured);
|
||||
router.route('/register').post(AuthController.register);
|
||||
|
||||
export default router;
|
|
@ -1,42 +1,52 @@
|
|||
import * as argon2 from 'argon2';
|
||||
import { IUser } from '../../config/types';
|
||||
import { readJsonFile, writeFile } from '../fs/fs.helpers';
|
||||
import AuthHelpers from './auth.helpers';
|
||||
import { UsernamePasswordInput, UserResponse } from './auth.types';
|
||||
import User from './user.entity';
|
||||
|
||||
const login = async (email: string, password: string) => {
|
||||
const user = AuthHelpers.getUser(email);
|
||||
const login = async (input: UsernamePasswordInput): Promise<UserResponse> => {
|
||||
const { password, username } = input;
|
||||
|
||||
const user = await User.findOne({ where: { username: username.trim().toLowerCase() } });
|
||||
|
||||
if (!user) {
|
||||
throw new Error('User not found');
|
||||
}
|
||||
|
||||
return AuthHelpers.getJwtToken(user, password);
|
||||
};
|
||||
const isPasswordValid = await argon2.verify(user.password, password);
|
||||
|
||||
const register = async (email: string, password: string, name: string) => {
|
||||
const users: IUser[] = readJsonFile('/state/users.json');
|
||||
|
||||
if (users.length > 0) {
|
||||
throw new Error('There is already an admin user');
|
||||
if (!isPasswordValid) {
|
||||
throw new Error('Wrong password');
|
||||
}
|
||||
|
||||
if (!email || !password) {
|
||||
return { user };
|
||||
};
|
||||
|
||||
const register = async (input: UsernamePasswordInput): Promise<UserResponse> => {
|
||||
const { password, username } = input;
|
||||
|
||||
if (!username || !password) {
|
||||
throw new Error('Missing email or password');
|
||||
}
|
||||
|
||||
const hash = await argon2.hash(password);
|
||||
const newuser: IUser = { email, name, password: hash };
|
||||
const newUser = await User.create({ username: username.trim().toLowerCase(), password: hash }).save();
|
||||
|
||||
const token = await AuthHelpers.getJwtToken(newuser, password);
|
||||
return { user: newUser };
|
||||
};
|
||||
|
||||
writeFile('/state/users.json', JSON.stringify([newuser]));
|
||||
const me = async (userId?: number): Promise<User | null> => {
|
||||
if (!userId) return null;
|
||||
|
||||
return token;
|
||||
const user = await User.findOne({ where: { id: userId } });
|
||||
|
||||
if (!user) return null;
|
||||
|
||||
return user;
|
||||
};
|
||||
|
||||
const AuthService = {
|
||||
login,
|
||||
register,
|
||||
me,
|
||||
};
|
||||
|
||||
export default AuthService;
|
||||
|
|
19
packages/system-api/src/modules/auth/auth.types.ts
Normal file
19
packages/system-api/src/modules/auth/auth.types.ts
Normal file
|
@ -0,0 +1,19 @@
|
|||
import { Field, InputType, ObjectType } from 'type-graphql';
|
||||
import User from './user.entity';
|
||||
|
||||
@InputType()
|
||||
class UsernamePasswordInput {
|
||||
@Field(() => String)
|
||||
username!: string;
|
||||
|
||||
@Field(() => String)
|
||||
password!: string;
|
||||
}
|
||||
|
||||
@ObjectType()
|
||||
class UserResponse {
|
||||
@Field(() => User, { nullable: true })
|
||||
user?: User;
|
||||
}
|
||||
|
||||
export { UsernamePasswordInput, UserResponse };
|
28
packages/system-api/src/modules/auth/user.entity.ts
Normal file
28
packages/system-api/src/modules/auth/user.entity.ts
Normal file
|
@ -0,0 +1,28 @@
|
|||
/* eslint-disable import/no-cycle */
|
||||
import { Field, ID, ObjectType } from 'type-graphql';
|
||||
import { BaseEntity, Column, CreateDateColumn, Entity, PrimaryGeneratedColumn, UpdateDateColumn } from 'typeorm';
|
||||
import { IsEmail } from 'class-validator';
|
||||
|
||||
@ObjectType()
|
||||
@Entity()
|
||||
export default class User extends BaseEntity {
|
||||
@Field(() => ID)
|
||||
@PrimaryGeneratedColumn()
|
||||
id!: number;
|
||||
|
||||
@Field(() => String)
|
||||
@IsEmail()
|
||||
@Column({ type: 'varchar', unique: true })
|
||||
username!: string;
|
||||
|
||||
@Column({ type: 'varchar', nullable: false })
|
||||
password!: string;
|
||||
|
||||
@Field(() => Date)
|
||||
@CreateDateColumn()
|
||||
createdAt!: Date;
|
||||
|
||||
@Field(() => Date)
|
||||
@UpdateDateColumn()
|
||||
updatedAt!: Date;
|
||||
}
|
|
@ -17,7 +17,11 @@ export const fileExists = (path: string): boolean => fs.existsSync(getAbsolutePa
|
|||
|
||||
export const writeFile = (path: string, data: any) => fs.writeFileSync(getAbsolutePath(path), data);
|
||||
|
||||
export const createFolder = (path: string) => fs.mkdirSync(getAbsolutePath(path));
|
||||
export const createFolder = (path: string) => {
|
||||
if (!fileExists(path)) {
|
||||
fs.mkdirSync(getAbsolutePath(path));
|
||||
}
|
||||
};
|
||||
export const deleteFolder = (path: string) => fs.rmSync(getAbsolutePath(path), { recursive: true });
|
||||
|
||||
export const copyFile = (source: string, destination: string) => fs.copyFileSync(getAbsolutePath(source), getAbsolutePath(destination));
|
||||
|
|
|
@ -2,10 +2,11 @@ import { GraphQLSchema } from 'graphql';
|
|||
import { buildSchema } from 'type-graphql';
|
||||
import { customAuthChecker } from './core/middlewares/authChecker';
|
||||
import AppsResolver from './modules/apps/apps.resolver';
|
||||
import AuthResolver from './modules/auth/auth.resolver';
|
||||
|
||||
const createSchema = (): Promise<GraphQLSchema> =>
|
||||
buildSchema({
|
||||
resolvers: [AppsResolver],
|
||||
resolvers: [AppsResolver, AuthResolver],
|
||||
validate: true,
|
||||
authChecker: customAuthChecker,
|
||||
});
|
||||
|
|
|
@ -9,6 +9,7 @@ import { ApolloLogs } from './config/logger/apollo.logger';
|
|||
import { createServer } from 'http';
|
||||
import logger from './config/logger/logger';
|
||||
import getSessionMiddleware from './core/middlewares/sessionMiddleware';
|
||||
import { MyContext } from './types';
|
||||
|
||||
const main = async () => {
|
||||
try {
|
||||
|
@ -27,6 +28,7 @@ const main = async () => {
|
|||
|
||||
const apolloServer = new ApolloServer({
|
||||
schema,
|
||||
context: ({ req, res }): MyContext => ({ req, res }),
|
||||
plugins: [Playground({ settings: { 'request.credentials': 'include' } }), ApolloLogs],
|
||||
});
|
||||
await apolloServer.start();
|
||||
|
|
|
@ -160,6 +160,7 @@ importers:
|
|||
express: ^4.17.3
|
||||
express-session: ^1.17.3
|
||||
graphql: ^15.3.0
|
||||
graphql-type-json: ^0.3.2
|
||||
helmet: ^5.0.2
|
||||
http: 0.0.1-security
|
||||
internal-ip: ^6.0.0
|
||||
|
@ -200,6 +201,7 @@ importers:
|
|||
express: 4.18.1
|
||||
express-session: 1.17.3
|
||||
graphql: 15.8.0
|
||||
graphql-type-json: 0.3.2_graphql@15.8.0
|
||||
helmet: 5.0.2
|
||||
http: 0.0.1-security
|
||||
internal-ip: 6.2.0
|
||||
|
@ -4683,7 +4685,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
|
||||
|
@ -5573,6 +5575,14 @@ packages:
|
|||
tslib: 2.4.0
|
||||
dev: false
|
||||
|
||||
/graphql-type-json/0.3.2_graphql@15.8.0:
|
||||
resolution: {integrity: sha512-J+vjof74oMlCWXSvt0DOf2APEdZOCdubEvGDUAlqH//VBYcOYsGgRW7Xzorr44LvkjiuvecWc8fChxuZZbChtg==}
|
||||
peerDependencies:
|
||||
graphql: '>=0.8.0'
|
||||
dependencies:
|
||||
graphql: 15.8.0
|
||||
dev: false
|
||||
|
||||
/graphql/15.8.0:
|
||||
resolution: {integrity: sha512-5gghUc24tP9HRznNpV2+FIoq3xKkj5dTQqf4v0CpdPbFVwFkWoxOM+o+2OC9ZSvjEMTjfmG9QT+gcvggTwW1zw==}
|
||||
engines: {node: '>= 10.x'}
|
||||
|
|
Loading…
Reference in a new issue