base system for graphql
This commit is contained in:
commit
faa4aa48eb
4
.dockerignore
Normal file
4
.dockerignore
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
/dist
|
||||||
|
/node_modules
|
||||||
|
/data
|
||||||
|
/.git
|
||||||
24
.eslintrc.js
Normal file
24
.eslintrc.js
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
module.exports = {
|
||||||
|
parser: '@typescript-eslint/parser',
|
||||||
|
parserOptions: {
|
||||||
|
project: 'tsconfig.json',
|
||||||
|
sourceType: 'module',
|
||||||
|
},
|
||||||
|
plugins: ['@typescript-eslint/eslint-plugin'],
|
||||||
|
extends: [
|
||||||
|
'plugin:@typescript-eslint/eslint-recommended',
|
||||||
|
'plugin:@typescript-eslint/recommended',
|
||||||
|
'prettier',
|
||||||
|
'prettier/@typescript-eslint',
|
||||||
|
],
|
||||||
|
root: true,
|
||||||
|
env: {
|
||||||
|
node: true,
|
||||||
|
jest: true,
|
||||||
|
},
|
||||||
|
rules: {
|
||||||
|
'@typescript-eslint/interface-name-prefix': 'off',
|
||||||
|
'@typescript-eslint/explicit-function-return-type': 'off',
|
||||||
|
'@typescript-eslint/no-explicit-any': 'off',
|
||||||
|
},
|
||||||
|
};
|
||||||
39
.gitignore
vendored
Normal file
39
.gitignore
vendored
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
# compiled output
|
||||||
|
/dist
|
||||||
|
/node_modules
|
||||||
|
|
||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
|
||||||
|
# OS
|
||||||
|
.DS_Store
|
||||||
|
|
||||||
|
# Tests
|
||||||
|
/coverage
|
||||||
|
/.nyc_output
|
||||||
|
|
||||||
|
# IDEs and editors
|
||||||
|
/.idea
|
||||||
|
.project
|
||||||
|
.classpath
|
||||||
|
.c9/
|
||||||
|
*.launch
|
||||||
|
.settings/
|
||||||
|
*.sublime-workspace
|
||||||
|
|
||||||
|
# IDE - VSCode
|
||||||
|
.vscode/*
|
||||||
|
!.vscode/settings.json
|
||||||
|
!.vscode/tasks.json
|
||||||
|
!.vscode/launch.json
|
||||||
|
!.vscode/extensions.json
|
||||||
|
|
||||||
|
# local data
|
||||||
|
/data
|
||||||
|
/.env
|
||||||
|
/src/schema.gql
|
||||||
4
.prettierrc
Normal file
4
.prettierrc
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"singleQuote": true,
|
||||||
|
"trailingComma": "all"
|
||||||
|
}
|
||||||
22
Dockerfile
Normal file
22
Dockerfile
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
FROM node:12 AS builder
|
||||||
|
MAINTAINER OhMyForm <admin@ohmyform.com>
|
||||||
|
|
||||||
|
WORKDIR /usr/src/app
|
||||||
|
|
||||||
|
# just copy everhing
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
RUN yarn install --frozen-lockfile
|
||||||
|
RUN yarn build
|
||||||
|
|
||||||
|
FROM node:12-alpine
|
||||||
|
|
||||||
|
WORKDIR /usr/src/app
|
||||||
|
|
||||||
|
COPY --from=builder /usr/src/app /usr/src/app
|
||||||
|
|
||||||
|
ENV PORT=3000
|
||||||
|
|
||||||
|
EXPOSE 3000
|
||||||
|
|
||||||
|
CMD [ "yarn", "start:prod" ]
|
||||||
52
README.md
Normal file
52
README.md
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
# OhMyForm API
|
||||||
|
|
||||||
|
## Description
|
||||||
|
|
||||||
|
[OhMyForm](https://github.com/ohmyforn) api backend
|
||||||
|
|
||||||
|
All calls to the api are through GraphQL, with the endpoint
|
||||||
|
providing an introspectable schema at `GET /graphql`
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ npm install
|
||||||
|
```
|
||||||
|
|
||||||
|
## Running the app
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# development
|
||||||
|
$ npm run start
|
||||||
|
|
||||||
|
# watch mode
|
||||||
|
$ npm run start:dev
|
||||||
|
|
||||||
|
# production mode
|
||||||
|
$ npm run start:prod
|
||||||
|
```
|
||||||
|
|
||||||
|
## Test
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# unit tests
|
||||||
|
$ npm run test
|
||||||
|
|
||||||
|
# e2e tests
|
||||||
|
$ npm run test:e2e
|
||||||
|
|
||||||
|
# test coverage
|
||||||
|
$ npm run test:cov
|
||||||
|
```
|
||||||
|
|
||||||
|
## Support
|
||||||
|
|
||||||
|
TODO
|
||||||
|
|
||||||
|
## Stay in touch
|
||||||
|
|
||||||
|
- Website - [https://ohmyform.com](https://ohmyform.com/)
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
TODO
|
||||||
24
doc/roles.md
Normal file
24
doc/roles.md
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
# Roles
|
||||||
|
|
||||||
|
## unauthenticated
|
||||||
|
|
||||||
|
every request is unauthenticated unless an `Authorization`
|
||||||
|
header with a valid `JWT Bearer` token is provided.
|
||||||
|
|
||||||
|
## user
|
||||||
|
|
||||||
|
any new registration is a user per default, they can only see their
|
||||||
|
responses and do not have access to forms.
|
||||||
|
|
||||||
|
## admin
|
||||||
|
|
||||||
|
an admin can create forms and edit their own forms. They do not
|
||||||
|
have access to forms from other users.
|
||||||
|
|
||||||
|
## superuser
|
||||||
|
|
||||||
|
a superuser can create and edit any form on the platform as well as
|
||||||
|
modify any user
|
||||||
|
|
||||||
|
they can also grant a user admin or superuser access, they cannot revoke
|
||||||
|
their own superuser role
|
||||||
37
docker-compose.yml
Normal file
37
docker-compose.yml
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
version: "3"
|
||||||
|
services:
|
||||||
|
mongo:
|
||||||
|
image: mongo
|
||||||
|
ports:
|
||||||
|
- "27017:27017"
|
||||||
|
volumes:
|
||||||
|
- "./data/mongo:/data"
|
||||||
|
# api:
|
||||||
|
# build: .
|
||||||
|
# volumes:
|
||||||
|
# - ".:/usr/src/app"
|
||||||
|
# environment:
|
||||||
|
# MONGODB_URI: mongodb://mongo/ohmyform
|
||||||
|
# MAILER_URI: smtp://mail:1025
|
||||||
|
# command: yarn start:dev
|
||||||
|
# links:
|
||||||
|
# - mongo
|
||||||
|
# - mail
|
||||||
|
# ports:
|
||||||
|
# - "6000:3000"
|
||||||
|
# depends_on:
|
||||||
|
# - mongo
|
||||||
|
mail:
|
||||||
|
image: mailhog/mailhog
|
||||||
|
ports:
|
||||||
|
- "6001:8025"
|
||||||
|
mongoexpress:
|
||||||
|
image: mongo-express
|
||||||
|
environment:
|
||||||
|
ME_CONFIG_MONGODB_SERVER: mongo
|
||||||
|
ports:
|
||||||
|
- "6002:8081"
|
||||||
|
links:
|
||||||
|
- mongo
|
||||||
|
depends_on:
|
||||||
|
- mongo
|
||||||
5
locales/en/mail/user/created/body.html
Normal file
5
locales/en/mail/user/created/body.html
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
Welcome to OhMyForm
|
||||||
|
|
||||||
|
Your Username is {{username}}
|
||||||
|
|
||||||
|
cheers
|
||||||
5
locales/en/mail/user/created/body.txt
Normal file
5
locales/en/mail/user/created/body.txt
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
Welcome to OhMyForm
|
||||||
|
|
||||||
|
Your Username is {{username}}
|
||||||
|
|
||||||
|
cheers
|
||||||
1
locales/en/mail/user/created/subject.txt
Normal file
1
locales/en/mail/user/created/subject.txt
Normal file
@ -0,0 +1 @@
|
|||||||
|
Welcome to OhMyForm
|
||||||
7
nest-cli.json
Normal file
7
nest-cli.json
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"collection": "@nestjs/schematics",
|
||||||
|
"sourceRoot": "src",
|
||||||
|
"compilerOptions": {
|
||||||
|
"plugins": ["@nestjs/graphql/plugin"]
|
||||||
|
}
|
||||||
|
}
|
||||||
101
package.json
Normal file
101
package.json
Normal file
@ -0,0 +1,101 @@
|
|||||||
|
{
|
||||||
|
"name": "ohmyform-api",
|
||||||
|
"version": "0.3.0",
|
||||||
|
"description": "",
|
||||||
|
"author": "",
|
||||||
|
"private": true,
|
||||||
|
"license": "UNLICENSED",
|
||||||
|
"scripts": {
|
||||||
|
"prebuild": "rimraf dist",
|
||||||
|
"build": "nest build",
|
||||||
|
"format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
|
||||||
|
"cli:dev": "cross-env CLI=true TS_NODE_TRANSPILE_ONLY=true ts-node -r tsconfig-paths/register src/cli.ts",
|
||||||
|
"cli": "cross-env CLI=true node dist/console.js",
|
||||||
|
"start": "nest start",
|
||||||
|
"start:dev": "nest start --watch",
|
||||||
|
"start:debug": "nest start --debug --watch",
|
||||||
|
"start:prod": "cross-env NODE_ENV=production node dist/main",
|
||||||
|
"lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",
|
||||||
|
"test": "jest",
|
||||||
|
"test:watch": "jest --watch",
|
||||||
|
"test:cov": "jest --coverage",
|
||||||
|
"test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
|
||||||
|
"test:e2e": "jest --config ./test/jest-e2e.json"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@nestjs-modules/mailer": "^1.4.2",
|
||||||
|
"@nestjs/common": "^7.0.9",
|
||||||
|
"@nestjs/config": "^0.4.0",
|
||||||
|
"@nestjs/core": "^7.0.9",
|
||||||
|
"@nestjs/graphql": "^7.3.7",
|
||||||
|
"@nestjs/jwt": "^7.0.0",
|
||||||
|
"@nestjs/mongoose": "^6.4.0",
|
||||||
|
"@nestjs/passport": "^7.0.0",
|
||||||
|
"@nestjs/platform-express": "^7.0.0",
|
||||||
|
"apollo-server-express": "^2.13.0",
|
||||||
|
"bcrypt": "^4.0.1",
|
||||||
|
"class-transformer": "^0.2.3",
|
||||||
|
"class-validator": "^0.12.2",
|
||||||
|
"commander": "^5.1.0",
|
||||||
|
"cors": "^2.8.5",
|
||||||
|
"cross-env": "^7.0.2",
|
||||||
|
"graphql": "15.0.0",
|
||||||
|
"graphql-tools": "^5.0.0",
|
||||||
|
"handlebars": "^4.7.6",
|
||||||
|
"inquirer": "^7.1.0",
|
||||||
|
"migrate-mongoose": "^4.0.0",
|
||||||
|
"mongoose": "^5.9.11",
|
||||||
|
"nestjs-console": "^3.0.2",
|
||||||
|
"nestjs-pino": "^1.2.0",
|
||||||
|
"nodemailer": "^6.4.6",
|
||||||
|
"passport": "^0.4.1",
|
||||||
|
"passport-jwt": "^4.0.0",
|
||||||
|
"passport-local": "^1.0.0",
|
||||||
|
"pino-pretty": "^4.0.0",
|
||||||
|
"reflect-metadata": "^0.1.13",
|
||||||
|
"rimraf": "^3.0.2",
|
||||||
|
"rxjs": "^6.5.4"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@nestjs/cli": "^7.0.0",
|
||||||
|
"@nestjs/schematics": "^7.0.0",
|
||||||
|
"@nestjs/testing": "^7.0.0",
|
||||||
|
"@types/bcrypt": "^3.0.0",
|
||||||
|
"@types/express": "^4.17.3",
|
||||||
|
"@types/handlebars": "^4.1.0",
|
||||||
|
"@types/inquirer": "^6.5.0",
|
||||||
|
"@types/jest": "25.1.4",
|
||||||
|
"@types/mongoose": "^5.7.14",
|
||||||
|
"@types/node": "^13.9.1",
|
||||||
|
"@types/passport-jwt": "^3.0.3",
|
||||||
|
"@types/passport-local": "^1.0.33",
|
||||||
|
"@types/supertest": "^2.0.8",
|
||||||
|
"@typescript-eslint/eslint-plugin": "^2.23.0",
|
||||||
|
"@typescript-eslint/parser": "^2.23.0",
|
||||||
|
"eslint": "^6.8.0",
|
||||||
|
"eslint-config-prettier": "^6.10.0",
|
||||||
|
"eslint-plugin-import": "^2.20.1",
|
||||||
|
"jest": "^25.1.0",
|
||||||
|
"prettier": "^1.19.1",
|
||||||
|
"supertest": "^4.0.2",
|
||||||
|
"ts-jest": "25.2.1",
|
||||||
|
"ts-loader": "^6.2.1",
|
||||||
|
"ts-node": "^8.6.2",
|
||||||
|
"tsconfig-paths": "^3.9.0",
|
||||||
|
"typescript": "^3.7.4"
|
||||||
|
},
|
||||||
|
"jest": {
|
||||||
|
"moduleFileExtensions": [
|
||||||
|
"js",
|
||||||
|
"json",
|
||||||
|
"ts"
|
||||||
|
],
|
||||||
|
"rootDir": "src",
|
||||||
|
"testRegex": ".spec.ts$",
|
||||||
|
"transform": {
|
||||||
|
"^.+\\.(t|j)s$": "ts-jest"
|
||||||
|
},
|
||||||
|
"coverageDirectory": "../coverage",
|
||||||
|
"testEnvironment": "node"
|
||||||
|
}
|
||||||
|
}
|
||||||
110
src/app.imports.ts
Normal file
110
src/app.imports.ts
Normal file
@ -0,0 +1,110 @@
|
|||||||
|
import { MailerModule } from '@nestjs-modules/mailer';
|
||||||
|
import { HttpModule, RequestMethod } from '@nestjs/common';
|
||||||
|
import { ConfigModule, ConfigService } from '@nestjs/config';
|
||||||
|
import { GraphQLFederationModule } from '@nestjs/graphql';
|
||||||
|
import { JwtModule, JwtModuleOptions } from '@nestjs/jwt';
|
||||||
|
import { MongooseModule } from '@nestjs/mongoose';
|
||||||
|
import { MongooseModuleOptions } from '@nestjs/mongoose/dist/interfaces/mongoose-options.interface';
|
||||||
|
import crypto from 'crypto';
|
||||||
|
import { ConsoleModule } from 'nestjs-console';
|
||||||
|
import { LoggerModule, Params as LoggerModuleParams } from 'nestjs-pino/dist';
|
||||||
|
import { join } from 'path';
|
||||||
|
import { ContextCache } from './resolver/context.cache';
|
||||||
|
import { schema } from './schema';
|
||||||
|
|
||||||
|
export const LoggerConfig: LoggerModuleParams = {
|
||||||
|
pinoHttp: {
|
||||||
|
level: process.env.CLI ? 'warn' : process.env.NODE_ENV !== 'production' ? 'debug' : 'info',
|
||||||
|
prettyPrint: process.env.NODE_ENV !== 'production' || process.env.CLI ? {
|
||||||
|
translateTime: true,
|
||||||
|
colorize: true,
|
||||||
|
ignore: 'pid,hostname,req,res',
|
||||||
|
} : false,
|
||||||
|
},
|
||||||
|
exclude: [
|
||||||
|
{
|
||||||
|
method: RequestMethod.ALL,
|
||||||
|
path: '_health',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
method: RequestMethod.ALL,
|
||||||
|
path: 'favicon.ico',
|
||||||
|
}
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
export const imports = [
|
||||||
|
ConsoleModule,
|
||||||
|
HttpModule.register({
|
||||||
|
timeout: 5000,
|
||||||
|
maxRedirects: 10,
|
||||||
|
}),
|
||||||
|
ConfigModule.forRoot({
|
||||||
|
load: [
|
||||||
|
() => {
|
||||||
|
return {
|
||||||
|
LOCALES_PATH: join(process.cwd(), 'locales'),
|
||||||
|
AUTH_SECRET: process.env.AUTH_SECRET || crypto.randomBytes(20).toString('hex'),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
JwtModule.registerAsync({
|
||||||
|
imports: [ConfigModule],
|
||||||
|
inject: [ConfigService],
|
||||||
|
useFactory: async (configService: ConfigService): Promise<JwtModuleOptions> => ({
|
||||||
|
secret: configService.get<string>('AUTH_SECRET'),
|
||||||
|
signOptions: {
|
||||||
|
expiresIn: '4h',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}),
|
||||||
|
LoggerModule.forRoot(LoggerConfig),
|
||||||
|
GraphQLFederationModule.forRoot({
|
||||||
|
debug: process.env.NODE_ENV !== 'production',
|
||||||
|
definitions: {
|
||||||
|
outputAs: 'class',
|
||||||
|
},
|
||||||
|
introspection: true,
|
||||||
|
playground: true,
|
||||||
|
// installSubscriptionHandlers: true,
|
||||||
|
autoSchemaFile: join(process.cwd(), 'src/schema.gql'),
|
||||||
|
// to allow guards on resolver props https://github.com/nestjs/graphql/issues/295
|
||||||
|
fieldResolverEnhancers: [
|
||||||
|
'guards',
|
||||||
|
'interceptors',
|
||||||
|
],
|
||||||
|
resolverValidationOptions: {
|
||||||
|
|
||||||
|
},
|
||||||
|
context: ({ req }) => {
|
||||||
|
return {
|
||||||
|
cache: new ContextCache(),
|
||||||
|
req,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
MongooseModule.forRootAsync({
|
||||||
|
imports: [ConfigModule],
|
||||||
|
inject: [ConfigService],
|
||||||
|
useFactory: async (configService: ConfigService): Promise<MongooseModuleOptions> => ({
|
||||||
|
uri: configService.get<string>('MONGODB_URI', 'mongodb://localhost/ohmyform'),
|
||||||
|
// takes care of deprecations from https://mongoosejs.com/docs/deprecations.html
|
||||||
|
useNewUrlParser: true,
|
||||||
|
useUnifiedTopology: true,
|
||||||
|
useCreateIndex: true,
|
||||||
|
useFindAndModify: false,
|
||||||
|
})
|
||||||
|
}),
|
||||||
|
MongooseModule.forFeature(schema),
|
||||||
|
MailerModule.forRootAsync({
|
||||||
|
imports: [ConfigModule],
|
||||||
|
inject: [ConfigService],
|
||||||
|
useFactory: async (configService: ConfigService) => ({
|
||||||
|
transport: configService.get<string>('MAILER_URI', 'smtp://localhost:1025'),
|
||||||
|
defaults: {
|
||||||
|
from: configService.get<string>('MAILER_FROM', 'OhMyForm <no-reply@localhost>'),
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
]
|
||||||
11
src/app.module.ts
Normal file
11
src/app.module.ts
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { imports } from './app.imports';
|
||||||
|
import { providers } from './app.providers';
|
||||||
|
import { controllers } from './controller';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
imports,
|
||||||
|
controllers,
|
||||||
|
providers,
|
||||||
|
})
|
||||||
|
export class AppModule {}
|
||||||
11
src/app.providers.ts
Normal file
11
src/app.providers.ts
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
import { commands } from './command';
|
||||||
|
import { guards } from './guard';
|
||||||
|
import { resolvers } from './resolver';
|
||||||
|
import { services } from './service';
|
||||||
|
|
||||||
|
export const providers = [
|
||||||
|
...resolvers,
|
||||||
|
...commands,
|
||||||
|
...services,
|
||||||
|
...guards,
|
||||||
|
]
|
||||||
16
src/cli.ts
Normal file
16
src/cli.ts
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
import { BootstrapConsole } from 'nestjs-console';
|
||||||
|
import { AppModule } from './app.module';
|
||||||
|
|
||||||
|
const bootstrap = new BootstrapConsole({
|
||||||
|
module: AppModule,
|
||||||
|
useDecorators: true,
|
||||||
|
});
|
||||||
|
bootstrap.init().then(async (app) => {
|
||||||
|
try {
|
||||||
|
await app.init();
|
||||||
|
await bootstrap.boot();
|
||||||
|
process.exit(0);
|
||||||
|
} catch (e) {
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
});
|
||||||
5
src/command/index.ts
Normal file
5
src/command/index.ts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
import { UserCommand } from './user.command';
|
||||||
|
|
||||||
|
export const commands = [
|
||||||
|
UserCommand,
|
||||||
|
]
|
||||||
64
src/command/user.command.ts
Normal file
64
src/command/user.command.ts
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
import inquirer from 'inquirer';
|
||||||
|
import { Command, Console } from 'nestjs-console';
|
||||||
|
import { matchType, validatePassword } from '../config/fields';
|
||||||
|
import { UserCreateService } from '../service/user/user.create.service';
|
||||||
|
|
||||||
|
@Console({
|
||||||
|
name: 'user',
|
||||||
|
description: 'handle instance users'
|
||||||
|
})
|
||||||
|
export class UserCommand {
|
||||||
|
constructor(
|
||||||
|
private readonly createUser: UserCreateService
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Command({
|
||||||
|
command: 'create',
|
||||||
|
})
|
||||||
|
async create(): Promise<void> {
|
||||||
|
const answers = await inquirer.prompt([
|
||||||
|
{
|
||||||
|
type: 'input',
|
||||||
|
name: 'username',
|
||||||
|
message: 'username for login',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'input',
|
||||||
|
name: 'email',
|
||||||
|
message: 'email to send notifications to',
|
||||||
|
validate(input: string): boolean | string {
|
||||||
|
if (!matchType.email.test(input)) {
|
||||||
|
return 'invalid email'
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'password',
|
||||||
|
name: 'password',
|
||||||
|
validate: validatePassword,
|
||||||
|
message: 'password to login',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'confirm',
|
||||||
|
name: 'create',
|
||||||
|
message: current => {
|
||||||
|
return `create user ${current.username} with email ${current.email}`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
])
|
||||||
|
|
||||||
|
await this.createUser.create(answers)
|
||||||
|
|
||||||
|
console.info(`user ${answers.username} has been created`)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Command({
|
||||||
|
command: 'activate <username>',
|
||||||
|
})
|
||||||
|
async activate(username: string): Promise<void> {
|
||||||
|
console.log(`activate user ${username}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
30
src/config/fields.ts
Normal file
30
src/config/fields.ts
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
|
||||||
|
export const fieldTypes = [
|
||||||
|
'textfield',
|
||||||
|
'date',
|
||||||
|
'email',
|
||||||
|
'legal',
|
||||||
|
'textarea',
|
||||||
|
'link',
|
||||||
|
'statement',
|
||||||
|
'dropdown',
|
||||||
|
'rating',
|
||||||
|
'radio',
|
||||||
|
'hidden',
|
||||||
|
'yes_no',
|
||||||
|
'number',
|
||||||
|
]
|
||||||
|
|
||||||
|
export const matchType = {
|
||||||
|
color: /^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$/,
|
||||||
|
url: /((([A-Za-z]{3,9}:(?:\/\/)?)(?:[-;:&=\+\$,\w]+@)?[A-Za-z0-9.-]+|(?:www.|[-;:&=\+\$,\w]+@)[A-Za-z0-9.-]+)((?:\/[\+~%\/.\w-_]*)?\??(?:[-\+=&;%@.\w_]*)#?(?:[\w]*))?)/,
|
||||||
|
email: /.+@.+\..+/,
|
||||||
|
}
|
||||||
|
|
||||||
|
export const validatePassword = (password: string): true | string => {
|
||||||
|
if (password.length < 4) {
|
||||||
|
return 'password is too short'
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
4
src/config/languages.ts
Normal file
4
src/config/languages.ts
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
|
||||||
|
export const languages = ['en', 'fr', 'es', 'it', 'de']
|
||||||
|
|
||||||
|
export const defaultLanguage = 'en'
|
||||||
5
src/config/roles.ts
Normal file
5
src/config/roles.ts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
|
||||||
|
export type roleType = 'user' | 'admin' | 'superuser'
|
||||||
|
export type rolesType = roleType[]
|
||||||
|
|
||||||
|
export const roles: rolesType = ['user', 'admin', 'superuser']
|
||||||
9
src/controller/health.controller.ts
Normal file
9
src/controller/health.controller.ts
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
import { Controller, Get } from '@nestjs/common';
|
||||||
|
|
||||||
|
@Controller()
|
||||||
|
export class HealthController {
|
||||||
|
@Get('/_health')
|
||||||
|
getHello(): string {
|
||||||
|
return 'ok';
|
||||||
|
}
|
||||||
|
}
|
||||||
5
src/controller/index.ts
Normal file
5
src/controller/index.ts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
import { HealthController } from './health.controller';
|
||||||
|
|
||||||
|
export const controllers = [
|
||||||
|
HealthController,
|
||||||
|
]
|
||||||
4
src/decorator/roles.decorator.ts
Normal file
4
src/decorator/roles.decorator.ts
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
import { SetMetadata } from '@nestjs/common';
|
||||||
|
import { rolesType } from '../config/roles';
|
||||||
|
|
||||||
|
export const Roles = (...roles: rolesType) => SetMetadata('roles', roles);
|
||||||
7
src/decorator/user.decorator.ts
Normal file
7
src/decorator/user.decorator.ts
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
import { createParamDecorator, ExecutionContext } from '@nestjs/common';
|
||||||
|
import { GqlExecutionContext } from '@nestjs/graphql';
|
||||||
|
|
||||||
|
export const User = createParamDecorator(
|
||||||
|
(data: unknown, ctx: ExecutionContext) =>
|
||||||
|
GqlExecutionContext.create(ctx).getContext().req.user,
|
||||||
|
);
|
||||||
15
src/dto/auth/auth.jwt.model.ts
Normal file
15
src/dto/auth/auth.jwt.model.ts
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
import { Field, ObjectType } from '@nestjs/graphql';
|
||||||
|
|
||||||
|
@ObjectType('AuthToken')
|
||||||
|
export class AuthJwtModel {
|
||||||
|
@Field()
|
||||||
|
readonly accessToken: string
|
||||||
|
|
||||||
|
@Field()
|
||||||
|
readonly refreshToken: string
|
||||||
|
|
||||||
|
constructor(partial: Partial<AuthJwtModel>) {
|
||||||
|
this.accessToken = partial.accessToken
|
||||||
|
this.refreshToken = partial.refreshToken
|
||||||
|
}
|
||||||
|
}
|
||||||
20
src/dto/form/abstract.notification.model.ts
Normal file
20
src/dto/form/abstract.notification.model.ts
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
import { Field, InterfaceType } from '@nestjs/graphql';
|
||||||
|
import { Notifications } from '../../schema/form.schema';
|
||||||
|
|
||||||
|
@InterfaceType('Notification')
|
||||||
|
export class AbstractNotificationModel {
|
||||||
|
@Field({ nullable: true })
|
||||||
|
readonly subject?: string
|
||||||
|
|
||||||
|
@Field({ nullable: true })
|
||||||
|
readonly htmlTemplate?: string
|
||||||
|
|
||||||
|
@Field()
|
||||||
|
readonly enabled: boolean
|
||||||
|
|
||||||
|
constructor(partial: Partial<Notifications>) {
|
||||||
|
this.subject = partial.subject
|
||||||
|
this.htmlTemplate = partial.htmlTemplate
|
||||||
|
this.enabled = partial.enabled
|
||||||
|
}
|
||||||
|
}
|
||||||
27
src/dto/form/button.model.ts
Normal file
27
src/dto/form/button.model.ts
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
import { Field, ObjectType } from '@nestjs/graphql';
|
||||||
|
|
||||||
|
@ObjectType('Button')
|
||||||
|
export class ButtonModel {
|
||||||
|
@Field({ nullable: true })
|
||||||
|
readonly url?: string
|
||||||
|
|
||||||
|
@Field({ nullable: true })
|
||||||
|
readonly action?: string
|
||||||
|
|
||||||
|
@Field({ nullable: true })
|
||||||
|
readonly text: string
|
||||||
|
|
||||||
|
@Field({ nullable: true })
|
||||||
|
readonly bgColor?: string
|
||||||
|
|
||||||
|
@Field({ nullable: true })
|
||||||
|
readonly color?: string
|
||||||
|
|
||||||
|
constructor(button: Partial<ButtonModel>) {
|
||||||
|
this.url = button.url
|
||||||
|
this.action = button.action
|
||||||
|
this.text = button.text
|
||||||
|
this.bgColor = button.bgColor
|
||||||
|
this.color = button.color
|
||||||
|
}
|
||||||
|
}
|
||||||
28
src/dto/form/colors.model.ts
Normal file
28
src/dto/form/colors.model.ts
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
import { Field, ObjectType } from '@nestjs/graphql';
|
||||||
|
import { Colors } from '../../schema/form.schema';
|
||||||
|
|
||||||
|
@ObjectType('Colors')
|
||||||
|
export class ColorsModel {
|
||||||
|
@Field()
|
||||||
|
readonly backgroundColor: string
|
||||||
|
|
||||||
|
@Field()
|
||||||
|
readonly questionColor: string
|
||||||
|
|
||||||
|
@Field()
|
||||||
|
readonly answerColor: string
|
||||||
|
|
||||||
|
@Field()
|
||||||
|
readonly buttonColor: string
|
||||||
|
|
||||||
|
@Field()
|
||||||
|
readonly buttonTextColor: string
|
||||||
|
|
||||||
|
constructor(partial: Partial<Colors>) {
|
||||||
|
this.backgroundColor = partial.backgroundColor
|
||||||
|
this.questionColor = partial.questionColor
|
||||||
|
this.answerColor = partial.answerColor
|
||||||
|
this.buttonColor = partial.buttonColor
|
||||||
|
this.buttonTextColor = partial.buttonTextColor
|
||||||
|
}
|
||||||
|
}
|
||||||
17
src/dto/form/design.model.ts
Normal file
17
src/dto/form/design.model.ts
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
import { Field, ObjectType } from '@nestjs/graphql';
|
||||||
|
import { Design } from '../../schema/form.schema';
|
||||||
|
import { ColorsModel } from './colors.model';
|
||||||
|
|
||||||
|
@ObjectType('Design')
|
||||||
|
export class DesignModel {
|
||||||
|
@Field()
|
||||||
|
readonly colors: ColorsModel
|
||||||
|
|
||||||
|
@Field({ nullable: true })
|
||||||
|
readonly font?: string
|
||||||
|
|
||||||
|
constructor(partial: Partial<Design>) {
|
||||||
|
this.colors = new ColorsModel(partial.colors)
|
||||||
|
this.font = partial.font
|
||||||
|
}
|
||||||
|
}
|
||||||
32
src/dto/form/form.model.ts
Normal file
32
src/dto/form/form.model.ts
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
import { Field, ID, ObjectType } from '@nestjs/graphql';
|
||||||
|
import { FormDocument } from '../../schema/form.schema';
|
||||||
|
|
||||||
|
@ObjectType('Form')
|
||||||
|
export class FormModel {
|
||||||
|
@Field(() => ID)
|
||||||
|
readonly id: string
|
||||||
|
|
||||||
|
@Field()
|
||||||
|
readonly title: string
|
||||||
|
|
||||||
|
@Field()
|
||||||
|
readonly created: Date
|
||||||
|
|
||||||
|
@Field({ nullable: true })
|
||||||
|
readonly lastModified?: Date
|
||||||
|
|
||||||
|
@Field()
|
||||||
|
readonly language: string
|
||||||
|
|
||||||
|
@Field()
|
||||||
|
readonly showFooter: boolean
|
||||||
|
|
||||||
|
constructor(partial: Partial<FormDocument>) {
|
||||||
|
this.id = partial.id
|
||||||
|
this.title = partial.title
|
||||||
|
this.created = partial.created
|
||||||
|
this.lastModified = partial.lastModified
|
||||||
|
this.language = partial.language
|
||||||
|
this.showFooter = partial.showFooter
|
||||||
|
}
|
||||||
|
}
|
||||||
29
src/dto/form/form.page.model.ts
Normal file
29
src/dto/form/form.page.model.ts
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
import { Field, ObjectType } from '@nestjs/graphql';
|
||||||
|
import { FormPage } from '../../schema/form.schema';
|
||||||
|
import { ButtonModel } from './button.model';
|
||||||
|
|
||||||
|
@ObjectType('FormPage')
|
||||||
|
export class FormPageModel {
|
||||||
|
@Field()
|
||||||
|
readonly show: boolean
|
||||||
|
|
||||||
|
@Field({ nullable: true })
|
||||||
|
readonly title?: string
|
||||||
|
|
||||||
|
@Field({ nullable: true })
|
||||||
|
readonly paragraph?: string
|
||||||
|
|
||||||
|
@Field({ nullable: true })
|
||||||
|
readonly buttonText?: string
|
||||||
|
|
||||||
|
@Field(() => [ButtonModel])
|
||||||
|
readonly buttons: ButtonModel[]
|
||||||
|
|
||||||
|
constructor(page: Partial<FormPage>) {
|
||||||
|
this.show = page.show
|
||||||
|
this.title = page.title
|
||||||
|
this.paragraph = page.paragraph
|
||||||
|
this.buttonText = page.buttonText
|
||||||
|
this.buttons = page.buttons.map(button => new ButtonModel(button))
|
||||||
|
}
|
||||||
|
}
|
||||||
21
src/dto/form/respondent.notifications.model.ts
Normal file
21
src/dto/form/respondent.notifications.model.ts
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
import { Field, ObjectType } from '@nestjs/graphql';
|
||||||
|
import { RespondentNotifications } from '../../schema/form.schema';
|
||||||
|
import { AbstractNotificationModel } from './abstract.notification.model';
|
||||||
|
|
||||||
|
@ObjectType({
|
||||||
|
implements: [AbstractNotificationModel],
|
||||||
|
})
|
||||||
|
export class RespondentNotificationsModel extends AbstractNotificationModel {
|
||||||
|
@Field({ nullable: true })
|
||||||
|
readonly toField?: string
|
||||||
|
|
||||||
|
@Field({ nullable: true })
|
||||||
|
readonly fromEmail?: string
|
||||||
|
|
||||||
|
constructor(partial: Partial<RespondentNotifications>) {
|
||||||
|
super(partial);
|
||||||
|
|
||||||
|
this.toField = partial.toField
|
||||||
|
this.fromEmail = partial.fromEmail
|
||||||
|
}
|
||||||
|
}
|
||||||
21
src/dto/form/self.notifications.model.ts
Normal file
21
src/dto/form/self.notifications.model.ts
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
import { Field, ObjectType } from '@nestjs/graphql';
|
||||||
|
import { SelfNotifications } from '../../schema/form.schema';
|
||||||
|
import { AbstractNotificationModel } from './abstract.notification.model';
|
||||||
|
|
||||||
|
@ObjectType({
|
||||||
|
implements: [AbstractNotificationModel],
|
||||||
|
})
|
||||||
|
export class SelfNotificationsModel extends AbstractNotificationModel {
|
||||||
|
@Field({ nullable: true })
|
||||||
|
readonly fromField?: string
|
||||||
|
|
||||||
|
@Field({ nullable: true })
|
||||||
|
readonly toEmail?: string
|
||||||
|
|
||||||
|
constructor(partial: Partial<SelfNotifications>) {
|
||||||
|
super(partial);
|
||||||
|
|
||||||
|
this.fromField = partial.fromField
|
||||||
|
this.toEmail = partial.toEmail
|
||||||
|
}
|
||||||
|
}
|
||||||
11
src/dto/status.model.ts
Normal file
11
src/dto/status.model.ts
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
import { Field, ObjectType } from '@nestjs/graphql';
|
||||||
|
|
||||||
|
@ObjectType('Version')
|
||||||
|
export class StatusModel {
|
||||||
|
@Field()
|
||||||
|
readonly version: string
|
||||||
|
|
||||||
|
constructor(partial: Partial<StatusModel>) {
|
||||||
|
this.version = partial.version
|
||||||
|
}
|
||||||
|
}
|
||||||
8
src/dto/user/own.user.model.ts
Normal file
8
src/dto/user/own.user.model.ts
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
import { Field, ObjectType } from '@nestjs/graphql';
|
||||||
|
import { UserModel } from './user.model';
|
||||||
|
|
||||||
|
@ObjectType('OwnUser')
|
||||||
|
export class OwnUserModel extends UserModel {
|
||||||
|
@Field(() => [String])
|
||||||
|
readonly roles: string[]
|
||||||
|
}
|
||||||
28
src/dto/user/user.create.input.ts
Normal file
28
src/dto/user/user.create.input.ts
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
import { Field, InputType } from '@nestjs/graphql';
|
||||||
|
import { IsEmail, IsNotEmpty, MaxLength, MinLength } from 'class-validator';
|
||||||
|
|
||||||
|
@InputType()
|
||||||
|
export class UserCreateInput {
|
||||||
|
@Field()
|
||||||
|
@MinLength(2)
|
||||||
|
@MaxLength(50)
|
||||||
|
username: string
|
||||||
|
|
||||||
|
@Field()
|
||||||
|
@IsEmail()
|
||||||
|
@IsNotEmpty()
|
||||||
|
email: string
|
||||||
|
|
||||||
|
@Field()
|
||||||
|
@MinLength(5)
|
||||||
|
password: string
|
||||||
|
|
||||||
|
@Field({ nullable: true })
|
||||||
|
firstName?: string
|
||||||
|
|
||||||
|
@Field({ nullable: true })
|
||||||
|
lastName?: string
|
||||||
|
|
||||||
|
@Field({ nullable: true })
|
||||||
|
language?: string
|
||||||
|
}
|
||||||
33
src/dto/user/user.model.ts
Normal file
33
src/dto/user/user.model.ts
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
import { Field, ID, ObjectType } from '@nestjs/graphql';
|
||||||
|
import { UserDocument } from '../../schema/user.schema';
|
||||||
|
|
||||||
|
@ObjectType('User')
|
||||||
|
export class UserModel {
|
||||||
|
@Field(() => ID)
|
||||||
|
readonly id: string
|
||||||
|
|
||||||
|
@Field()
|
||||||
|
readonly username: string
|
||||||
|
|
||||||
|
@Field()
|
||||||
|
readonly email: string
|
||||||
|
|
||||||
|
@Field()
|
||||||
|
readonly language: string
|
||||||
|
|
||||||
|
@Field()
|
||||||
|
readonly firstName?: string
|
||||||
|
|
||||||
|
@Field()
|
||||||
|
readonly lastName?: string
|
||||||
|
|
||||||
|
constructor(user: Partial<UserDocument>) {
|
||||||
|
this.id = user.id
|
||||||
|
this.username = user.username
|
||||||
|
this.email = user.email
|
||||||
|
|
||||||
|
this.language = user.language
|
||||||
|
this.firstName = user.firstName
|
||||||
|
this.lastName = user.lastName
|
||||||
|
}
|
||||||
|
}
|
||||||
23
src/guard/gql.auth.guard.ts
Normal file
23
src/guard/gql.auth.guard.ts
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
import { ExecutionContext, Injectable } from '@nestjs/common';
|
||||||
|
import { GqlExecutionContext } from '@nestjs/graphql';
|
||||||
|
import { AuthGuard } from '@nestjs/passport';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class GqlAuthGuard extends AuthGuard('jwt') {
|
||||||
|
getRequest(context: ExecutionContext) {
|
||||||
|
const ctx = GqlExecutionContext.create(context);
|
||||||
|
if (!ctx.getContext().cache) {
|
||||||
|
ctx.getContext().cache = {
|
||||||
|
// add(type, id, object) =>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ctx.getContext().req;
|
||||||
|
}
|
||||||
|
|
||||||
|
handleRequest(err, user) {
|
||||||
|
if (err) {
|
||||||
|
throw new Error('invalid token')
|
||||||
|
}
|
||||||
|
return user
|
||||||
|
}
|
||||||
|
}
|
||||||
16
src/guard/index.ts
Normal file
16
src/guard/index.ts
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
import { APP_GUARD } from '@nestjs/core';
|
||||||
|
import { GqlAuthGuard } from './gql.auth.guard';
|
||||||
|
import { LocalAuthGuard } from './local.auth.guard';
|
||||||
|
import { RolesGuard } from './roles.guard';
|
||||||
|
|
||||||
|
export const guards = [
|
||||||
|
{
|
||||||
|
provide: APP_GUARD,
|
||||||
|
useClass: GqlAuthGuard,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
provide: APP_GUARD,
|
||||||
|
useClass: RolesGuard,
|
||||||
|
},
|
||||||
|
LocalAuthGuard,
|
||||||
|
]
|
||||||
10
src/guard/local.auth.guard.ts
Normal file
10
src/guard/local.auth.guard.ts
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
import { ExecutionContext, Injectable } from '@nestjs/common';
|
||||||
|
import { GqlExecutionContext } from '@nestjs/graphql';
|
||||||
|
import { AuthGuard } from '@nestjs/passport';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class LocalAuthGuard extends AuthGuard('local') {
|
||||||
|
getRequest(context: ExecutionContext) {
|
||||||
|
return GqlExecutionContext.create(context).getContext().req
|
||||||
|
}
|
||||||
|
}
|
||||||
27
src/guard/roles.guard.ts
Normal file
27
src/guard/roles.guard.ts
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
|
||||||
|
import { Reflector } from '@nestjs/core';
|
||||||
|
import { GqlExecutionContext } from '@nestjs/graphql';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class RolesGuard implements CanActivate {
|
||||||
|
constructor(private reflector: Reflector) {}
|
||||||
|
|
||||||
|
canActivate(context: ExecutionContext): boolean {
|
||||||
|
const ctx = GqlExecutionContext.create(context);
|
||||||
|
|
||||||
|
const roles = this.reflector.get<string[]>('roles', ctx.getHandler());
|
||||||
|
if (!roles) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const userRoles = ctx.getContext().req.user ? ctx.getContext().req.user.roles : []
|
||||||
|
|
||||||
|
for (const role of roles) {
|
||||||
|
if (!userRoles.includes(role)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
23
src/main.ts
Normal file
23
src/main.ts
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
import { NestApplicationOptions, ValidationPipe } from '@nestjs/common';
|
||||||
|
import { NestFactory } from '@nestjs/core';
|
||||||
|
import cors from 'cors';
|
||||||
|
import { Logger, PinoLogger } from 'nestjs-pino/dist';
|
||||||
|
import { LoggerConfig } from './app.imports';
|
||||||
|
import { AppModule } from './app.module';
|
||||||
|
|
||||||
|
(async () => {
|
||||||
|
const options: NestApplicationOptions = {
|
||||||
|
logger: new Logger(new PinoLogger(LoggerConfig), {}),
|
||||||
|
}
|
||||||
|
|
||||||
|
const app = await NestFactory.create(AppModule, options)
|
||||||
|
app.useLogger(app.get(Logger))
|
||||||
|
app.useGlobalPipes(new ValidationPipe({
|
||||||
|
disableErrorMessages: false,
|
||||||
|
transform: true,
|
||||||
|
}))
|
||||||
|
app.enableCors({origin: '*'})
|
||||||
|
app.getHttpAdapter().options('*', cors())
|
||||||
|
|
||||||
|
await app.listen(process.env.PORT || 3000);
|
||||||
|
})()
|
||||||
26
src/resolver/auth/auth.login.resolver.ts
Normal file
26
src/resolver/auth/auth.login.resolver.ts
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { Args, Mutation } from '@nestjs/graphql';
|
||||||
|
import { AuthJwtModel } from '../../dto/auth/auth.jwt.model';
|
||||||
|
import { AuthService } from '../../service/auth/auth.service';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class AuthLoginResolver {
|
||||||
|
constructor(
|
||||||
|
private readonly auth: AuthService
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Mutation(() => AuthJwtModel)
|
||||||
|
async authLogin(
|
||||||
|
@Args({ name: 'username', type: () => String }) username,
|
||||||
|
@Args({ name: 'password', type: () => String }) password,
|
||||||
|
): Promise<AuthJwtModel> {
|
||||||
|
const user = await this.auth.validateUser(username, password)
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
throw new Error('invalid user / password')
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.auth.login(user)
|
||||||
|
}
|
||||||
|
}
|
||||||
24
src/resolver/auth/auth.register.resolver.ts
Normal file
24
src/resolver/auth/auth.register.resolver.ts
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { Args, Mutation } from '@nestjs/graphql';
|
||||||
|
import { AuthJwtModel } from '../../dto/auth/auth.jwt.model';
|
||||||
|
import { UserCreateInput } from '../../dto/user/user.create.input';
|
||||||
|
import { AuthService } from '../../service/auth/auth.service';
|
||||||
|
import { UserCreateService } from '../../service/user/user.create.service';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class AuthRegisterResolver {
|
||||||
|
constructor(
|
||||||
|
private readonly createUser: UserCreateService,
|
||||||
|
private readonly auth: AuthService,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Mutation(() => AuthJwtModel)
|
||||||
|
async authRegister(
|
||||||
|
@Args({ name: 'user' }) data: UserCreateInput,
|
||||||
|
): Promise<AuthJwtModel> {
|
||||||
|
const user = await this.createUser.create(data)
|
||||||
|
|
||||||
|
return this.auth.login(user)
|
||||||
|
}
|
||||||
|
}
|
||||||
7
src/resolver/auth/index.ts
Normal file
7
src/resolver/auth/index.ts
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
import { AuthLoginResolver } from './auth.login.resolver';
|
||||||
|
import { AuthRegisterResolver } from './auth.register.resolver';
|
||||||
|
|
||||||
|
export const authServices = [
|
||||||
|
AuthRegisterResolver,
|
||||||
|
AuthLoginResolver,
|
||||||
|
]
|
||||||
28
src/resolver/context.cache.ts
Normal file
28
src/resolver/context.cache.ts
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
import { FormDocument } from '../schema/form.schema';
|
||||||
|
import { UserDocument } from '../schema/user.schema';
|
||||||
|
|
||||||
|
export class ContextCache {
|
||||||
|
private users: {
|
||||||
|
[id: string]: UserDocument,
|
||||||
|
} = {}
|
||||||
|
|
||||||
|
private forms: {
|
||||||
|
[id: string]: FormDocument,
|
||||||
|
} = {}
|
||||||
|
|
||||||
|
public addUser(user: UserDocument) {
|
||||||
|
this.users[user.id] = user;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async getUser(id: any): Promise<UserDocument> {
|
||||||
|
return this.users[id]
|
||||||
|
}
|
||||||
|
|
||||||
|
public addForm(form: FormDocument) {
|
||||||
|
this.forms[form.id] = form
|
||||||
|
}
|
||||||
|
|
||||||
|
public async getForm(id: any): Promise<FormDocument> {
|
||||||
|
return this.forms[id]
|
||||||
|
}
|
||||||
|
}
|
||||||
132
src/resolver/form/form.resolver.ts
Normal file
132
src/resolver/form/form.resolver.ts
Normal file
@ -0,0 +1,132 @@
|
|||||||
|
import { Args, Context, ID, Parent, Query, ResolveField, Resolver } from '@nestjs/graphql';
|
||||||
|
import { Roles } from '../../decorator/roles.decorator';
|
||||||
|
import { User } from '../../decorator/user.decorator';
|
||||||
|
import { DesignModel } from '../../dto/form/design.model';
|
||||||
|
import { FormModel } from '../../dto/form/form.model';
|
||||||
|
import { FormPageModel } from '../../dto/form/form.page.model';
|
||||||
|
import { RespondentNotificationsModel } from '../../dto/form/respondent.notifications.model';
|
||||||
|
import { SelfNotificationsModel } from '../../dto/form/self.notifications.model';
|
||||||
|
import { UserModel } from '../../dto/user/user.model';
|
||||||
|
import { UserDocument } from '../../schema/user.schema';
|
||||||
|
import { FormService } from '../../service/form/form.service';
|
||||||
|
import { ContextCache } from '../context.cache';
|
||||||
|
|
||||||
|
@Resolver(() => FormModel)
|
||||||
|
export class FormResolver {
|
||||||
|
constructor(
|
||||||
|
private readonly formService: FormService,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Query(() => FormModel)
|
||||||
|
async getFormById(
|
||||||
|
@User() user: UserDocument,
|
||||||
|
@Args('id', {type: () => ID}) id,
|
||||||
|
@Context('cache') cache: ContextCache,
|
||||||
|
) {
|
||||||
|
const form = await this.formService.findById(id)
|
||||||
|
|
||||||
|
if (!form.isLive && !await this.formService.isAdmin(form, user)) {
|
||||||
|
throw new Error('invalid form')
|
||||||
|
}
|
||||||
|
|
||||||
|
cache.addForm(form)
|
||||||
|
|
||||||
|
return new FormModel(form)
|
||||||
|
}
|
||||||
|
|
||||||
|
@ResolveField('isLive', () => Boolean)
|
||||||
|
@Roles('admin')
|
||||||
|
async getRoles(
|
||||||
|
@User() user: UserDocument,
|
||||||
|
@Parent() parent: FormModel,
|
||||||
|
@Context('cache') cache: ContextCache,
|
||||||
|
): Promise<boolean> {
|
||||||
|
const form = await cache.getForm(parent.id)
|
||||||
|
|
||||||
|
if (!await this.formService.isAdmin(form, user)) {
|
||||||
|
throw new Error('no access to field')
|
||||||
|
}
|
||||||
|
|
||||||
|
return form.isLive
|
||||||
|
}
|
||||||
|
|
||||||
|
@ResolveField('selfNotifications', () => SelfNotificationsModel)
|
||||||
|
@Roles('admin')
|
||||||
|
async getSelfNotifications(
|
||||||
|
@User() user: UserDocument,
|
||||||
|
@Parent() parent: FormModel,
|
||||||
|
@Context('cache') cache: ContextCache,
|
||||||
|
): Promise<SelfNotificationsModel> {
|
||||||
|
const form = await cache.getForm(parent.id)
|
||||||
|
|
||||||
|
if (!await this.formService.isAdmin(form, user)) {
|
||||||
|
throw new Error('no access to field')
|
||||||
|
}
|
||||||
|
|
||||||
|
return new SelfNotificationsModel(form.selfNotifications)
|
||||||
|
}
|
||||||
|
|
||||||
|
@ResolveField('respondentNotifications', () => SelfNotificationsModel)
|
||||||
|
@Roles('admin')
|
||||||
|
async getRespondentNotifications(
|
||||||
|
@User() user: UserDocument,
|
||||||
|
@Parent() parent: FormModel,
|
||||||
|
@Context('cache') cache: ContextCache,
|
||||||
|
): Promise<SelfNotificationsModel> {
|
||||||
|
const form = await cache.getForm(parent.id)
|
||||||
|
|
||||||
|
if (!await this.formService.isAdmin(form, user)) {
|
||||||
|
throw new Error('no access to field')
|
||||||
|
}
|
||||||
|
|
||||||
|
return new RespondentNotificationsModel(form.respondentNotifications)
|
||||||
|
}
|
||||||
|
|
||||||
|
@ResolveField('design', () => DesignModel)
|
||||||
|
async getDesign(
|
||||||
|
@User() user: UserDocument,
|
||||||
|
@Parent() parent: FormModel,
|
||||||
|
@Context('cache') cache: ContextCache,
|
||||||
|
): Promise<DesignModel> {
|
||||||
|
const form = await cache.getForm(parent.id)
|
||||||
|
|
||||||
|
return new DesignModel(form.design)
|
||||||
|
}
|
||||||
|
|
||||||
|
@ResolveField('startPage', () => FormPageModel)
|
||||||
|
async getStartPage(
|
||||||
|
@Parent() parent: FormModel,
|
||||||
|
@Context('cache') cache: ContextCache,
|
||||||
|
): Promise<FormPageModel> {
|
||||||
|
const form = await cache.getForm(parent.id)
|
||||||
|
|
||||||
|
return new FormPageModel(form.startPage)
|
||||||
|
}
|
||||||
|
|
||||||
|
@ResolveField('endPage', () => FormPageModel)
|
||||||
|
async getEndPage(
|
||||||
|
@Parent() parent: FormModel,
|
||||||
|
@Context('cache') cache: ContextCache,
|
||||||
|
): Promise<FormPageModel> {
|
||||||
|
const form = await cache.getForm(parent.id)
|
||||||
|
|
||||||
|
return new FormPageModel(form.endPage)
|
||||||
|
}
|
||||||
|
|
||||||
|
@ResolveField('admin', () => UserModel)
|
||||||
|
@Roles('superuser')
|
||||||
|
async getAdmin(
|
||||||
|
@Parent() parent: FormModel,
|
||||||
|
@Context('cache') cache: ContextCache,
|
||||||
|
): Promise<UserModel> {
|
||||||
|
const form = await cache.getForm(parent.id)
|
||||||
|
|
||||||
|
if (!form.populated('admin')) {
|
||||||
|
form.populate('admin')
|
||||||
|
await form.execPopulate()
|
||||||
|
}
|
||||||
|
|
||||||
|
return new UserModel(form.admin)
|
||||||
|
}
|
||||||
|
}
|
||||||
9
src/resolver/form/index.ts
Normal file
9
src/resolver/form/index.ts
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
import { FieldResolver } from './field.resolver';
|
||||||
|
import { FormCreateResolver } from './form.create.resolver';
|
||||||
|
import { FormResolver } from './form.resolver';
|
||||||
|
|
||||||
|
export const formResolvers = [
|
||||||
|
FormResolver,
|
||||||
|
FormCreateResolver,
|
||||||
|
FieldResolver,
|
||||||
|
]
|
||||||
13
src/resolver/index.ts
Normal file
13
src/resolver/index.ts
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import { authServices } from './auth';
|
||||||
|
import { formResolvers } from './form';
|
||||||
|
import { myResolvers } from './me';
|
||||||
|
import { StatusResolver } from './status.resolver';
|
||||||
|
import { userResolvers } from './user';
|
||||||
|
|
||||||
|
export const resolvers = [
|
||||||
|
StatusResolver,
|
||||||
|
...userResolvers,
|
||||||
|
...authServices,
|
||||||
|
...myResolvers,
|
||||||
|
...formResolvers,
|
||||||
|
]
|
||||||
5
src/resolver/me/index.ts
Normal file
5
src/resolver/me/index.ts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
import { ProfileResolver } from './profile.resolver';
|
||||||
|
|
||||||
|
export const myResolvers = [
|
||||||
|
ProfileResolver,
|
||||||
|
]
|
||||||
19
src/resolver/me/profile.resolver.ts
Normal file
19
src/resolver/me/profile.resolver.ts
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
import { Context, Query } from '@nestjs/graphql';
|
||||||
|
import { Roles } from '../../decorator/roles.decorator';
|
||||||
|
import { User } from '../../decorator/user.decorator';
|
||||||
|
import { OwnUserModel } from '../../dto/user/own.user.model';
|
||||||
|
import { UserDocument } from '../../schema/user.schema';
|
||||||
|
import { ContextCache } from '../context.cache';
|
||||||
|
|
||||||
|
export class ProfileResolver {
|
||||||
|
@Query(() => OwnUserModel)
|
||||||
|
@Roles('user')
|
||||||
|
async me(
|
||||||
|
@User() user: UserDocument,
|
||||||
|
@Context('cache') cache: ContextCache,
|
||||||
|
): Promise<OwnUserModel> {
|
||||||
|
cache.addUser(user)
|
||||||
|
|
||||||
|
return new OwnUserModel(user)
|
||||||
|
}
|
||||||
|
}
|
||||||
12
src/resolver/status.resolver.ts
Normal file
12
src/resolver/status.resolver.ts
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
import { Query, Resolver } from '@nestjs/graphql';
|
||||||
|
import { StatusModel } from '../dto/status.model';
|
||||||
|
|
||||||
|
@Resolver(() => StatusModel)
|
||||||
|
export class StatusResolver {
|
||||||
|
@Query(() => StatusModel)
|
||||||
|
async status(): Promise<StatusModel> {
|
||||||
|
return new StatusModel({
|
||||||
|
version: process.env.npm_package_version,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
5
src/resolver/user/index.ts
Normal file
5
src/resolver/user/index.ts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
import { UserResolver } from './user.resolver';
|
||||||
|
|
||||||
|
export const userResolvers = [
|
||||||
|
UserResolver,
|
||||||
|
]
|
||||||
36
src/resolver/user/user.resolver.ts
Normal file
36
src/resolver/user/user.resolver.ts
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
import { Args, Context, GraphQLExecutionContext, ID, Parent, Query, ResolveField, Resolver } from '@nestjs/graphql';
|
||||||
|
import { rolesType } from '../../config/roles';
|
||||||
|
import { Roles } from '../../decorator/roles.decorator';
|
||||||
|
import { UserModel } from '../../dto/user/user.model';
|
||||||
|
import { UserService } from '../../service/user/user.service';
|
||||||
|
import { ContextCache } from '../context.cache';
|
||||||
|
|
||||||
|
@Resolver(() => UserModel)
|
||||||
|
export class UserResolver {
|
||||||
|
constructor(
|
||||||
|
private readonly userService: UserService,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Query(() => UserModel)
|
||||||
|
@Roles('admin')
|
||||||
|
async getUserById(
|
||||||
|
@Args('id', {type: () => ID}) id,
|
||||||
|
@Context('cache') cache: ContextCache,
|
||||||
|
) {
|
||||||
|
const user = await this.userService.findById(id)
|
||||||
|
|
||||||
|
cache.addUser(user)
|
||||||
|
|
||||||
|
return new UserModel(user)
|
||||||
|
}
|
||||||
|
|
||||||
|
@ResolveField('roles', () => [String])
|
||||||
|
@Roles('superuser')
|
||||||
|
async getRoles(
|
||||||
|
@Parent() user: UserModel,
|
||||||
|
@Context('cache') cache: ContextCache,
|
||||||
|
): Promise<string[]> {
|
||||||
|
return (await cache.getUser(user.id)).roles
|
||||||
|
}
|
||||||
|
}
|
||||||
33
src/schema/button.schema.ts
Normal file
33
src/schema/button.schema.ts
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
import { Schema, Document } from 'mongoose';
|
||||||
|
import { matchType } from '../config/fields';
|
||||||
|
|
||||||
|
export interface ButtonDocument extends Document{
|
||||||
|
readonly url?: string
|
||||||
|
readonly action?: string
|
||||||
|
readonly text?: string
|
||||||
|
readonly bgColor?: string
|
||||||
|
readonly color?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ButtonSchema = new Schema({
|
||||||
|
url: {
|
||||||
|
type: String,
|
||||||
|
match: matchType.url,
|
||||||
|
},
|
||||||
|
action: {
|
||||||
|
type: String,
|
||||||
|
},
|
||||||
|
text: {
|
||||||
|
type: String,
|
||||||
|
},
|
||||||
|
bgColor: {
|
||||||
|
type: String,
|
||||||
|
match: matchType.color,
|
||||||
|
default: '#5bc0de',
|
||||||
|
},
|
||||||
|
color: {
|
||||||
|
type: String,
|
||||||
|
match: matchType.color,
|
||||||
|
default: '#ffffff'
|
||||||
|
},
|
||||||
|
})
|
||||||
17
src/schema/embedded/field.option.ts
Normal file
17
src/schema/embedded/field.option.ts
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
import { SchemaDefinition } from 'mongoose';
|
||||||
|
|
||||||
|
export const FieldOption: SchemaDefinition = {
|
||||||
|
id: {
|
||||||
|
alias: 'option_id',
|
||||||
|
type: Number,
|
||||||
|
},
|
||||||
|
title: {
|
||||||
|
alias: 'option_title',
|
||||||
|
type: String,
|
||||||
|
},
|
||||||
|
value: {
|
||||||
|
alias: 'option_value',
|
||||||
|
type: String,
|
||||||
|
trim: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
37
src/schema/embedded/logic.jump.ts
Normal file
37
src/schema/embedded/logic.jump.ts
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
import { Schema, SchemaDefinition } from 'mongoose';
|
||||||
|
import { FieldSchemaName } from '../field.schema';
|
||||||
|
|
||||||
|
export const LogicJump: SchemaDefinition = {
|
||||||
|
expressionString: {
|
||||||
|
type: String,
|
||||||
|
enum: [
|
||||||
|
'field == static',
|
||||||
|
'field != static',
|
||||||
|
'field > static',
|
||||||
|
'field >= static',
|
||||||
|
'field <= static',
|
||||||
|
'field < static',
|
||||||
|
'field contains static',
|
||||||
|
'field !contains static',
|
||||||
|
'field begins static',
|
||||||
|
'field !begins static',
|
||||||
|
'field ends static',
|
||||||
|
'field !ends static',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
fieldA: {
|
||||||
|
type: Schema.Types.ObjectId,
|
||||||
|
ref: FieldSchemaName
|
||||||
|
},
|
||||||
|
valueB: {
|
||||||
|
type: String,
|
||||||
|
},
|
||||||
|
jumpTo: {
|
||||||
|
type: Schema.Types.ObjectId,
|
||||||
|
ref: FieldSchemaName
|
||||||
|
},
|
||||||
|
enabled: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
32
src/schema/embedded/rating.field.ts
Normal file
32
src/schema/embedded/rating.field.ts
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
import { SchemaDefinition } from 'mongoose';
|
||||||
|
|
||||||
|
export const RatingField: SchemaDefinition = {
|
||||||
|
steps: {
|
||||||
|
type: Number,
|
||||||
|
min: 1,
|
||||||
|
max: 10,
|
||||||
|
},
|
||||||
|
shape: {
|
||||||
|
type: String,
|
||||||
|
enum: [
|
||||||
|
'Heart',
|
||||||
|
'Star',
|
||||||
|
'thumbs-up',
|
||||||
|
'thumbs-down',
|
||||||
|
'Circle',
|
||||||
|
'Square',
|
||||||
|
'Check Circle',
|
||||||
|
'Smile Outlined',
|
||||||
|
'Hourglass',
|
||||||
|
'bell',
|
||||||
|
'Paper Plane',
|
||||||
|
'Comment',
|
||||||
|
'Trash',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
validShapes: {
|
||||||
|
type: [{
|
||||||
|
type: String,
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
}
|
||||||
58
src/schema/field.schema.ts
Normal file
58
src/schema/field.schema.ts
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
import { Document, Schema } from 'mongoose';
|
||||||
|
import { fieldTypes } from '../config/fields';
|
||||||
|
import { FieldOption } from './embedded/field.option';
|
||||||
|
import { LogicJump } from './embedded/logic.jump';
|
||||||
|
import { RatingField } from './embedded/rating.field';
|
||||||
|
|
||||||
|
export const FieldSchemaName = 'FormField'
|
||||||
|
|
||||||
|
export interface FieldDocument extends Document {
|
||||||
|
isSubmission: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export const FieldSchema = new Schema({
|
||||||
|
isSubmission: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
title: {
|
||||||
|
type: String,
|
||||||
|
trim: true,
|
||||||
|
},
|
||||||
|
description: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
logicJump: {
|
||||||
|
type: LogicJump,
|
||||||
|
},
|
||||||
|
ratingOptions: {
|
||||||
|
type: RatingField,
|
||||||
|
},
|
||||||
|
fieldOptions: {
|
||||||
|
type: [FieldOption],
|
||||||
|
},
|
||||||
|
required: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
|
disabled: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
deletePreserved: { // TODO remove
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
validFieldTypes: { // TODO remove
|
||||||
|
type: [String],
|
||||||
|
},
|
||||||
|
fieldType: {
|
||||||
|
type: String,
|
||||||
|
enum: fieldTypes,
|
||||||
|
},
|
||||||
|
fieldValue: {
|
||||||
|
type: Schema.Types.Mixed,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
})
|
||||||
249
src/schema/form.schema.ts
Normal file
249
src/schema/form.schema.ts
Normal file
@ -0,0 +1,249 @@
|
|||||||
|
import exp from 'constants';
|
||||||
|
import { Document, Schema } from 'mongoose';
|
||||||
|
import { matchType } from '../config/fields';
|
||||||
|
import { defaultLanguage, languages } from '../config/languages';
|
||||||
|
import { rolesType } from '../config/roles';
|
||||||
|
import { ButtonDocument, ButtonSchema } from './button.schema';
|
||||||
|
import { FieldDocument, FieldSchema } from './field.schema';
|
||||||
|
import { VisitorDataDocument, VisitorDataSchema } from './visitor.data.schema';
|
||||||
|
import { UserDocument, UserSchemaName } from './user.schema';
|
||||||
|
|
||||||
|
export const FormSchemaName = 'Form'
|
||||||
|
|
||||||
|
export interface FormPage {
|
||||||
|
readonly show: boolean
|
||||||
|
readonly title?: string
|
||||||
|
readonly paragraph?: string
|
||||||
|
readonly buttonText?: string
|
||||||
|
readonly buttons: [ButtonDocument]
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Notifications {
|
||||||
|
readonly subject?: string
|
||||||
|
readonly htmlTemplate?: string
|
||||||
|
readonly enabled: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SelfNotifications extends Notifications{
|
||||||
|
readonly fromField?: string
|
||||||
|
readonly toEmail?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RespondentNotifications extends Notifications{
|
||||||
|
readonly toField?: string
|
||||||
|
readonly fromEmail?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Colors {
|
||||||
|
readonly backgroundColor: string
|
||||||
|
readonly questionColor: string
|
||||||
|
readonly answerColor: string
|
||||||
|
readonly buttonColor: string
|
||||||
|
readonly buttonTextColor: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Design {
|
||||||
|
readonly colors: Colors
|
||||||
|
|
||||||
|
readonly font?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FormDocument extends Document {
|
||||||
|
readonly title: string
|
||||||
|
readonly language: string
|
||||||
|
|
||||||
|
readonly analytics: {
|
||||||
|
readonly gaCode?: string
|
||||||
|
// TODO extract to separate documents!
|
||||||
|
readonly visitors: [VisitorDataDocument]
|
||||||
|
}
|
||||||
|
|
||||||
|
readonly fields: [FieldDocument]
|
||||||
|
|
||||||
|
readonly admin: UserDocument
|
||||||
|
|
||||||
|
readonly startPage: FormPage;
|
||||||
|
readonly endPage: FormPage;
|
||||||
|
|
||||||
|
readonly selfNotifications: SelfNotifications;
|
||||||
|
readonly respondentNotifications: RespondentNotifications;
|
||||||
|
|
||||||
|
readonly showFooter: boolean;
|
||||||
|
readonly isLive: boolean;
|
||||||
|
readonly design: Design;
|
||||||
|
|
||||||
|
|
||||||
|
readonly created: Date
|
||||||
|
readonly lastModified: Date
|
||||||
|
}
|
||||||
|
|
||||||
|
export const FormSchema = new Schema({
|
||||||
|
title: {
|
||||||
|
trim: true,
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
created: {
|
||||||
|
type: Date,
|
||||||
|
},
|
||||||
|
lastModified: {
|
||||||
|
type: Date,
|
||||||
|
},
|
||||||
|
language: {
|
||||||
|
type: String,
|
||||||
|
enum: languages,
|
||||||
|
default: defaultLanguage,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
analytics: {
|
||||||
|
gaCode: {
|
||||||
|
type: String,
|
||||||
|
},
|
||||||
|
visitors: {
|
||||||
|
type: [VisitorDataSchema],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
fields: {
|
||||||
|
alias: 'form_fields',
|
||||||
|
type: [FieldSchema],
|
||||||
|
default: [],
|
||||||
|
},
|
||||||
|
admin: {
|
||||||
|
type: Schema.Types.ObjectId,
|
||||||
|
ref: UserSchemaName,
|
||||||
|
},
|
||||||
|
startPage: {
|
||||||
|
show: {
|
||||||
|
alias: 'showStart',
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
title: {
|
||||||
|
alias: 'introTitle',
|
||||||
|
type: String,
|
||||||
|
default: 'Welcome to Form',
|
||||||
|
},
|
||||||
|
paragraph: {
|
||||||
|
alias: 'introParagraph',
|
||||||
|
type: String,
|
||||||
|
default: 'Start',
|
||||||
|
},
|
||||||
|
buttons: {
|
||||||
|
type: [ButtonSchema],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
endPage: {
|
||||||
|
show: {
|
||||||
|
alias: 'showEnd',
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
title: {
|
||||||
|
type: String,
|
||||||
|
default: 'Thank you for filling out the form',
|
||||||
|
},
|
||||||
|
paragraph: {
|
||||||
|
type: String,
|
||||||
|
},
|
||||||
|
buttonText: {
|
||||||
|
type: String,
|
||||||
|
default: 'Go back to Form'
|
||||||
|
},
|
||||||
|
buttons: {
|
||||||
|
type: [ButtonSchema],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
selfNotifications: {
|
||||||
|
fromField: {
|
||||||
|
type: String,
|
||||||
|
},
|
||||||
|
toEmail: {
|
||||||
|
alias: 'toEmails',
|
||||||
|
type: String,
|
||||||
|
},
|
||||||
|
subject: {
|
||||||
|
type: String,
|
||||||
|
},
|
||||||
|
htmlTemplate: {
|
||||||
|
type: String,
|
||||||
|
},
|
||||||
|
enabled: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
},
|
||||||
|
respondentNotifications: {
|
||||||
|
toField: {
|
||||||
|
type: String,
|
||||||
|
},
|
||||||
|
fromEmail: {
|
||||||
|
alias: 'fromEmails',
|
||||||
|
type: String,
|
||||||
|
match: matchType.email,
|
||||||
|
},
|
||||||
|
subject: {
|
||||||
|
type: String,
|
||||||
|
default: 'OhMyForm: Thank you for filling out this OhMyForm',
|
||||||
|
},
|
||||||
|
htmlTemplate: {
|
||||||
|
type: String,
|
||||||
|
default: 'Hello, <br><br> We’ve received your submission. <br><br> Thank you & have a nice day!',
|
||||||
|
},
|
||||||
|
enabled: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
showFooter: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
|
isLive: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
|
design: {
|
||||||
|
colors: {
|
||||||
|
backgroundColor: {
|
||||||
|
type: String,
|
||||||
|
match: matchType.color,
|
||||||
|
default: '#fff'
|
||||||
|
},
|
||||||
|
questionColor: {
|
||||||
|
type: String,
|
||||||
|
match: matchType.color,
|
||||||
|
default: '#333'
|
||||||
|
},
|
||||||
|
answerColor: {
|
||||||
|
type: String,
|
||||||
|
match: matchType.color,
|
||||||
|
default: '#333'
|
||||||
|
},
|
||||||
|
buttonColor: {
|
||||||
|
type: String,
|
||||||
|
match: matchType.color,
|
||||||
|
default: '#fff'
|
||||||
|
},
|
||||||
|
buttonTextColor: {
|
||||||
|
type: String,
|
||||||
|
match: matchType.color,
|
||||||
|
default: '#333'
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
font: {
|
||||||
|
type: String,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}, {
|
||||||
|
timestamps: {
|
||||||
|
createdAt: 'created',
|
||||||
|
updatedAt: 'lastModified',
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
export const FormDefinition = {
|
||||||
|
name: FormSchemaName,
|
||||||
|
schema: FormSchema,
|
||||||
|
}
|
||||||
|
|
||||||
57
src/schema/form.submission.schema.ts
Normal file
57
src/schema/form.submission.schema.ts
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
import { Document, Schema } from 'mongoose';
|
||||||
|
import { FieldSchema } from './field.schema';
|
||||||
|
import { FormSchemaName } from './form.schema';
|
||||||
|
|
||||||
|
export const FormSubmissionSchemaName = 'FormSubmission'
|
||||||
|
|
||||||
|
export interface FormSubmissionDocument extends Document {
|
||||||
|
}
|
||||||
|
|
||||||
|
export const FormSubmissionSchema = new Schema({
|
||||||
|
fields: {
|
||||||
|
alias: 'form_fields',
|
||||||
|
type: [FieldSchema],
|
||||||
|
default: [],
|
||||||
|
},
|
||||||
|
form: {
|
||||||
|
type: Schema.Types.ObjectId,
|
||||||
|
ref: FormSchemaName,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
ipAddr: {
|
||||||
|
type: String
|
||||||
|
},
|
||||||
|
geoLocation: {
|
||||||
|
Country: {
|
||||||
|
type: String
|
||||||
|
},
|
||||||
|
City: {
|
||||||
|
type: String
|
||||||
|
}
|
||||||
|
},
|
||||||
|
device: {
|
||||||
|
type: {
|
||||||
|
type: String
|
||||||
|
},
|
||||||
|
name: {
|
||||||
|
type: String
|
||||||
|
}
|
||||||
|
},
|
||||||
|
timeElapsed: {
|
||||||
|
type: Number
|
||||||
|
},
|
||||||
|
percentageComplete: {
|
||||||
|
type: Number
|
||||||
|
},
|
||||||
|
}, {
|
||||||
|
timestamps: {
|
||||||
|
createdAt: 'created',
|
||||||
|
updatedAt: 'lastModified',
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
export const FormSubmissionDefinition = {
|
||||||
|
name: FormSubmissionSchemaName,
|
||||||
|
schema: FormSubmissionSchema,
|
||||||
|
}
|
||||||
|
|
||||||
9
src/schema/index.ts
Normal file
9
src/schema/index.ts
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
import { FormDefinition } from './form.schema';
|
||||||
|
import { FormSubmissionDefinition, FormSubmissionSchema } from './form.submission.schema';
|
||||||
|
import { UserDefinition } from './user.schema';
|
||||||
|
|
||||||
|
export const schema = [
|
||||||
|
FormDefinition,
|
||||||
|
FormSubmissionDefinition,
|
||||||
|
UserDefinition,
|
||||||
|
]
|
||||||
99
src/schema/user.schema.ts
Normal file
99
src/schema/user.schema.ts
Normal file
@ -0,0 +1,99 @@
|
|||||||
|
import { Document, Schema } from 'mongoose';
|
||||||
|
import { defaultLanguage, languages } from '../config/languages';
|
||||||
|
import { roles, rolesType } from '../config/roles';
|
||||||
|
|
||||||
|
export const UserSchemaName = 'User'
|
||||||
|
|
||||||
|
export interface UserDocument extends Document {
|
||||||
|
readonly firstName?: string
|
||||||
|
readonly lastName?: string
|
||||||
|
readonly email: string
|
||||||
|
readonly username: string
|
||||||
|
readonly passwordHash: string
|
||||||
|
readonly salt: string
|
||||||
|
readonly provider: string
|
||||||
|
readonly roles: rolesType
|
||||||
|
readonly language: string
|
||||||
|
readonly resetPasswordToken?: string
|
||||||
|
readonly resetPasswordExpires?: Date
|
||||||
|
readonly token?: string
|
||||||
|
readonly apiKey?: string
|
||||||
|
readonly created: Date
|
||||||
|
readonly lastModified: Date
|
||||||
|
}
|
||||||
|
|
||||||
|
export const UserSchema = new Schema({
|
||||||
|
firstName: {
|
||||||
|
type: String,
|
||||||
|
trim: true,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
lastName: {
|
||||||
|
type: String,
|
||||||
|
trim: true,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
email: {
|
||||||
|
type: String,
|
||||||
|
trim: true,
|
||||||
|
lowercase: true,
|
||||||
|
unique: true,
|
||||||
|
match: /^(([^<>()\[\]\\.,;:\s@']+(\.[^<>()\[\]\\.,;:\s@']+)*)|('.+'))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
username: {
|
||||||
|
type: String,
|
||||||
|
unique: true,
|
||||||
|
lowercase: true,
|
||||||
|
match: /^[a-zA-Z0-9\-]+$/,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
passwordHash: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
salt: {
|
||||||
|
type: String,
|
||||||
|
},
|
||||||
|
provider: {
|
||||||
|
type: String,
|
||||||
|
default: 'local'
|
||||||
|
},
|
||||||
|
roles: {
|
||||||
|
type: [{
|
||||||
|
type: String,
|
||||||
|
enum: roles,
|
||||||
|
}],
|
||||||
|
default: ['user'],
|
||||||
|
},
|
||||||
|
language: {
|
||||||
|
type: String,
|
||||||
|
enum: languages,
|
||||||
|
default: defaultLanguage,
|
||||||
|
},
|
||||||
|
resetPasswordToken: {
|
||||||
|
type: String,
|
||||||
|
},
|
||||||
|
resetPasswordExpires: {
|
||||||
|
type: Date,
|
||||||
|
},
|
||||||
|
token: {
|
||||||
|
type: String,
|
||||||
|
},
|
||||||
|
apiKey: {
|
||||||
|
type: String,
|
||||||
|
unique: true,
|
||||||
|
index: true,
|
||||||
|
sparse: true
|
||||||
|
},
|
||||||
|
}, {
|
||||||
|
timestamps: {
|
||||||
|
createdAt: 'created',
|
||||||
|
updatedAt: 'lastModified',
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
export const UserDefinition = {
|
||||||
|
name: UserSchemaName,
|
||||||
|
schema: UserSchema,
|
||||||
|
}
|
||||||
52
src/schema/visitor.data.schema.ts
Normal file
52
src/schema/visitor.data.schema.ts
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
import { Document, Schema } from 'mongoose';
|
||||||
|
import { defaultLanguage, languages } from '../config/languages';
|
||||||
|
import { FieldDocument, FieldSchemaName } from './field.schema';
|
||||||
|
|
||||||
|
export interface VisitorDataDocument extends Document {
|
||||||
|
readonly introParagraph?: string
|
||||||
|
readonly referrer?: string
|
||||||
|
readonly filledOutFields: [FieldDocument]
|
||||||
|
readonly timeElapsed: number
|
||||||
|
readonly isSubmitted: boolean
|
||||||
|
readonly language: string
|
||||||
|
readonly ipAddr: string
|
||||||
|
readonly deviceType: string
|
||||||
|
readonly userAgent: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export const VisitorDataSchema = new Schema({
|
||||||
|
introParagraph: {
|
||||||
|
type: String,
|
||||||
|
},
|
||||||
|
referrer: {
|
||||||
|
type: String,
|
||||||
|
},
|
||||||
|
filledOutFields: {
|
||||||
|
type: [{
|
||||||
|
type: Schema.Types.ObjectId,
|
||||||
|
ref: FieldSchemaName,
|
||||||
|
}],
|
||||||
|
},
|
||||||
|
timeElapsed: {
|
||||||
|
type: Number,
|
||||||
|
},
|
||||||
|
isSubmitted: {
|
||||||
|
type: Boolean,
|
||||||
|
},
|
||||||
|
language: {
|
||||||
|
type: String,
|
||||||
|
enum: languages,
|
||||||
|
default: defaultLanguage,
|
||||||
|
},
|
||||||
|
ipAddr: {
|
||||||
|
type: String,
|
||||||
|
},
|
||||||
|
deviceType: {
|
||||||
|
type: String,
|
||||||
|
enum: ['desktop', 'phone', 'tablet', 'other'],
|
||||||
|
default: 'other',
|
||||||
|
},
|
||||||
|
userAgent: {
|
||||||
|
type: String,
|
||||||
|
},
|
||||||
|
})
|
||||||
40
src/service/auth/auth.service.ts
Normal file
40
src/service/auth/auth.service.ts
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { JwtService } from '@nestjs/jwt';
|
||||||
|
import { AuthJwtModel } from '../../dto/auth/auth.jwt.model';
|
||||||
|
import { UserDocument } from '../../schema/user.schema';
|
||||||
|
import { UserService } from '../user/user.service';
|
||||||
|
import { PasswordService } from './password.service';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class AuthService {
|
||||||
|
constructor(
|
||||||
|
private userService: UserService,
|
||||||
|
private jwtService: JwtService,
|
||||||
|
private passwordService: PasswordService,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async validateUser(username: string, password: string): Promise<UserDocument> {
|
||||||
|
console.log('check user??', username)
|
||||||
|
|
||||||
|
const user = await this.userService.findByUsername(username);
|
||||||
|
if (user && await this.passwordService.verify(password, user.passwordHash, user.salt)) {
|
||||||
|
return user;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async login(user: UserDocument): Promise<AuthJwtModel> {
|
||||||
|
return new AuthJwtModel({
|
||||||
|
accessToken: this.jwtService.sign({
|
||||||
|
username: user.username,
|
||||||
|
roles: user.roles,
|
||||||
|
sub: user.id,
|
||||||
|
}),
|
||||||
|
refreshToken: this.jwtService.sign({
|
||||||
|
sub: user.id,
|
||||||
|
}, {
|
||||||
|
expiresIn: '30d',
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
11
src/service/auth/index.ts
Normal file
11
src/service/auth/index.ts
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
import { AuthService } from './auth.service';
|
||||||
|
import { JwtStrategy } from './jwt.strategy';
|
||||||
|
import { LocalStrategy } from './local.strategy';
|
||||||
|
import { PasswordService } from './password.service';
|
||||||
|
|
||||||
|
export const authServices = [
|
||||||
|
AuthService,
|
||||||
|
LocalStrategy,
|
||||||
|
PasswordService,
|
||||||
|
JwtStrategy,
|
||||||
|
]
|
||||||
30
src/service/auth/jwt.strategy.ts
Normal file
30
src/service/auth/jwt.strategy.ts
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { ConfigService } from '@nestjs/config';
|
||||||
|
import { PassportStrategy } from '@nestjs/passport';
|
||||||
|
import { ExtractJwt, Strategy } from 'passport-jwt';
|
||||||
|
import { UserDocument } from '../../schema/user.schema';
|
||||||
|
import { UserService } from '../user/user.service';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class JwtStrategy extends PassportStrategy(Strategy) {
|
||||||
|
constructor(
|
||||||
|
configService: ConfigService,
|
||||||
|
private readonly userService: UserService,
|
||||||
|
) {
|
||||||
|
super({
|
||||||
|
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
|
||||||
|
ignoreExpiration: false,
|
||||||
|
secretOrKey: configService.get<string>('AUTH_SECRET'),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async validate(payload: any): Promise<UserDocument> {
|
||||||
|
try {
|
||||||
|
return await this.userService.findById(payload.sub)
|
||||||
|
} catch (e) {
|
||||||
|
// log error
|
||||||
|
}
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
19
src/service/auth/local.strategy.ts
Normal file
19
src/service/auth/local.strategy.ts
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
import { Injectable, UnauthorizedException } from '@nestjs/common';
|
||||||
|
import { PassportStrategy } from '@nestjs/passport';
|
||||||
|
import { Strategy } from 'passport-local';
|
||||||
|
import { AuthService } from './auth.service';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class LocalStrategy extends PassportStrategy(Strategy) {
|
||||||
|
constructor(private authService: AuthService) {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
async validate(username: string, password: string): Promise<any> {
|
||||||
|
const user = await this.authService.validateUser(username, password);
|
||||||
|
if (!user) {
|
||||||
|
throw new UnauthorizedException();
|
||||||
|
}
|
||||||
|
return user;
|
||||||
|
}
|
||||||
|
}
|
||||||
29
src/service/auth/password.service.ts
Normal file
29
src/service/auth/password.service.ts
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import * as bcrypt from 'bcrypt';
|
||||||
|
import * as crypto from 'crypto';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class PasswordService {
|
||||||
|
async verify (password: string, hash: string, salt?: string): Promise<boolean> {
|
||||||
|
if (hash[0] === '$') {
|
||||||
|
return await bcrypt.compare(password, hash)
|
||||||
|
}
|
||||||
|
|
||||||
|
//Generate salt if it doesn't exist yet
|
||||||
|
if(!salt){
|
||||||
|
salt = crypto.randomBytes(64).toString('base64');
|
||||||
|
}
|
||||||
|
|
||||||
|
return hash === crypto.pbkdf2Sync(
|
||||||
|
password,
|
||||||
|
new Buffer(salt, 'base64'),
|
||||||
|
10000,
|
||||||
|
128,
|
||||||
|
'SHA1'
|
||||||
|
).toString('base64');
|
||||||
|
}
|
||||||
|
|
||||||
|
hash (password): Promise<string> {
|
||||||
|
return bcrypt.hash(password, 4)
|
||||||
|
}
|
||||||
|
}
|
||||||
13
src/service/form/form.create.service.ts
Normal file
13
src/service/form/form.create.service.ts
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { InjectModel } from '@nestjs/mongoose';
|
||||||
|
import { Model } from "mongoose";
|
||||||
|
import { FormDocument, FormSchemaName } from '../../schema/form.schema';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class FormCreateService {
|
||||||
|
constructor(
|
||||||
|
@InjectModel(FormSchemaName) private formModel: Model<FormDocument>,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
32
src/service/form/form.service.ts
Normal file
32
src/service/form/form.service.ts
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { InjectModel } from '@nestjs/mongoose';
|
||||||
|
import { GraphQLError } from 'graphql';
|
||||||
|
import { Model, Types } from 'mongoose';
|
||||||
|
import { FormDocument, FormSchemaName } from '../../schema/form.schema';
|
||||||
|
import { UserDocument } from '../../schema/user.schema';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class FormService {
|
||||||
|
constructor(
|
||||||
|
@InjectModel(FormSchemaName) private formModel: Model<FormDocument>,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
async isAdmin(form: FormDocument, user: UserDocument): Promise<boolean> {
|
||||||
|
if (user.roles.includes('superuser')) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return Types.ObjectId(form.admin.id).equals(Types.ObjectId(user.id))
|
||||||
|
}
|
||||||
|
|
||||||
|
async findById(id: string): Promise<FormDocument> {
|
||||||
|
const form = await this.formModel.findById(id);
|
||||||
|
|
||||||
|
if (!form) {
|
||||||
|
throw new Error('no form found')
|
||||||
|
}
|
||||||
|
|
||||||
|
return form
|
||||||
|
}
|
||||||
|
}
|
||||||
7
src/service/form/index.ts
Normal file
7
src/service/form/index.ts
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
import { FormCreateService } from './form.create.service';
|
||||||
|
import { FormService } from './form.service';
|
||||||
|
|
||||||
|
export const formServices = [
|
||||||
|
FormService,
|
||||||
|
FormCreateService,
|
||||||
|
]
|
||||||
11
src/service/index.ts
Normal file
11
src/service/index.ts
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
import { authServices } from './auth';
|
||||||
|
import { formServices } from './form';
|
||||||
|
import { MailService } from './mail.service';
|
||||||
|
import { userServices } from './user';
|
||||||
|
|
||||||
|
export const services = [
|
||||||
|
...userServices,
|
||||||
|
...formServices,
|
||||||
|
...authServices,
|
||||||
|
MailService,
|
||||||
|
]
|
||||||
65
src/service/mail.service.ts
Normal file
65
src/service/mail.service.ts
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
import { MailerService } from '@nestjs-modules/mailer';
|
||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { ConfigService } from '@nestjs/config';
|
||||||
|
import fs from 'fs';
|
||||||
|
import handlebars from 'handlebars';
|
||||||
|
import { PinoLogger } from 'nestjs-pino/dist';
|
||||||
|
import { join } from 'path';
|
||||||
|
import { defaultLanguage } from '../config/languages';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class MailService {
|
||||||
|
constructor(
|
||||||
|
private readonly nestMailer: MailerService,
|
||||||
|
private readonly configService: ConfigService,
|
||||||
|
private readonly logger: PinoLogger,
|
||||||
|
) {
|
||||||
|
logger.setContext(this.constructor.name)
|
||||||
|
}
|
||||||
|
|
||||||
|
async send(to: string, template: string, context: { [key: string]: any }, language: string = defaultLanguage): Promise<boolean> {
|
||||||
|
this.logger.info({
|
||||||
|
email: to,
|
||||||
|
}, `send email ${template}`)
|
||||||
|
try {
|
||||||
|
const path = this.getTemplatePath(template, language)
|
||||||
|
|
||||||
|
const process = (file: string) => {
|
||||||
|
const content = fs.readFileSync(join(path, file))
|
||||||
|
const template = handlebars.compile(content.toString('UTF-8'))
|
||||||
|
|
||||||
|
return template(context)
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.nestMailer.sendMail({
|
||||||
|
to,
|
||||||
|
subject: process('subject.txt'),
|
||||||
|
html: process('body.html'),
|
||||||
|
text: process('body.txt'),
|
||||||
|
})
|
||||||
|
this.logger.info('sent email')
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error({
|
||||||
|
error: error.message,
|
||||||
|
email: to,
|
||||||
|
}, `failed to send email ${template}`)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
private getTemplatePath(template: string, language: string): string {
|
||||||
|
let templatePath = join(this.configService.get<string>('LOCALES_PATH'), language, 'mail', template)
|
||||||
|
|
||||||
|
if (!fs.existsSync(templatePath)) {
|
||||||
|
templatePath = join(this.configService.get<string>('LOCALES_PATH'), 'en', 'mail', template)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!fs.existsSync(templatePath)) {
|
||||||
|
throw new Error('invalid template')
|
||||||
|
}
|
||||||
|
|
||||||
|
return templatePath
|
||||||
|
}
|
||||||
|
}
|
||||||
7
src/service/user/index.ts
Normal file
7
src/service/user/index.ts
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
import { UserCreateService } from './user.create.service';
|
||||||
|
import { UserService } from './user.service';
|
||||||
|
|
||||||
|
export const userServices = [
|
||||||
|
UserCreateService,
|
||||||
|
UserService,
|
||||||
|
]
|
||||||
47
src/service/user/user.create.service.ts
Normal file
47
src/service/user/user.create.service.ts
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { InjectModel } from '@nestjs/mongoose';
|
||||||
|
import { Model } from 'mongoose';
|
||||||
|
import { PinoLogger } from 'nestjs-pino/dist';
|
||||||
|
import { UserCreateInput } from '../../dto/user/user.create.input';
|
||||||
|
import { UserDocument, UserSchemaName } from '../../schema/user.schema';
|
||||||
|
import { PasswordService } from '../auth/password.service';
|
||||||
|
import { MailService } from '../mail.service';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class UserCreateService {
|
||||||
|
constructor(
|
||||||
|
@InjectModel(UserSchemaName) private userModel: Model<UserDocument>,
|
||||||
|
private readonly mailerService: MailService,
|
||||||
|
private readonly logger: PinoLogger,
|
||||||
|
private readonly passwordService: PasswordService,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async create(user: UserCreateInput): Promise<UserDocument> {
|
||||||
|
// TODO check for uniqueness of email & username!
|
||||||
|
|
||||||
|
const entry = new this.userModel({
|
||||||
|
...user,
|
||||||
|
passwordHash: await this.passwordService.hash(user.password),
|
||||||
|
})
|
||||||
|
|
||||||
|
await entry.save({
|
||||||
|
validateBeforeSave: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
const sent = await this.mailerService.send(
|
||||||
|
entry.email,
|
||||||
|
'user/created',
|
||||||
|
{
|
||||||
|
username: entry.username,
|
||||||
|
confirm: 'https://www.google.com', // TODO confirm url
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
// so send email
|
||||||
|
if (!sent) {
|
||||||
|
this.logger.warn('failed to send email for user creation')
|
||||||
|
}
|
||||||
|
|
||||||
|
return entry
|
||||||
|
}
|
||||||
|
}
|
||||||
35
src/service/user/user.service.ts
Normal file
35
src/service/user/user.service.ts
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { InjectModel } from '@nestjs/mongoose';
|
||||||
|
import { GraphQLError } from 'graphql';
|
||||||
|
import { Model } from 'mongoose';
|
||||||
|
import { UserDocument, UserSchemaName } from '../../schema/user.schema';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class UserService {
|
||||||
|
constructor(
|
||||||
|
@InjectModel(UserSchemaName) private userModel: Model<UserDocument>,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
async findById(id: string): Promise<UserDocument> {
|
||||||
|
const user = await this.userModel.findById(id);
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
throw new Error('no user found')
|
||||||
|
}
|
||||||
|
|
||||||
|
return user
|
||||||
|
}
|
||||||
|
|
||||||
|
async findByUsername(username: string): Promise<UserDocument> {
|
||||||
|
const user = await this.userModel.findOne({
|
||||||
|
username,
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
throw new Error('no user found')
|
||||||
|
}
|
||||||
|
|
||||||
|
return user
|
||||||
|
}
|
||||||
|
}
|
||||||
4
tsconfig.build.json
Normal file
4
tsconfig.build.json
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"extends": "./tsconfig.json",
|
||||||
|
"exclude": ["node_modules", "test", "dist", "**/*spec.ts"]
|
||||||
|
}
|
||||||
21
tsconfig.json
Normal file
21
tsconfig.json
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"module": "commonjs",
|
||||||
|
"declaration": true,
|
||||||
|
"removeComments": true,
|
||||||
|
"emitDecoratorMetadata": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"experimentalDecorators": true,
|
||||||
|
"target": "es2017",
|
||||||
|
"sourceMap": true,
|
||||||
|
"outDir": "./dist",
|
||||||
|
"baseUrl": "./",
|
||||||
|
"incremental": true,
|
||||||
|
"allowSyntheticDefaultImports": true
|
||||||
|
},
|
||||||
|
"include": ["src/**/*"],
|
||||||
|
"exclude": [
|
||||||
|
"node_modules",
|
||||||
|
"dist"
|
||||||
|
]
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user