Merge pull request #29 from wodka/features/api_next
Implement existing API with nestjs
This commit is contained in:
commit
56a6565427
2
api/.gitignore
vendored
Normal file
2
api/.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
/node_modules
|
||||
/yarn.lock
|
||||
4
api/.prettierrc
Normal file
4
api/.prettierrc
Normal file
@ -0,0 +1,4 @@
|
||||
{
|
||||
"singleQuote": true,
|
||||
"trailingComma": "all"
|
||||
}
|
||||
17
api/Dockerfile
Normal file
17
api/Dockerfile
Normal file
@ -0,0 +1,17 @@
|
||||
FROM node:10 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:10-alpine
|
||||
|
||||
COPY --from=builder /usr/src/app /usr/src/app
|
||||
|
||||
EXPOSE 3000
|
||||
CMD [ "yarn", "start:prod" ]
|
||||
55
api/README.md
Normal file
55
api/README.md
Normal file
@ -0,0 +1,55 @@
|
||||
<p align="center">
|
||||
<a href="http://ohmyform.com/" target="blank"><img src="https://ohmyform.com/img/logo_text.svg" width="320" alt="OhMyForm Logo" /></a>
|
||||
</p>
|
||||
|
||||
## Description
|
||||
|
||||
[OhMyForm](https://github.com/ohmyforn) api backend
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
$ yarn install
|
||||
```
|
||||
|
||||
## Running the app
|
||||
|
||||
```bash
|
||||
# development
|
||||
$ npm run start
|
||||
|
||||
# watch mode
|
||||
$ npm run start:dev
|
||||
|
||||
# incremental rebuild (webpack)
|
||||
$ npm run webpack
|
||||
$ npm run start:hmr
|
||||
|
||||
# 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
|
||||
5
api/nest-cli.json
Normal file
5
api/nest-cli.json
Normal file
@ -0,0 +1,5 @@
|
||||
{
|
||||
"language": "ts",
|
||||
"collection": "@nestjs/schematics",
|
||||
"sourceRoot": "src"
|
||||
}
|
||||
6
api/nodemon-debug.json
Normal file
6
api/nodemon-debug.json
Normal file
@ -0,0 +1,6 @@
|
||||
{
|
||||
"watch": ["src"],
|
||||
"ext": "ts",
|
||||
"ignore": ["src/**/*.spec.ts"],
|
||||
"exec": "node --inspect-brk -r ts-node/register src/main.ts"
|
||||
}
|
||||
6
api/nodemon.json
Normal file
6
api/nodemon.json
Normal file
@ -0,0 +1,6 @@
|
||||
{
|
||||
"watch": ["src"],
|
||||
"ext": "ts",
|
||||
"ignore": ["src/**/*.spec.ts"],
|
||||
"exec": "ts-node -r tsconfig-paths/register src/main.ts"
|
||||
}
|
||||
79
api/package.json
Normal file
79
api/package.json
Normal file
@ -0,0 +1,79 @@
|
||||
{
|
||||
"name": "ohmyform",
|
||||
"version": "0.1.0",
|
||||
"description": "Opensource alternative to TypeForm",
|
||||
"author": "multiple",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
"build": "rimraf dist && tsc -p tsconfig.build.json",
|
||||
"format": "prettier --write \"src/**/*.ts\"",
|
||||
"start": "ts-node -r tsconfig-paths/register src/main.ts",
|
||||
"start:dev": "tsc-watch -p tsconfig.build.json --onSuccess \"node dist/main.js\"",
|
||||
"start:debug": "tsc-watch -p tsconfig.build.json --onSuccess \"node --inspect-brk dist/main.js\"",
|
||||
"start:prod": "node dist/main.js",
|
||||
"lint": "tslint -p tsconfig.json -c tslint.json",
|
||||
"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": {
|
||||
"@godaddy/terminus": "^4.1.2",
|
||||
"@nest-modules/mailer": "^1.1.3",
|
||||
"@nestjs/common": "^6.5.2",
|
||||
"@nestjs/core": "^6.5.2",
|
||||
"@nestjs/jwt": "^6.1.1",
|
||||
"@nestjs/mongoose": "^6.1.2",
|
||||
"@nestjs/passport": "^6.1.0",
|
||||
"@nestjs/platform-express": "^6.5.2",
|
||||
"@nestjs/swagger": "^3.1.0",
|
||||
"@nestjs/terminus": "^6.5.0",
|
||||
"@types/bcrypt": "^3.0.0",
|
||||
"@types/mongoose": "^5.5.11",
|
||||
"bcrypt": "^3.0.6",
|
||||
"class-transformer": "^0.2.3",
|
||||
"class-validator": "^0.9.1",
|
||||
"handlebars": "^4.1.2",
|
||||
"mongoose": "^5.6.7",
|
||||
"nestjs-typegoose": "^5.2.1",
|
||||
"passport": "^0.4.0",
|
||||
"passport-jwt": "^4.0.0",
|
||||
"passport-local": "^1.0.0",
|
||||
"reflect-metadata": "^0.1.12",
|
||||
"rimraf": "^2.6.2",
|
||||
"rxjs": "^6.3.3",
|
||||
"swagger-ui-express": "^4.0.7",
|
||||
"typegoose": "^5.9.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@nestjs/testing": "6.1.1",
|
||||
"@types/express": "4.16.1",
|
||||
"@types/jest": "24.0.11",
|
||||
"@types/node": "11.13.4",
|
||||
"@types/supertest": "2.0.7",
|
||||
"jest": "24.7.1",
|
||||
"prettier": "1.17.0",
|
||||
"supertest": "4.0.2",
|
||||
"ts-jest": "24.0.2",
|
||||
"ts-node": "8.1.0",
|
||||
"tsc-watch": "2.2.1",
|
||||
"tsconfig-paths": "3.8.0",
|
||||
"tslint": "5.16.0",
|
||||
"typescript": "3.4.3"
|
||||
},
|
||||
"jest": {
|
||||
"moduleFileExtensions": [
|
||||
"js",
|
||||
"json",
|
||||
"ts"
|
||||
],
|
||||
"rootDir": "src",
|
||||
"testRegex": ".spec.ts$",
|
||||
"transform": {
|
||||
"^.+\\.(t|j)s$": "ts-jest"
|
||||
},
|
||||
"coverageDirectory": "../coverage",
|
||||
"testEnvironment": "node"
|
||||
}
|
||||
}
|
||||
20
api/src/app.controller.ts
Normal file
20
api/src/app.controller.ts
Normal file
@ -0,0 +1,20 @@
|
||||
import { Controller, Get } from '@nestjs/common';
|
||||
import { AppService } from './app.service';
|
||||
import {ApiExcludeEndpoint} from "@nestjs/swagger"
|
||||
|
||||
@Controller()
|
||||
export class AppController {
|
||||
constructor(private readonly appService: AppService) {}
|
||||
|
||||
@ApiExcludeEndpoint()
|
||||
@Get()
|
||||
getIndex(): object {
|
||||
return {
|
||||
message: 'hello humanoid',
|
||||
_links: {
|
||||
doc: '/doc',
|
||||
health: '/health'
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
35
api/src/app.module.ts
Normal file
35
api/src/app.module.ts
Normal file
@ -0,0 +1,35 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { TerminusModule } from '@nestjs/terminus';
|
||||
import { MongooseModule } from '@nestjs/mongoose';
|
||||
import { TypegooseModule } from 'nestjs-typegoose';
|
||||
import { TerminusOptionsService } from './terminus-options.service';
|
||||
import { AppController } from './app.controller';
|
||||
import { AppService } from './app.service';
|
||||
import { UserModule } from "./user/user.module"
|
||||
import { FormModule } from "./form/form.module"
|
||||
import { AuthModule } from './auth/auth.module';
|
||||
import { MailModule } from "./mail/mail.module"
|
||||
import { MailerModule } from "@nest-modules/mailer"
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
TypegooseModule.forRoot('mongodb://localhost/ohmyform', { useNewUrlParser: true }),
|
||||
MongooseModule.forRoot('mongodb://localhost/ohmyform'),
|
||||
TerminusModule.forRootAsync({
|
||||
useClass: TerminusOptionsService,
|
||||
}),
|
||||
MailerModule.forRoot({
|
||||
transport: 'smtp://localhost:1025',
|
||||
defaults: {
|
||||
from:'"OhMyForm" <noreply@ohmyform.com>',
|
||||
}
|
||||
}),
|
||||
UserModule,
|
||||
FormModule,
|
||||
AuthModule,
|
||||
MailModule,
|
||||
],
|
||||
controllers: [AppController],
|
||||
providers: [AppService],
|
||||
})
|
||||
export class AppModule {}
|
||||
8
api/src/app.service.ts
Normal file
8
api/src/app.service.ts
Normal file
@ -0,0 +1,8 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
@Injectable()
|
||||
export class AppService {
|
||||
getHello(): string {
|
||||
return 'Hello World!';
|
||||
}
|
||||
}
|
||||
9
api/src/auth/auth.controllers.ts
Normal file
9
api/src/auth/auth.controllers.ts
Normal file
@ -0,0 +1,9 @@
|
||||
import {AuthController} from "./controllers/auth.controller"
|
||||
import {RecoverController} from "./controllers/recover.controller"
|
||||
import {RegisterController} from "./controllers/register.controller"
|
||||
|
||||
export default [
|
||||
AuthController,
|
||||
RecoverController,
|
||||
RegisterController,
|
||||
]
|
||||
23
api/src/auth/auth.module.ts
Normal file
23
api/src/auth/auth.module.ts
Normal file
@ -0,0 +1,23 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { UserModule } from '../user/user.module';
|
||||
import { PassportModule } from '@nestjs/passport';
|
||||
import { jwtConstants } from "./constants"
|
||||
import { JwtModule } from "@nestjs/jwt"
|
||||
import controllers from './auth.controllers'
|
||||
import providers from './auth.providers'
|
||||
import { MailModule } from "../mail/mail.module"
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
UserModule,
|
||||
PassportModule,
|
||||
MailModule,
|
||||
JwtModule.register({
|
||||
secret: jwtConstants.secret,
|
||||
signOptions: { expiresIn: '12h' },
|
||||
}),
|
||||
],
|
||||
controllers,
|
||||
providers
|
||||
})
|
||||
export class AuthModule {}
|
||||
15
api/src/auth/auth.providers.ts
Normal file
15
api/src/auth/auth.providers.ts
Normal file
@ -0,0 +1,15 @@
|
||||
import { AuthService } from "./services/auth.service"
|
||||
import { PasswordService } from "./services/password.service"
|
||||
import { PasswordStrategy } from "./strategies/password.strategy"
|
||||
import { JwtStrategy } from "./strategies/jwt.strategy"
|
||||
import { JwtRefreshStrategy } from "./strategies/jwt.refresh.strategy"
|
||||
import { RegisterService } from "./services/register.service"
|
||||
|
||||
export default [
|
||||
AuthService,
|
||||
PasswordService,
|
||||
PasswordStrategy,
|
||||
JwtStrategy,
|
||||
JwtRefreshStrategy,
|
||||
RegisterService,
|
||||
]
|
||||
3
api/src/auth/constants.ts
Normal file
3
api/src/auth/constants.ts
Normal file
@ -0,0 +1,3 @@
|
||||
export const jwtConstants = {
|
||||
secret: 'secretKey',
|
||||
};
|
||||
30
api/src/auth/controllers/auth.controller.ts
Normal file
30
api/src/auth/controllers/auth.controller.ts
Normal file
@ -0,0 +1,30 @@
|
||||
import { Controller, Request, Post, UseGuards } from '@nestjs/common';
|
||||
import { AuthGuard } from '@nestjs/passport';
|
||||
import { AuthService } from "../services/auth.service"
|
||||
import { AuthJwtDto } from "../dto/auth.jwt.dto"
|
||||
import {ApiBearerAuth, ApiImplicitBody, ApiImplicitQuery, ApiResponse, ApiUseTags} from "@nestjs/swagger"
|
||||
|
||||
@ApiUseTags('authentication')
|
||||
@Controller('auth')
|
||||
export class AuthController {
|
||||
constructor(private readonly authService: AuthService) {}
|
||||
|
||||
@ApiResponse({ status: 201, description: 'Successful login.', type: AuthJwtDto})
|
||||
@ApiResponse({ status: 401, description: 'Invalid Credentials.'})
|
||||
@ApiImplicitQuery({name: 'username', type: String})
|
||||
@ApiImplicitQuery({name: 'password', type: String})
|
||||
@UseGuards(AuthGuard('password'))
|
||||
@Post('login')
|
||||
async login(@Request() req): Promise<AuthJwtDto> {
|
||||
return this.authService.createToken(req.user);
|
||||
}
|
||||
|
||||
@ApiBearerAuth()
|
||||
@ApiResponse({ status: 201, description: 'Consumed Refresh Token.', type: AuthJwtDto})
|
||||
@ApiResponse({ status: 401, description: 'Invalid Token.'})
|
||||
@UseGuards(AuthGuard('jwt.refresh'))
|
||||
@Post('refresh')
|
||||
async refresh(@Request() req): Promise<AuthJwtDto> {
|
||||
return this.authService.createToken(req.user);
|
||||
}
|
||||
}
|
||||
29
api/src/auth/controllers/recover.controller.ts
Normal file
29
api/src/auth/controllers/recover.controller.ts
Normal file
@ -0,0 +1,29 @@
|
||||
import { Controller, Request, Post, UseGuards } from '@nestjs/common';
|
||||
import { AuthGuard } from '@nestjs/passport';
|
||||
import { AuthService } from "../services/auth.service"
|
||||
import { AuthJwtDto } from "../dto/auth.jwt.dto"
|
||||
import {ApiBearerAuth, ApiImplicitBody, ApiImplicitQuery, ApiResponse, ApiUseTags} from "@nestjs/swagger"
|
||||
|
||||
@ApiUseTags('authentication')
|
||||
@Controller('auth/recover')
|
||||
export class RecoverController {
|
||||
constructor(private readonly authService: AuthService) {}
|
||||
|
||||
@ApiResponse({ status: 201, description: 'Successful registration.', type: AuthJwtDto})
|
||||
@ApiImplicitQuery({name: 'email', type: String})
|
||||
@ApiImplicitQuery({name: 'username', type: String})
|
||||
@Post('request')
|
||||
async request(@Request() req): Promise<AuthJwtDto> {
|
||||
// TODO
|
||||
return null
|
||||
}
|
||||
|
||||
@ApiResponse({ status: 201, description: 'Successful registration.', type: AuthJwtDto})
|
||||
@ApiImplicitQuery({name: 'token', type: String})
|
||||
@ApiImplicitQuery({name: 'password', type: String})
|
||||
@Post('finish')
|
||||
async finish(@Request() req): Promise<AuthJwtDto> {
|
||||
// TODO
|
||||
return null
|
||||
}
|
||||
}
|
||||
18
api/src/auth/controllers/register.controller.ts
Normal file
18
api/src/auth/controllers/register.controller.ts
Normal file
@ -0,0 +1,18 @@
|
||||
import { Controller, Post, Body } from '@nestjs/common';
|
||||
import { AuthService } from "../services/auth.service"
|
||||
import { ApiBadRequestResponse, ApiCreatedResponse, ApiUseTags } from "@nestjs/swagger"
|
||||
import { RegisterDto } from "../dto/register.dto"
|
||||
import {RegisterService} from "../services/register.service"
|
||||
|
||||
@ApiUseTags('authentication')
|
||||
@Controller('auth')
|
||||
export class RegisterController {
|
||||
constructor(private readonly registerService: RegisterService) {}
|
||||
|
||||
@ApiCreatedResponse({ description: 'Successful registration.'})
|
||||
@ApiBadRequestResponse({})
|
||||
@Post('register')
|
||||
async register(@Body() params: RegisterDto): Promise<void> {
|
||||
await this.registerService.register(params.username, params.email, params.password)
|
||||
}
|
||||
}
|
||||
9
api/src/auth/dto/auth.jwt.dto.ts
Normal file
9
api/src/auth/dto/auth.jwt.dto.ts
Normal file
@ -0,0 +1,9 @@
|
||||
import { ApiModelProperty } from '@nestjs/swagger';
|
||||
|
||||
export class AuthJwtDto {
|
||||
@ApiModelProperty()
|
||||
accessToken: string;
|
||||
|
||||
@ApiModelProperty()
|
||||
refreshToken: string;
|
||||
}
|
||||
20
api/src/auth/dto/register.dto.ts
Normal file
20
api/src/auth/dto/register.dto.ts
Normal file
@ -0,0 +1,20 @@
|
||||
import { ApiModelProperty } from "@nestjs/swagger"
|
||||
import { IsEmail, IsNotEmpty, Validate } from "class-validator"
|
||||
import { UsernameAlreadyInUse } from "../../user/validators/UsernameAlreadyInUse"
|
||||
import { EmailAlreadyInUse } from "../../user/validators/EmailAlreadyInUse"
|
||||
|
||||
export class RegisterDto {
|
||||
@ApiModelProperty()
|
||||
@IsNotEmpty()
|
||||
@Validate(UsernameAlreadyInUse)
|
||||
readonly username: string;
|
||||
|
||||
@ApiModelProperty()
|
||||
@IsNotEmpty()
|
||||
readonly password: string;
|
||||
|
||||
@ApiModelProperty()
|
||||
@Validate(EmailAlreadyInUse)
|
||||
@IsEmail()
|
||||
readonly email: string;
|
||||
}
|
||||
8
api/src/auth/interfaces/auth.user.interface.ts
Normal file
8
api/src/auth/interfaces/auth.user.interface.ts
Normal file
@ -0,0 +1,8 @@
|
||||
export interface AuthUser {
|
||||
id: string;
|
||||
username: string;
|
||||
email: string;
|
||||
roles: [string];
|
||||
created: Date;
|
||||
lastUpdated: Date;
|
||||
}
|
||||
18
api/src/auth/services/auth.service.spec.ts
Normal file
18
api/src/auth/services/auth.service.spec.ts
Normal file
@ -0,0 +1,18 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { AuthService } from './auth.service';
|
||||
|
||||
describe('AuthService', () => {
|
||||
let service: AuthService;
|
||||
|
||||
beforeEach(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [AuthService],
|
||||
}).compile();
|
||||
|
||||
service = module.get<AuthService>(AuthService);
|
||||
});
|
||||
|
||||
it('should be defined', () => {
|
||||
expect(service).toBeDefined();
|
||||
});
|
||||
});
|
||||
70
api/src/auth/services/auth.service.ts
Normal file
70
api/src/auth/services/auth.service.ts
Normal file
@ -0,0 +1,70 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { UserService } from '../../user/services/user.service';
|
||||
import { PasswordService } from "./password.service"
|
||||
import { JwtService } from '@nestjs/jwt';
|
||||
import { AuthUser } from "../interfaces/auth.user.interface"
|
||||
import { User } from "../../user/models/user.model"
|
||||
import { AuthJwtDto } from "../dto/auth.jwt.dto"
|
||||
|
||||
@Injectable()
|
||||
export class AuthService {
|
||||
constructor(
|
||||
private readonly usersService: UserService,
|
||||
private readonly passwordService: PasswordService,
|
||||
private readonly jwtService: JwtService
|
||||
) {}
|
||||
|
||||
async verifyForLogin(identifier: string, pass: string): Promise<AuthUser | null> {
|
||||
try {
|
||||
const user = await this.usersService.findOneByIdentifier(identifier);
|
||||
|
||||
if (await this.passwordService.verify(pass, user.passwordHash, user.salt)) {
|
||||
return AuthService.setupAuthUser(user)
|
||||
}
|
||||
} catch (e) {}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
async validate(identifier: string): Promise<AuthUser | null> {
|
||||
try {
|
||||
return AuthService.setupAuthUser(
|
||||
await this.usersService.findOneByIdentifier(identifier)
|
||||
)
|
||||
} catch (e) {}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static setupAuthUser(user:User):AuthUser {
|
||||
return {
|
||||
id: user._id,
|
||||
username: user.username,
|
||||
email: user.email,
|
||||
roles: user.roles,
|
||||
created: user.created,
|
||||
lastUpdated: user.lastModified
|
||||
};
|
||||
}
|
||||
|
||||
async createToken(user: AuthUser): Promise<AuthJwtDto> {
|
||||
const payload = {
|
||||
id: user.id,
|
||||
username: user.username,
|
||||
scope: user.roles,
|
||||
};
|
||||
return {
|
||||
accessToken: this.jwtService.sign(payload),
|
||||
// TODO add refresh token invalidation uppon usage! They should only work once
|
||||
refreshToken: this.jwtService.sign(
|
||||
{
|
||||
...payload,
|
||||
refresh: true
|
||||
},
|
||||
{
|
||||
expiresIn: '30days',
|
||||
}
|
||||
),
|
||||
};
|
||||
}
|
||||
}
|
||||
30
api/src/auth/services/password.service.ts
Normal file
30
api/src/auth/services/password.service.ts
Normal file
@ -0,0 +1,30 @@
|
||||
import { Injectable } from "@nestjs/common"
|
||||
import * as crypto from 'crypto'
|
||||
import * as bcrypt from 'bcrypt'
|
||||
|
||||
@Injectable()
|
||||
export class PasswordService {
|
||||
async verify (password: string, hash: string, salt?: string): Promise<boolean> {
|
||||
console.log('try to verify password', password)
|
||||
if (password[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)
|
||||
}
|
||||
}
|
||||
33
api/src/auth/services/register.service.ts
Normal file
33
api/src/auth/services/register.service.ts
Normal file
@ -0,0 +1,33 @@
|
||||
import { Injectable } from "@nestjs/common"
|
||||
import { MailService } from "../../mail/services/mail.service"
|
||||
import { User } from "../../user/models/user.model"
|
||||
import { PasswordService } from "./password.service"
|
||||
import { UserService } from "../../user/services/user.service"
|
||||
|
||||
@Injectable()
|
||||
export class RegisterService {
|
||||
constructor(
|
||||
private readonly mailService: MailService,
|
||||
private readonly passwordService: PasswordService,
|
||||
private readonly userService: UserService
|
||||
) {}
|
||||
|
||||
async register (username: string, email: string, password: string): Promise<void> {
|
||||
let user = new User()
|
||||
user.email = email
|
||||
user.username = username
|
||||
user.passwordHash = await this.passwordService.hash(password)
|
||||
|
||||
await this.userService.save(user)
|
||||
|
||||
await this.mailService.sendEmail(
|
||||
{
|
||||
template: 'auth/register',
|
||||
to: email
|
||||
},
|
||||
{
|
||||
confirm: 'some url'
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
26
api/src/auth/strategies/jwt.refresh.strategy.ts
Normal file
26
api/src/auth/strategies/jwt.refresh.strategy.ts
Normal file
@ -0,0 +1,26 @@
|
||||
import { ExtractJwt, Strategy } from 'passport-jwt';
|
||||
import { PassportStrategy } from '@nestjs/passport';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { jwtConstants } from '../constants';
|
||||
import { AuthService } from "../services/auth.service"
|
||||
import { AuthUser } from "../interfaces/auth.user.interface"
|
||||
|
||||
@Injectable()
|
||||
export class JwtRefreshStrategy extends PassportStrategy(Strategy, 'jwt.refresh') {
|
||||
constructor(private readonly authService: AuthService) {
|
||||
super({
|
||||
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
|
||||
ignoreExpiration: false,
|
||||
secretOrKey: jwtConstants.secret,
|
||||
});
|
||||
}
|
||||
|
||||
async validate(payload: any): Promise<AuthUser | null> {
|
||||
if (!payload.refresh) {
|
||||
return null
|
||||
}
|
||||
|
||||
// TODO add refresh token invalidation uppon usage! They should only work once
|
||||
return this.authService.validate(payload.username);
|
||||
}
|
||||
}
|
||||
21
api/src/auth/strategies/jwt.strategy.ts
Normal file
21
api/src/auth/strategies/jwt.strategy.ts
Normal file
@ -0,0 +1,21 @@
|
||||
import { ExtractJwt, Strategy } from 'passport-jwt';
|
||||
import { PassportStrategy } from '@nestjs/passport';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { jwtConstants } from '../constants';
|
||||
import { AuthService } from "../services/auth.service"
|
||||
import {AuthUser} from "../interfaces/auth.user.interface"
|
||||
|
||||
@Injectable()
|
||||
export class JwtStrategy extends PassportStrategy(Strategy) {
|
||||
constructor(private readonly authService: AuthService) {
|
||||
super({
|
||||
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
|
||||
ignoreExpiration: false,
|
||||
secretOrKey: jwtConstants.secret,
|
||||
});
|
||||
}
|
||||
|
||||
async validate(payload: any): Promise<AuthUser | null> {
|
||||
return this.authService.validate(payload.username);
|
||||
}
|
||||
}
|
||||
16
api/src/auth/strategies/password.strategy.ts
Normal file
16
api/src/auth/strategies/password.strategy.ts
Normal file
@ -0,0 +1,16 @@
|
||||
import { Strategy } from 'passport-local';
|
||||
import { PassportStrategy } from '@nestjs/passport';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { AuthService } from '../services/auth.service';
|
||||
import {AuthUser} from "../interfaces/auth.user.interface"
|
||||
|
||||
@Injectable()
|
||||
export class PasswordStrategy extends PassportStrategy(Strategy, 'password') {
|
||||
constructor(private readonly authService: AuthService) {
|
||||
super();
|
||||
}
|
||||
|
||||
validate(username: string, password: string): Promise<AuthUser | null> {
|
||||
return this.authService.verifyForLogin(username, password);
|
||||
}
|
||||
}
|
||||
6
api/src/core/dto/find.one.dto.ts
Normal file
6
api/src/core/dto/find.one.dto.ts
Normal file
@ -0,0 +1,6 @@
|
||||
import { IsMongoId } from 'class-validator';
|
||||
|
||||
export class FindOneDto {
|
||||
@IsMongoId()
|
||||
id: string;
|
||||
}
|
||||
47
api/src/form/controllers/form.controller.ts
Normal file
47
api/src/form/controllers/form.controller.ts
Normal file
@ -0,0 +1,47 @@
|
||||
import {Controller, Request, Get, Post, Put, Delete, UseGuards, Param, NotImplementedException} from '@nestjs/common';
|
||||
import { AuthGuard } from '@nestjs/passport';
|
||||
import { ApiBearerAuth, ApiImplicitQuery, ApiResponse, ApiUseTags } from "@nestjs/swagger"
|
||||
import { FormService } from "../services/form.service"
|
||||
import { FormDto } from "../dto/form.dto"
|
||||
import { FindOneDto } from "../../core/dto/find.one.dto"
|
||||
|
||||
@ApiUseTags('forms')
|
||||
@ApiBearerAuth()
|
||||
@Controller('forms')
|
||||
export class FormController {
|
||||
constructor(private readonly formService: FormService) {}
|
||||
|
||||
@Get()
|
||||
@UseGuards(AuthGuard('jwt'))
|
||||
async list(@Request() req): Promise<FormDto[]> {
|
||||
// TODO calculate total forms, add for pagination
|
||||
const results = await this.formService.findBy({})
|
||||
return results.map(form => new FormDto(form))
|
||||
}
|
||||
|
||||
@Post()
|
||||
@UseGuards(AuthGuard('jwt'))
|
||||
async create(@Request() req): Promise<FormDto> {
|
||||
throw new NotImplementedException()
|
||||
}
|
||||
|
||||
@ApiResponse({ status: 200, description: 'Form Object', type: FormDto})
|
||||
@ApiImplicitQuery({name: 'id', type: String})
|
||||
@Get(':id')
|
||||
@UseGuards(AuthGuard('jwt'))
|
||||
async read(@Param() params: FindOneDto): Promise<FormDto> {
|
||||
return new FormDto(await this.formService.findById(params.id));
|
||||
}
|
||||
|
||||
@Put(':id')
|
||||
@UseGuards(AuthGuard('jwt'))
|
||||
async update(@Param() params: FindOneDto, @Request() req): Promise<FormDto> {
|
||||
throw new NotImplementedException()
|
||||
}
|
||||
|
||||
@Delete(':id')
|
||||
@UseGuards(AuthGuard('jwt'))
|
||||
async delete(@Param() params: FindOneDto): Promise<void> {
|
||||
throw new NotImplementedException()
|
||||
}
|
||||
}
|
||||
25
api/src/form/controllers/public.controller.ts
Normal file
25
api/src/form/controllers/public.controller.ts
Normal file
@ -0,0 +1,25 @@
|
||||
import { Controller, Get, Param, NotFoundException } from '@nestjs/common';
|
||||
import { ApiImplicitQuery, ApiResponse, ApiUseTags } from "@nestjs/swagger"
|
||||
import { FormService } from "../services/form.service"
|
||||
import { Form } from "../models/form.model"
|
||||
import { PublicFormDto } from "../dto/public.form.dto"
|
||||
import { FindOneDto } from "../../core/dto/find.one.dto"
|
||||
|
||||
@ApiUseTags('forms')
|
||||
@Controller('public')
|
||||
export class PublicController {
|
||||
constructor(private readonly formService: FormService) {}
|
||||
|
||||
@ApiResponse({ status: 200, description: 'Form Object', type: PublicFormDto})
|
||||
@ApiImplicitQuery({name: 'id', type: String})
|
||||
@Get(':id')
|
||||
async read(@Param() params: FindOneDto): Promise<PublicFormDto> {
|
||||
const form:Form = await this.formService.findById(params.id)
|
||||
|
||||
if (!form.isLive) {
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
return new PublicFormDto(form);
|
||||
}
|
||||
}
|
||||
39
api/src/form/dto/form.dto.ts
Normal file
39
api/src/form/dto/form.dto.ts
Normal file
@ -0,0 +1,39 @@
|
||||
import { ApiModelProperty } from '@nestjs/swagger';
|
||||
import { Form } from "../models/form.model"
|
||||
|
||||
export class FormDto {
|
||||
@ApiModelProperty()
|
||||
id: string;
|
||||
|
||||
@ApiModelProperty()
|
||||
title: string;
|
||||
|
||||
@ApiModelProperty()
|
||||
live: boolean;
|
||||
|
||||
@ApiModelProperty()
|
||||
created: Date;
|
||||
|
||||
@ApiModelProperty()
|
||||
lastModified: Date;
|
||||
|
||||
@ApiModelProperty()
|
||||
fields: any;
|
||||
|
||||
@ApiModelProperty()
|
||||
info: {
|
||||
responses: number;
|
||||
}
|
||||
|
||||
constructor(partial: Partial<Form>) {
|
||||
this.id = partial._id.toString()
|
||||
this.title = partial.title
|
||||
this.live = partial.isLive
|
||||
this.created = partial.created
|
||||
this.lastModified = partial.lastModified
|
||||
this.fields = partial.form_fields
|
||||
this.info = {
|
||||
responses: 0 // TODO
|
||||
}
|
||||
}
|
||||
}
|
||||
17
api/src/form/dto/public.form.dto.ts
Normal file
17
api/src/form/dto/public.form.dto.ts
Normal file
@ -0,0 +1,17 @@
|
||||
import { ApiModelProperty } from '@nestjs/swagger';
|
||||
import { Form } from "../models/form.model"
|
||||
|
||||
export class PublicFormDto {
|
||||
@ApiModelProperty()
|
||||
id: string;
|
||||
|
||||
@ApiModelProperty()
|
||||
title: string;
|
||||
|
||||
fields: [];
|
||||
|
||||
constructor(partial: Partial<Form>) {
|
||||
this.id = partial._id.toString();
|
||||
this.title = partial.title
|
||||
}
|
||||
}
|
||||
7
api/src/form/form.controllers.ts
Normal file
7
api/src/form/form.controllers.ts
Normal file
@ -0,0 +1,7 @@
|
||||
import { FormController } from "./controllers/form.controller"
|
||||
import { PublicController } from "./controllers/public.controller"
|
||||
|
||||
export default [
|
||||
FormController,
|
||||
PublicController,
|
||||
]
|
||||
15
api/src/form/form.module.ts
Normal file
15
api/src/form/form.module.ts
Normal file
@ -0,0 +1,15 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import controllers from './form.controllers';
|
||||
import providers from './form.providers'
|
||||
import {TypegooseModule} from "nestjs-typegoose"
|
||||
import {Form} from "./models/form.model"
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
TypegooseModule.forFeature([Form]),
|
||||
],
|
||||
exports: [],
|
||||
controllers,
|
||||
providers,
|
||||
})
|
||||
export class FormModule {}
|
||||
5
api/src/form/form.providers.ts
Normal file
5
api/src/form/form.providers.ts
Normal file
@ -0,0 +1,5 @@
|
||||
import { FormService } from "./services/form.service"
|
||||
|
||||
export default [
|
||||
FormService
|
||||
]
|
||||
5
api/src/form/interfaces/button.interface.ts
Normal file
5
api/src/form/interfaces/button.interface.ts
Normal file
@ -0,0 +1,5 @@
|
||||
import { Document } from 'mongoose';
|
||||
|
||||
export interface Button extends Document{
|
||||
|
||||
}
|
||||
5
api/src/form/interfaces/field.interface.ts
Normal file
5
api/src/form/interfaces/field.interface.ts
Normal file
@ -0,0 +1,5 @@
|
||||
import { Document } from 'mongoose';
|
||||
|
||||
export interface Field extends Document{
|
||||
|
||||
}
|
||||
51
api/src/form/interfaces/form.interface.ts
Normal file
51
api/src/form/interfaces/form.interface.ts
Normal file
@ -0,0 +1,51 @@
|
||||
import { Document } from 'mongoose';
|
||||
import { Field } from "./field.interface"
|
||||
import { Button } from "./button.interface"
|
||||
|
||||
export class Form extends Document{
|
||||
readonly firstName: string;
|
||||
readonly language: string;
|
||||
readonly analytics: string;
|
||||
readonly form_fields: Field[];
|
||||
readonly admin: any;
|
||||
readonly startPage: {
|
||||
readonly showStart: boolean;
|
||||
readonly introTitle: string;
|
||||
readonly introParagraph: string;
|
||||
readonly introButtonText: string;
|
||||
readonly buttons: Button[];
|
||||
};
|
||||
readonly endPage: {
|
||||
readonly showEnd: boolean;
|
||||
readonly title: string;
|
||||
readonly paragraph: string;
|
||||
readonly buttonText: string;
|
||||
readonly buttons: Button[];
|
||||
};
|
||||
readonly selfNotifications: {
|
||||
readonly fromField: string;
|
||||
readonly toEmails: string;
|
||||
readonly subject: string;
|
||||
readonly htmlTemplate: string;
|
||||
readonly enabled: boolean;
|
||||
};
|
||||
readonly respondentNotifications: {
|
||||
readonly toField: string;
|
||||
readonly fromEmails: string;
|
||||
readonly subject: string;
|
||||
readonly htmlTemplate: string;
|
||||
readonly enabled: boolean;
|
||||
};
|
||||
readonly showFooter: boolean;
|
||||
readonly isLive: boolean;
|
||||
readonly design: {
|
||||
readonly colors: {
|
||||
readonly backgroundColor: string;
|
||||
readonly questionColor: string;
|
||||
readonly answerColor: string;
|
||||
readonly buttonColor: string;
|
||||
readonly buttonTextColor: string;
|
||||
};
|
||||
readonly font: string;
|
||||
};
|
||||
}
|
||||
14
api/src/form/models/embedded/analytics.ts
Normal file
14
api/src/form/models/embedded/analytics.ts
Normal file
@ -0,0 +1,14 @@
|
||||
import {prop} from "typegoose"
|
||||
import {VisitorData} from "./visitor.data"
|
||||
import {Exclude} from "class-transformer"
|
||||
|
||||
export class Analytics {
|
||||
@Exclude()
|
||||
readonly _id: string;
|
||||
|
||||
@prop()
|
||||
gaCode: string;
|
||||
|
||||
@prop()
|
||||
visitors: VisitorData[]
|
||||
}
|
||||
30
api/src/form/models/embedded/button.ts
Normal file
30
api/src/form/models/embedded/button.ts
Normal file
@ -0,0 +1,30 @@
|
||||
import {Exclude} from "class-transformer"
|
||||
import {prop} from "typegoose"
|
||||
|
||||
export class Button {
|
||||
@Exclude()
|
||||
readonly _id: string;
|
||||
|
||||
@prop({
|
||||
match: /((([A-Za-z]{3,9}:(?:\/\/)?)(?:[-;:&=\+\$,\w]+@)?[A-Za-z0-9.-]+|(?:www.|[-;:&=\+\$,\w]+@)[A-Za-z0-9.-]+)((?:\/[\+~%\/.\w-_]*)?\??(?:[-\+=&;%@.\w_]*)#?(?:[\w]*))?)/
|
||||
})
|
||||
url: string;
|
||||
|
||||
@prop()
|
||||
action: string;
|
||||
|
||||
@prop()
|
||||
text: string;
|
||||
|
||||
@prop({
|
||||
match: [/^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$/],
|
||||
default: '#5bc0de'
|
||||
})
|
||||
bgColor: string;
|
||||
|
||||
@prop({
|
||||
match: [/^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$/],
|
||||
default: '#ffffff'
|
||||
})
|
||||
color: string;
|
||||
}
|
||||
37
api/src/form/models/embedded/colors.ts
Normal file
37
api/src/form/models/embedded/colors.ts
Normal file
@ -0,0 +1,37 @@
|
||||
import {prop, Ref, Typegoose} from "typegoose"
|
||||
import {Exclude} from "class-transformer"
|
||||
|
||||
export class Colors {
|
||||
@Exclude()
|
||||
readonly _id: string;
|
||||
|
||||
@prop({
|
||||
match: [/^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$/],
|
||||
default: '#fff'
|
||||
})
|
||||
readonly backgroundColor: string;
|
||||
|
||||
@prop({
|
||||
match: [/^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$/],
|
||||
default: '#333'
|
||||
})
|
||||
readonly questionColor: string;
|
||||
|
||||
@prop({
|
||||
match: [/^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$/],
|
||||
default: '#333'
|
||||
})
|
||||
readonly answerColor: string;
|
||||
|
||||
@prop({
|
||||
match: [/^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$/],
|
||||
default: '#fff'
|
||||
})
|
||||
readonly buttonColor: string;
|
||||
|
||||
@prop({
|
||||
match: [/^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$/],
|
||||
default: '#333'
|
||||
})
|
||||
readonly buttonTextColor: string;
|
||||
}
|
||||
14
api/src/form/models/embedded/design.ts
Normal file
14
api/src/form/models/embedded/design.ts
Normal file
@ -0,0 +1,14 @@
|
||||
import {prop} from "typegoose"
|
||||
import {Colors} from "./colors"
|
||||
import {Exclude} from "class-transformer"
|
||||
|
||||
export class Design {
|
||||
@Exclude()
|
||||
readonly _id: string;
|
||||
|
||||
@prop()
|
||||
readonly colors: Colors;
|
||||
|
||||
@prop()
|
||||
readonly font: string;
|
||||
}
|
||||
29
api/src/form/models/embedded/end.page.ts
Normal file
29
api/src/form/models/embedded/end.page.ts
Normal file
@ -0,0 +1,29 @@
|
||||
import {prop} from "typegoose"
|
||||
import {Button} from "./button"
|
||||
import {Exclude} from "class-transformer"
|
||||
|
||||
export class EndPage {
|
||||
@Exclude()
|
||||
readonly _id: string;
|
||||
|
||||
@prop({
|
||||
default: false
|
||||
})
|
||||
readonly showEnd: boolean;
|
||||
|
||||
@prop({
|
||||
default: 'Thank you for filling out the form'
|
||||
})
|
||||
readonly title: string;
|
||||
|
||||
@prop()
|
||||
readonly paragraph: string;
|
||||
|
||||
@prop({
|
||||
default: 'Go back to Form'
|
||||
})
|
||||
readonly buttonText: string;
|
||||
|
||||
@prop()
|
||||
readonly buttons: Button[];
|
||||
}
|
||||
19
api/src/form/models/embedded/field.option.ts
Normal file
19
api/src/form/models/embedded/field.option.ts
Normal file
@ -0,0 +1,19 @@
|
||||
import {prop} from "typegoose"
|
||||
import {VisitorData} from "./visitor.data"
|
||||
import {Exclude} from "class-transformer"
|
||||
|
||||
export class FieldOption {
|
||||
@Exclude()
|
||||
readonly _id: string;
|
||||
|
||||
@prop()
|
||||
option_id: number;
|
||||
|
||||
@prop()
|
||||
option_title: string;
|
||||
|
||||
@prop({
|
||||
trim: true
|
||||
})
|
||||
option_value: string;
|
||||
}
|
||||
83
api/src/form/models/embedded/field.ts
Normal file
83
api/src/form/models/embedded/field.ts
Normal file
@ -0,0 +1,83 @@
|
||||
import {arrayProp, prop} from "typegoose"
|
||||
import {LogicJump} from "./logic.jump"
|
||||
import {RatingField} from "./rating.field"
|
||||
import {FieldOption} from "./field.option"
|
||||
import {Exclude} from "class-transformer"
|
||||
|
||||
export class Field {
|
||||
@Exclude()
|
||||
readonly _id: string;
|
||||
|
||||
@prop({
|
||||
default: false
|
||||
})
|
||||
isSubmission: boolean;
|
||||
|
||||
@prop()
|
||||
submissionId: string;
|
||||
|
||||
@prop({
|
||||
trim: true
|
||||
})
|
||||
title: string;
|
||||
|
||||
@prop({
|
||||
default: ''
|
||||
})
|
||||
description: string;
|
||||
|
||||
@prop()
|
||||
logicJump: LogicJump;
|
||||
|
||||
@prop()
|
||||
ratingOptions: RatingField;
|
||||
|
||||
@arrayProp({
|
||||
items: FieldOption
|
||||
})
|
||||
fieldOptions: FieldOption[];
|
||||
|
||||
@prop({
|
||||
default: true
|
||||
})
|
||||
required: boolean;
|
||||
|
||||
@prop({
|
||||
default: false
|
||||
})
|
||||
disabled: boolean;
|
||||
|
||||
@prop({
|
||||
default: false
|
||||
})
|
||||
deletePreserved: boolean;
|
||||
|
||||
@arrayProp({
|
||||
items: String
|
||||
})
|
||||
validFieldTypes: boolean;
|
||||
|
||||
@prop({
|
||||
enum: [
|
||||
'textfield',
|
||||
'date',
|
||||
'email',
|
||||
'legal',
|
||||
'textarea',
|
||||
'link',
|
||||
'statement',
|
||||
'dropdown',
|
||||
'rating',
|
||||
'radio',
|
||||
'hidden',
|
||||
'yes_no',
|
||||
'number'
|
||||
]
|
||||
})
|
||||
fieldType: string;
|
||||
|
||||
@prop({
|
||||
default: ''
|
||||
})
|
||||
fieldValue: any;
|
||||
}
|
||||
45
api/src/form/models/embedded/logic.jump.ts
Normal file
45
api/src/form/models/embedded/logic.jump.ts
Normal file
@ -0,0 +1,45 @@
|
||||
import {prop, Ref} from "typegoose"
|
||||
import {VisitorData} from "./visitor.data"
|
||||
import {Exclude} from "class-transformer"
|
||||
import {Field} from "./field"
|
||||
|
||||
export class LogicJump {
|
||||
@Exclude()
|
||||
readonly _id: string;
|
||||
|
||||
@prop({
|
||||
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'
|
||||
]
|
||||
})
|
||||
expressionString: string;
|
||||
|
||||
@prop({
|
||||
ref: Field
|
||||
})
|
||||
fieldA: Ref<Field>;
|
||||
|
||||
@prop()
|
||||
valueB: string;
|
||||
|
||||
@prop({
|
||||
ref: Field
|
||||
})
|
||||
jumpTo: Ref<Field>;
|
||||
|
||||
@prop({
|
||||
default: false
|
||||
})
|
||||
enabled: boolean;
|
||||
}
|
||||
38
api/src/form/models/embedded/rating.field.ts
Normal file
38
api/src/form/models/embedded/rating.field.ts
Normal file
@ -0,0 +1,38 @@
|
||||
import {arrayProp, prop} from "typegoose"
|
||||
import {VisitorData} from "./visitor.data"
|
||||
import {Exclude} from "class-transformer"
|
||||
|
||||
export class RatingField {
|
||||
@Exclude()
|
||||
readonly _id: string;
|
||||
|
||||
@prop({
|
||||
min: 1,
|
||||
max: 10
|
||||
})
|
||||
steps: number;
|
||||
|
||||
@prop({
|
||||
enum: [
|
||||
'Heart',
|
||||
'Star',
|
||||
'thumbs-up',
|
||||
'thumbs-down',
|
||||
'Circle',
|
||||
'Square',
|
||||
'Check Circle',
|
||||
'Smile Outlined',
|
||||
'Hourglass',
|
||||
'bell',
|
||||
'Paper Plane',
|
||||
'Comment',
|
||||
'Trash'
|
||||
]
|
||||
})
|
||||
shape: string;
|
||||
|
||||
@arrayProp({
|
||||
items: String
|
||||
})
|
||||
validShapes: [string];
|
||||
}
|
||||
30
api/src/form/models/embedded/respondent.notifications.ts
Normal file
30
api/src/form/models/embedded/respondent.notifications.ts
Normal file
@ -0,0 +1,30 @@
|
||||
import {prop} from "typegoose"
|
||||
import {Exclude} from "class-transformer"
|
||||
|
||||
export class RespondentNotifications {
|
||||
@Exclude()
|
||||
readonly _id: string;
|
||||
|
||||
@prop()
|
||||
readonly toField: string;
|
||||
|
||||
@prop({
|
||||
match: [/.+\@.+\..+/, 'Please fill a valid email address']
|
||||
})
|
||||
readonly fromEmails: string;
|
||||
|
||||
@prop({
|
||||
default: 'OhMyForm: Thank you for filling out this OhMyForm'
|
||||
})
|
||||
readonly subject: string;
|
||||
|
||||
@prop({
|
||||
default: 'Hello, <br><br> We’ve received your submission. <br><br> Thank you & have a nice day!',
|
||||
})
|
||||
readonly htmlTemplate: string;
|
||||
|
||||
@prop({
|
||||
default: false
|
||||
})
|
||||
readonly enabled: boolean;
|
||||
}
|
||||
24
api/src/form/models/embedded/self.notifications.ts
Normal file
24
api/src/form/models/embedded/self.notifications.ts
Normal file
@ -0,0 +1,24 @@
|
||||
import {prop} from "typegoose"
|
||||
import {Exclude} from "class-transformer"
|
||||
|
||||
export class SelfNotifications {
|
||||
@Exclude()
|
||||
readonly _id: string;
|
||||
|
||||
@prop()
|
||||
readonly fromField: string;
|
||||
|
||||
@prop()
|
||||
readonly toEmails: string;
|
||||
|
||||
@prop()
|
||||
readonly subject: string;
|
||||
|
||||
@prop()
|
||||
readonly htmlTemplate: string;
|
||||
|
||||
@prop({
|
||||
default: false
|
||||
})
|
||||
readonly enabled: boolean;
|
||||
}
|
||||
29
api/src/form/models/embedded/start.page.ts
Normal file
29
api/src/form/models/embedded/start.page.ts
Normal file
@ -0,0 +1,29 @@
|
||||
import {prop} from "typegoose"
|
||||
import {Button} from "./button"
|
||||
import {Exclude} from "class-transformer"
|
||||
|
||||
export class StartPage {
|
||||
@Exclude()
|
||||
readonly _id: string;
|
||||
|
||||
@prop({
|
||||
default: false
|
||||
})
|
||||
readonly showStart: boolean;
|
||||
|
||||
@prop({
|
||||
default: 'Welcome to Form'
|
||||
})
|
||||
readonly introTitle: string;
|
||||
|
||||
@prop()
|
||||
readonly introParagraph: string;
|
||||
|
||||
@prop({
|
||||
default: 'Start'
|
||||
})
|
||||
readonly introButtonText: string;
|
||||
|
||||
@prop()
|
||||
readonly buttons: Button[];
|
||||
}
|
||||
44
api/src/form/models/embedded/visitor.data.ts
Normal file
44
api/src/form/models/embedded/visitor.data.ts
Normal file
@ -0,0 +1,44 @@
|
||||
import {Exclude} from "class-transformer"
|
||||
import {arrayProp, prop} from "typegoose"
|
||||
import {Field} from "./field"
|
||||
import {Ref} from "typegoose/lib/prop"
|
||||
|
||||
export class VisitorData {
|
||||
@Exclude()
|
||||
readonly _id: string;
|
||||
|
||||
@prop()
|
||||
readonly introParagraph: string;
|
||||
|
||||
@prop()
|
||||
readonly referrer: string;
|
||||
|
||||
@arrayProp({
|
||||
itemsRef: Field
|
||||
})
|
||||
readonly filledOutFields: Ref<Field>[];
|
||||
|
||||
@prop()
|
||||
readonly timeElapsed: number;
|
||||
|
||||
@prop()
|
||||
readonly isSubmitted: boolean;
|
||||
|
||||
@prop({
|
||||
enum: ['en', 'fr', 'es', 'it', 'de'],
|
||||
default: 'en',
|
||||
})
|
||||
readonly language: string;
|
||||
|
||||
@prop()
|
||||
readonly ipAddr: string;
|
||||
|
||||
@prop({
|
||||
enum: ['desktop', 'phone', 'tablet', 'other'],
|
||||
default: 'other'
|
||||
})
|
||||
readonly deviceType: string;
|
||||
|
||||
@prop()
|
||||
readonly userAgent: string;
|
||||
}
|
||||
72
api/src/form/models/form.model.ts
Normal file
72
api/src/form/models/form.model.ts
Normal file
@ -0,0 +1,72 @@
|
||||
import {arrayProp, prop, Ref, Typegoose} from "typegoose"
|
||||
import {Analytics} from "./embedded/analytics"
|
||||
import {Field} from "./embedded/field"
|
||||
import {StartPage} from "./embedded/start.page"
|
||||
import {EndPage} from "./embedded/end.page"
|
||||
import {SelfNotifications} from "./embedded/self.notifications"
|
||||
import {RespondentNotifications} from "./embedded/respondent.notifications"
|
||||
import {Design} from "./embedded/design"
|
||||
import {User} from "../../user/models/user.model"
|
||||
|
||||
export class Form extends Typegoose {
|
||||
readonly _id: any;
|
||||
|
||||
@prop({
|
||||
trim: true,
|
||||
required: 'Form Title cannot be blank'
|
||||
})
|
||||
readonly title: string;
|
||||
|
||||
@prop()
|
||||
readonly created: any;
|
||||
|
||||
@prop()
|
||||
readonly lastModified: any;
|
||||
|
||||
@prop({
|
||||
enum: ['en', 'fr', 'es', 'it', 'de'],
|
||||
default: 'en',
|
||||
required: 'Form must have a language'
|
||||
})
|
||||
readonly language: string;
|
||||
|
||||
@prop()
|
||||
readonly analytics: Analytics;
|
||||
|
||||
@arrayProp({
|
||||
items: Field,
|
||||
default: []
|
||||
})
|
||||
readonly form_fields: Field[];
|
||||
|
||||
@prop({
|
||||
ref: User,
|
||||
required: 'Form must have an Admin'
|
||||
})
|
||||
readonly admin: Ref<User>;
|
||||
|
||||
@prop()
|
||||
readonly startPage: StartPage;
|
||||
|
||||
@prop()
|
||||
readonly endPage: EndPage;
|
||||
|
||||
@prop()
|
||||
readonly selfNotifications: SelfNotifications;
|
||||
|
||||
@prop()
|
||||
readonly respondentNotifications: RespondentNotifications;
|
||||
|
||||
@prop({
|
||||
default: true
|
||||
})
|
||||
readonly showFooter: boolean;
|
||||
|
||||
@prop({
|
||||
default: true
|
||||
})
|
||||
readonly isLive: boolean;
|
||||
|
||||
@prop()
|
||||
readonly design: Design;
|
||||
}
|
||||
18
api/src/form/services/form.service.ts
Normal file
18
api/src/form/services/form.service.ts
Normal file
@ -0,0 +1,18 @@
|
||||
import {Injectable} from '@nestjs/common';
|
||||
import { InjectModel } from 'nestjs-typegoose';
|
||||
import { ModelType } from 'typegoose';
|
||||
import {Form} from "../models/form.model"
|
||||
import {User} from "../../user/models/user.model"
|
||||
|
||||
@Injectable()
|
||||
export class FormService {
|
||||
constructor(@InjectModel(Form) private readonly formModel: ModelType<Form>) {}
|
||||
|
||||
async findById(id: string): Promise<Form> {
|
||||
return await this.formModel.findById(id).exec()
|
||||
}
|
||||
|
||||
async findBy(conditions): Promise<Form[]> {
|
||||
return await this.formModel.find(conditions).exec()
|
||||
}
|
||||
}
|
||||
12
api/src/mail/dto/options.dto.ts
Normal file
12
api/src/mail/dto/options.dto.ts
Normal file
@ -0,0 +1,12 @@
|
||||
|
||||
export class OptionsDto {
|
||||
template: string;
|
||||
|
||||
language?: string;
|
||||
|
||||
to: string;
|
||||
|
||||
cc?: string[];
|
||||
|
||||
bcc?: string[];
|
||||
}
|
||||
5
api/src/mail/mail.exports.ts
Normal file
5
api/src/mail/mail.exports.ts
Normal file
@ -0,0 +1,5 @@
|
||||
import { MailService } from "./services/mail.service"
|
||||
|
||||
export default [
|
||||
MailService
|
||||
]
|
||||
12
api/src/mail/mail.module.ts
Normal file
12
api/src/mail/mail.module.ts
Normal file
@ -0,0 +1,12 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import providers from './mail.providers'
|
||||
import exportList from './mail.exports'
|
||||
import { HandlebarsAdapter, MailerModule } from "@nest-modules/mailer"
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
],
|
||||
providers,
|
||||
exports: exportList,
|
||||
})
|
||||
export class MailModule {}
|
||||
5
api/src/mail/mail.providers.ts
Normal file
5
api/src/mail/mail.providers.ts
Normal file
@ -0,0 +1,5 @@
|
||||
import { MailService } from "./services/mail.service"
|
||||
|
||||
export default [
|
||||
MailService
|
||||
]
|
||||
35
api/src/mail/services/mail.service.ts
Normal file
35
api/src/mail/services/mail.service.ts
Normal file
@ -0,0 +1,35 @@
|
||||
import {Inject, Injectable, NotFoundException} from '@nestjs/common';
|
||||
import { OptionsDto } from "../dto/options.dto"
|
||||
import { MailerService } from '@nest-modules/mailer';
|
||||
import * as Handlebars from 'handlebars'
|
||||
import * as fs from 'fs';
|
||||
|
||||
@Injectable()
|
||||
export class MailService {
|
||||
constructor(
|
||||
private readonly nodeMailer: MailerService
|
||||
) {}
|
||||
|
||||
async sendEmail(options:OptionsDto, placeholders:any): Promise<boolean> {
|
||||
const template = fs.readFileSync(`${__dirname}/../../../views/en/mail/${options.template}.hbs`, 'UTF-8');
|
||||
|
||||
const parts = Handlebars
|
||||
.compile(template)
|
||||
(placeholders)
|
||||
.split('---')
|
||||
|
||||
const mail:any = {
|
||||
to: options.to,
|
||||
subject: parts[0],
|
||||
text: parts[1],
|
||||
}
|
||||
|
||||
if (parts.length > 2) {
|
||||
mail.html = parts[2]
|
||||
}
|
||||
|
||||
await this.nodeMailer.sendMail(mail)
|
||||
|
||||
return false
|
||||
}
|
||||
}
|
||||
33
api/src/main.ts
Normal file
33
api/src/main.ts
Normal file
@ -0,0 +1,33 @@
|
||||
import {NestFactory, Reflector} from '@nestjs/core';
|
||||
import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger';
|
||||
import { AppModule } from './app.module';
|
||||
import { ClassSerializerInterceptor, ValidationPipe } from '@nestjs/common';
|
||||
import { useContainer } from "class-validator"
|
||||
const pkg = require('../package.json')
|
||||
|
||||
async function bootstrap() {
|
||||
const app = await NestFactory.create(AppModule, { cors: true });
|
||||
|
||||
useContainer(app.select(AppModule), { fallbackOnErrors: true });
|
||||
|
||||
app.useGlobalPipes(new ValidationPipe({
|
||||
disableErrorMessages: false,
|
||||
transform: true,
|
||||
}));
|
||||
|
||||
app.useGlobalInterceptors(new ClassSerializerInterceptor(app.get(Reflector)))
|
||||
|
||||
const options = new DocumentBuilder()
|
||||
.setTitle('OhMyForm')
|
||||
.setDescription('API documentation')
|
||||
.setVersion(pkg.version)
|
||||
.addBearerAuth()
|
||||
.build();
|
||||
|
||||
const document = SwaggerModule.createDocument(app, options);
|
||||
SwaggerModule.setup('doc', app, document);
|
||||
|
||||
await app.listen(3000);
|
||||
}
|
||||
|
||||
bootstrap();
|
||||
29
api/src/terminus-options.service.ts
Normal file
29
api/src/terminus-options.service.ts
Normal file
@ -0,0 +1,29 @@
|
||||
import {
|
||||
TerminusEndpoint,
|
||||
TerminusOptionsFactory,
|
||||
DNSHealthIndicator,
|
||||
MongooseHealthIndicator,
|
||||
TerminusModuleOptions
|
||||
} from '@nestjs/terminus';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
@Injectable()
|
||||
export class TerminusOptionsService implements TerminusOptionsFactory {
|
||||
constructor(
|
||||
private readonly dns: DNSHealthIndicator,
|
||||
private readonly mongoose: MongooseHealthIndicator
|
||||
) {}
|
||||
|
||||
createTerminusOptions(): TerminusModuleOptions {
|
||||
const healthEndpoint: TerminusEndpoint = {
|
||||
url: '/health',
|
||||
healthIndicators: [
|
||||
async () => this.dns.pingCheck('mail', 'https://google.com'),
|
||||
async () => this.mongoose.pingCheck('mongo')
|
||||
],
|
||||
};
|
||||
return {
|
||||
endpoints: [healthEndpoint],
|
||||
};
|
||||
}
|
||||
}
|
||||
53
api/src/user/controllers/user.controller.ts
Normal file
53
api/src/user/controllers/user.controller.ts
Normal file
@ -0,0 +1,53 @@
|
||||
import {Controller, Request, Get, Post, Put, Delete, UseGuards, Param, NotImplementedException} from '@nestjs/common';
|
||||
import { AuthGuard } from '@nestjs/passport';
|
||||
import { ApiBearerAuth, ApiImplicitQuery, ApiResponse, ApiUseTags } from "@nestjs/swagger"
|
||||
import { UserService } from "../services/user.service"
|
||||
import { UserDto } from "../dto/user.dto"
|
||||
import { FindOneDto } from "../../core/dto/find.one.dto"
|
||||
|
||||
@ApiUseTags('users')
|
||||
@ApiBearerAuth()
|
||||
@Controller('users')
|
||||
export class UserController {
|
||||
constructor(private readonly userService: UserService) {}
|
||||
|
||||
@Get()
|
||||
@UseGuards(AuthGuard('jwt'))
|
||||
async list(@Request() req): Promise<UserDto[]> {
|
||||
// TODO calculate total forms, add for pagination
|
||||
const results = await this.userService.findBy({})
|
||||
return results.map(form => new UserDto(form))
|
||||
}
|
||||
|
||||
@ApiResponse({ status: 200, description: 'User Object', type: UserDto})
|
||||
@Post()
|
||||
@UseGuards(AuthGuard('jwt'))
|
||||
async create(@Request() req): Promise<UserDto> {
|
||||
throw new NotImplementedException()
|
||||
}
|
||||
|
||||
@ApiResponse({ status: 200, description: 'User Object', type: UserDto})
|
||||
@ApiImplicitQuery({name: 'id', type: String})
|
||||
@Get(':id')
|
||||
@UseGuards(AuthGuard('jwt'))
|
||||
async read(@Param() params: FindOneDto): Promise<UserDto> {
|
||||
return new UserDto(await this.userService.findById(params.id));
|
||||
}
|
||||
|
||||
@ApiResponse({ status: 200, description: 'User Object', type: UserDto})
|
||||
@ApiImplicitQuery({name: 'id', type: String})
|
||||
@Put(':id')
|
||||
@UseGuards(AuthGuard('jwt'))
|
||||
async update(@Param() params: FindOneDto, @Request() req): Promise<UserDto> {
|
||||
throw new NotImplementedException()
|
||||
}
|
||||
|
||||
|
||||
@ApiResponse({ status: 200, description: 'User Object', type: UserDto})
|
||||
@ApiImplicitQuery({name: 'id', type: String})
|
||||
@Delete(':id')
|
||||
@UseGuards(AuthGuard('jwt'))
|
||||
async delete(@Param() params: FindOneDto): Promise<void> {
|
||||
throw new NotImplementedException()
|
||||
}
|
||||
}
|
||||
27
api/src/user/dto/user.dto.ts
Normal file
27
api/src/user/dto/user.dto.ts
Normal file
@ -0,0 +1,27 @@
|
||||
import { ApiModelProperty } from '@nestjs/swagger';
|
||||
import {User} from "../models/user.model"
|
||||
|
||||
export class UserDto {
|
||||
@ApiModelProperty()
|
||||
id: string;
|
||||
|
||||
@ApiModelProperty()
|
||||
username: string;
|
||||
|
||||
@ApiModelProperty()
|
||||
email: string;
|
||||
|
||||
@ApiModelProperty()
|
||||
roles: string[];
|
||||
|
||||
@ApiModelProperty()
|
||||
created: Date;
|
||||
|
||||
constructor(partial: Partial<User>) {
|
||||
this.id = partial._id.toString()
|
||||
this.username = partial.username
|
||||
this.email = partial.email
|
||||
this.roles = partial.roles
|
||||
this.created = partial.created
|
||||
}
|
||||
}
|
||||
97
api/src/user/models/user.model.ts
Normal file
97
api/src/user/models/user.model.ts
Normal file
@ -0,0 +1,97 @@
|
||||
import {arrayProp, pre, prop, Typegoose} from 'typegoose';
|
||||
import { IsString } from 'class-validator';
|
||||
import {Exclude} from "class-transformer"
|
||||
|
||||
@pre<User>('save', (next) => {
|
||||
this.lastModified = new Date()
|
||||
next()
|
||||
})
|
||||
export class User extends Typegoose {
|
||||
@Exclude()
|
||||
readonly _id: string;
|
||||
|
||||
@prop({
|
||||
trim: true,
|
||||
default: ''
|
||||
})
|
||||
firstName: string;
|
||||
|
||||
@prop({
|
||||
trim: true,
|
||||
default: ''
|
||||
})
|
||||
lastName: string;
|
||||
|
||||
@prop({
|
||||
trim: true,
|
||||
lowercase: true,
|
||||
unique: true, // 'Account already exists with this email',
|
||||
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,}))$/,
|
||||
'Please fill a valid email address'
|
||||
],
|
||||
required: [true, 'Email is required']
|
||||
})
|
||||
email: string;
|
||||
|
||||
@prop({
|
||||
unique: true,
|
||||
lowercase: true,
|
||||
match: [
|
||||
/^[a-zA-Z0-9\-]+$/,
|
||||
'Username can only contain alphanumeric characters and \'-\''
|
||||
],
|
||||
required: [true, 'Username is required']
|
||||
})
|
||||
username: string;
|
||||
|
||||
@prop({
|
||||
default: ''
|
||||
})
|
||||
passwordHash: string;
|
||||
|
||||
@prop()
|
||||
salt: string;
|
||||
|
||||
@prop({
|
||||
default: 'local'
|
||||
})
|
||||
provider: string;
|
||||
|
||||
@arrayProp({
|
||||
items: String,
|
||||
enum: ['user', 'admin', 'superuser'],
|
||||
default: ['user']
|
||||
})
|
||||
roles: [string];
|
||||
|
||||
@prop({
|
||||
enum: ['en', 'fr', 'es', 'it', 'de'],
|
||||
default: 'en',
|
||||
})
|
||||
language: string;
|
||||
|
||||
@prop({
|
||||
default: Date.now
|
||||
})
|
||||
readonly created: Date;
|
||||
|
||||
@prop()
|
||||
readonly lastModified: Date;
|
||||
|
||||
@prop()
|
||||
resetPasswordToken: string;
|
||||
|
||||
@prop()
|
||||
resetPasswordExpires: Date;
|
||||
|
||||
@prop()
|
||||
token: string;
|
||||
|
||||
@prop({
|
||||
unique: true,
|
||||
index: true,
|
||||
sparse: true
|
||||
})
|
||||
apiKey: string;
|
||||
}
|
||||
18
api/src/user/services/user.service.spec.ts
Normal file
18
api/src/user/services/user.service.spec.ts
Normal file
@ -0,0 +1,18 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { UserService } from './users.service';
|
||||
|
||||
describe('UsersService', () => {
|
||||
let service: UserService;
|
||||
|
||||
beforeEach(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [UserService],
|
||||
}).compile();
|
||||
|
||||
service = module.get<UserService>(UserService);
|
||||
});
|
||||
|
||||
it('should be defined', () => {
|
||||
expect(service).toBeDefined();
|
||||
});
|
||||
});
|
||||
44
api/src/user/services/user.service.ts
Normal file
44
api/src/user/services/user.service.ts
Normal file
@ -0,0 +1,44 @@
|
||||
import {Injectable, NotFoundException} from '@nestjs/common';
|
||||
import { InjectModel } from 'nestjs-typegoose';
|
||||
import { ModelType } from 'typegoose';
|
||||
import { User } from "../models/user.model"
|
||||
import {Form} from "../../form/models/form.model"
|
||||
|
||||
@Injectable()
|
||||
export class UserService {
|
||||
constructor(@InjectModel(User) private readonly userModel: ModelType<User>) {}
|
||||
|
||||
async findOneByIdentifier(identifier: string): Promise<User> {
|
||||
const results = await this.userModel.find().or([
|
||||
{
|
||||
username: identifier
|
||||
},
|
||||
{
|
||||
email: identifier
|
||||
}
|
||||
]).exec();
|
||||
|
||||
if (results.length !== 1) {
|
||||
throw new NotFoundException()
|
||||
}
|
||||
|
||||
return results[0]
|
||||
}
|
||||
|
||||
async findById(id: string): Promise<User> {
|
||||
return await this.userModel.findById(id).exec()
|
||||
}
|
||||
|
||||
async findOneBy(conditions): Promise<User> {
|
||||
return await this.userModel.findOne(conditions).exec()
|
||||
}
|
||||
|
||||
async findBy(conditions): Promise<User[]> {
|
||||
return await this.userModel.find(conditions).exec()
|
||||
}
|
||||
|
||||
async save(user: User): Promise<User> {
|
||||
let model = new this.userModel(user)
|
||||
return await model.save()
|
||||
}
|
||||
}
|
||||
5
api/src/user/user.controllers.ts
Normal file
5
api/src/user/user.controllers.ts
Normal file
@ -0,0 +1,5 @@
|
||||
import { UserController } from "./controllers/user.controller"
|
||||
|
||||
export default [
|
||||
UserController,
|
||||
]
|
||||
9
api/src/user/user.exports.ts
Normal file
9
api/src/user/user.exports.ts
Normal file
@ -0,0 +1,9 @@
|
||||
import { UserService } from "./services/user.service"
|
||||
import { UsernameAlreadyInUse } from "./validators/UsernameAlreadyInUse"
|
||||
import { EmailAlreadyInUse } from "./validators/EmailAlreadyInUse"
|
||||
|
||||
export default [
|
||||
UserService,
|
||||
UsernameAlreadyInUse,
|
||||
EmailAlreadyInUse,
|
||||
]
|
||||
16
api/src/user/user.module.ts
Normal file
16
api/src/user/user.module.ts
Normal file
@ -0,0 +1,16 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { TypegooseModule } from 'nestjs-typegoose';
|
||||
import providers from './user.providers'
|
||||
import exportList from './user.exports'
|
||||
import { User } from "./models/user.model"
|
||||
import controllers from './user.controllers'
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
TypegooseModule.forFeature([User]),
|
||||
],
|
||||
controllers,
|
||||
providers,
|
||||
exports: exportList,
|
||||
})
|
||||
export class UserModule {}
|
||||
9
api/src/user/user.providers.ts
Normal file
9
api/src/user/user.providers.ts
Normal file
@ -0,0 +1,9 @@
|
||||
import { UserService } from "./services/user.service"
|
||||
import { UsernameAlreadyInUse } from "./validators/UsernameAlreadyInUse"
|
||||
import { EmailAlreadyInUse } from "./validators/EmailAlreadyInUse"
|
||||
|
||||
export default [
|
||||
UserService,
|
||||
UsernameAlreadyInUse,
|
||||
EmailAlreadyInUse,
|
||||
]
|
||||
20
api/src/user/validators/EmailAlreadyInUse.ts
Normal file
20
api/src/user/validators/EmailAlreadyInUse.ts
Normal file
@ -0,0 +1,20 @@
|
||||
import { ValidationArguments, ValidatorConstraint } from "class-validator"
|
||||
import { Inject, Injectable } from "@nestjs/common"
|
||||
import { UserService } from "../services/user.service"
|
||||
|
||||
@ValidatorConstraint({ name: 'EmailAlreadyInUse', async: true })
|
||||
@Injectable()
|
||||
export class EmailAlreadyInUse {
|
||||
constructor(
|
||||
@Inject('UserService') private readonly userService: UserService,
|
||||
) {}
|
||||
|
||||
async validate(text: string) {
|
||||
const user = await this.userService.findOneBy({email: text});
|
||||
return !user;
|
||||
}
|
||||
|
||||
defaultMessage(args: ValidationArguments) {
|
||||
return 'User with this email already exists.';
|
||||
}
|
||||
}
|
||||
20
api/src/user/validators/UsernameAlreadyInUse.ts
Normal file
20
api/src/user/validators/UsernameAlreadyInUse.ts
Normal file
@ -0,0 +1,20 @@
|
||||
import { ValidationArguments, ValidatorConstraint } from "class-validator"
|
||||
import { Inject, Injectable } from "@nestjs/common"
|
||||
import { UserService } from "../services/user.service"
|
||||
|
||||
@ValidatorConstraint({ name: 'UsernameAlreadyInUse', async: true })
|
||||
@Injectable()
|
||||
export class UsernameAlreadyInUse {
|
||||
constructor(
|
||||
@Inject('UserService') private readonly userService: UserService,
|
||||
) {}
|
||||
|
||||
async validate(text: string) {
|
||||
const user = await this.userService.findOneBy({username: text});
|
||||
return !user;
|
||||
}
|
||||
|
||||
defaultMessage(args: ValidationArguments) {
|
||||
return 'User with this username already exists.';
|
||||
}
|
||||
}
|
||||
4
api/tsconfig.build.json
Normal file
4
api/tsconfig.build.json
Normal file
@ -0,0 +1,4 @@
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"exclude": ["node_modules", "test", "dist", "**/*spec.ts"]
|
||||
}
|
||||
15
api/tsconfig.json
Normal file
15
api/tsconfig.json
Normal file
@ -0,0 +1,15 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"module": "commonjs",
|
||||
"declaration": true,
|
||||
"removeComments": true,
|
||||
"emitDecoratorMetadata": true,
|
||||
"experimentalDecorators": true,
|
||||
"target": "es2017",
|
||||
"sourceMap": true,
|
||||
"outDir": "./dist",
|
||||
"baseUrl": "./",
|
||||
"incremental": true
|
||||
},
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
59
api/tslint.json
Normal file
59
api/tslint.json
Normal file
@ -0,0 +1,59 @@
|
||||
{
|
||||
"defaultSeverity": "error",
|
||||
"extends": [
|
||||
"tslint:recommended"
|
||||
],
|
||||
"jsRules": {
|
||||
"no-unused-expression": true
|
||||
},
|
||||
"rules": {
|
||||
"eofline": false,
|
||||
"quotemark": [
|
||||
true,
|
||||
"single"
|
||||
],
|
||||
"indent": false,
|
||||
"member-access": [
|
||||
false
|
||||
],
|
||||
"ordered-imports": [
|
||||
false
|
||||
],
|
||||
"max-line-length": [
|
||||
true,
|
||||
150
|
||||
],
|
||||
"member-ordering": [
|
||||
false
|
||||
],
|
||||
"curly": false,
|
||||
"interface-name": [
|
||||
false
|
||||
],
|
||||
"array-type": [
|
||||
false
|
||||
],
|
||||
"no-empty-interface": false,
|
||||
"no-empty": false,
|
||||
"arrow-parens": false,
|
||||
"object-literal-sort-keys": false,
|
||||
"no-unused-expression": false,
|
||||
"max-classes-per-file": [
|
||||
false
|
||||
],
|
||||
"variable-name": [
|
||||
false
|
||||
],
|
||||
"one-line": [
|
||||
false
|
||||
],
|
||||
"one-variable-per-declaration": [
|
||||
false
|
||||
],
|
||||
"whitespace": [
|
||||
true,
|
||||
"check-postbrace"
|
||||
]
|
||||
},
|
||||
"rulesDirectory": []
|
||||
}
|
||||
19
api/views/en/mail/auth/recover.hbs
Normal file
19
api/views/en/mail/auth/recover.hbs
Normal file
@ -0,0 +1,19 @@
|
||||
Password Reset Request
|
||||
---
|
||||
Hi,
|
||||
|
||||
if you have not requested a new password you can ignore this email. To set a new password for your account
|
||||
just follow this link: {{ recover }}
|
||||
|
||||
See you soon
|
||||
---
|
||||
<h1>Hi, </h1>
|
||||
|
||||
<p>
|
||||
if you have not requested a new password you can ignore this email. To set a new password for your account
|
||||
just follow this link: <a href="{{ recover }}">{{ recover }}</a>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
See you soon
|
||||
</p>
|
||||
14
api/views/en/mail/auth/register.hbs
Normal file
14
api/views/en/mail/auth/register.hbs
Normal file
@ -0,0 +1,14 @@
|
||||
Welcome to OhMyForm
|
||||
---
|
||||
Welcome to OhMyForm!
|
||||
|
||||
please confirm your account by following the following link: {{ confirm }}
|
||||
|
||||
enjoy!
|
||||
---
|
||||
<h1>Welcome to OhMyForm!</h1>
|
||||
<p>
|
||||
please confirm your account by following the following link: <a href="{{ confirm }}">{{ confirm }}</a>
|
||||
</p>
|
||||
|
||||
<p>enjoy!</p>
|
||||
@ -5,7 +5,8 @@
|
||||
*/
|
||||
var mongoose = require('mongoose'),
|
||||
Schema = mongoose.Schema,
|
||||
crypto = require('crypto'),
|
||||
crypto = require('crypto'),
|
||||
bcrypt = require('bcrypt'),
|
||||
config = require('../../config/config'),
|
||||
timeStampPlugin = require('../libs/timestamp.server.plugin'),
|
||||
path = require('path'),
|
||||
@ -109,29 +110,37 @@ UserSchema.virtual('password').get(function () {
|
||||
* Create instance method for hashing a password
|
||||
*/
|
||||
UserSchema.statics.hashPassword = UserSchema.methods.hashPassword = function(password) {
|
||||
var encoding = 'base64';
|
||||
var iterations = 10000;
|
||||
var keylen = 128;
|
||||
var size = 64;
|
||||
var digest = 'SHA1';
|
||||
|
||||
//Generate salt if it doesn't exist yet
|
||||
if(!this.salt){
|
||||
this.salt = crypto.randomBytes(size).toString(encoding);
|
||||
}
|
||||
|
||||
if (password) {
|
||||
return crypto.pbkdf2Sync(password, new Buffer(this.salt, encoding), iterations, keylen, digest).toString(encoding);
|
||||
} else {
|
||||
return password;
|
||||
}
|
||||
return bcrypt.hashSync(password, 4);
|
||||
};
|
||||
|
||||
/**
|
||||
* Create instance method for authenticating user
|
||||
*/
|
||||
UserSchema.methods.authenticate = function(password) {
|
||||
return this.password === this.hashPassword(password);
|
||||
if (this.password[0] === '$') {
|
||||
return bcrypt.compareSync(password, this.password);
|
||||
}
|
||||
|
||||
var encoding = 'base64';
|
||||
var iterations = 10000;
|
||||
var keylen = 128;
|
||||
var size = 64;
|
||||
var digest = 'SHA1';
|
||||
|
||||
//Generate salt if it doesn't exist yet
|
||||
if(!this.salt){
|
||||
this.salt = crypto.randomBytes(size).toString(encoding);
|
||||
}
|
||||
|
||||
var hash;
|
||||
|
||||
if (password) {
|
||||
hash = crypto.pbkdf2Sync(password, new Buffer(this.salt, encoding), iterations, keylen, digest).toString(encoding);
|
||||
} else {
|
||||
hash = password;
|
||||
}
|
||||
|
||||
return this.password === hash;
|
||||
};
|
||||
|
||||
/**
|
||||
@ -167,4 +176,4 @@ UserSchema.methods.isAdmin = function() {
|
||||
|
||||
mongoose.model('User', UserSchema);
|
||||
|
||||
module.exports = mongoose.model('User');
|
||||
module.exports = mongoose.model('User');
|
||||
|
||||
265
package-lock.json
generated
265
package-lock.json
generated
@ -1141,8 +1141,16 @@
|
||||
"aproba": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz",
|
||||
"integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==",
|
||||
"dev": true
|
||||
"integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw=="
|
||||
},
|
||||
"are-we-there-yet": {
|
||||
"version": "1.1.5",
|
||||
"resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-1.1.5.tgz",
|
||||
"integrity": "sha512-5hYdAkZlcG8tOLujVDTgCT+uPX0VnpAH28gWsLfzpXYm7wP6mp5Q/gYyR7YQ0cKVJcXJnl3j2kpBan13PtQf6w==",
|
||||
"requires": {
|
||||
"delegates": "^1.0.0",
|
||||
"readable-stream": "^2.0.6"
|
||||
}
|
||||
},
|
||||
"argparse": {
|
||||
"version": "1.0.10",
|
||||
@ -1536,6 +1544,22 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"bcrypt": {
|
||||
"version": "3.0.6",
|
||||
"resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-3.0.6.tgz",
|
||||
"integrity": "sha512-taA5bCTfXe7FUjKroKky9EXpdhkVvhE5owfxfLYodbrAR1Ul3juLmIQmIQBK4L9a5BuUcE6cqmwT+Da20lF9tg==",
|
||||
"requires": {
|
||||
"nan": "2.13.2",
|
||||
"node-pre-gyp": "0.12.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"nan": {
|
||||
"version": "2.13.2",
|
||||
"resolved": "https://registry.npmjs.org/nan/-/nan-2.13.2.tgz",
|
||||
"integrity": "sha512-TghvYc72wlMGMVMluVo9WRJc0mB8KxxF/gZ4YYFy7V2ZQX9l7rgbPg7vjS9mt6U5HXODVFVI2bOduCzwOMv/lw=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"bcrypt-pbkdf": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz",
|
||||
@ -2333,8 +2357,7 @@
|
||||
"chownr": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.2.tgz",
|
||||
"integrity": "sha512-GkfeAQh+QNy3wquu9oIZr6SS5x7wGdSgNQvD10X3r+AZr1Oys22HW8kAmDMvNg2+Dm0TeGaEuO8gFwdBXxwO8A==",
|
||||
"dev": true
|
||||
"integrity": "sha512-GkfeAQh+QNy3wquu9oIZr6SS5x7wGdSgNQvD10X3r+AZr1Oys22HW8kAmDMvNg2+Dm0TeGaEuO8gFwdBXxwO8A=="
|
||||
},
|
||||
"chrome-trace-event": {
|
||||
"version": "1.0.2",
|
||||
@ -2577,8 +2600,7 @@
|
||||
"code-point-at": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz",
|
||||
"integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=",
|
||||
"dev": true
|
||||
"integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c="
|
||||
},
|
||||
"coffeescript": {
|
||||
"version": "1.10.0",
|
||||
@ -2816,6 +2838,11 @@
|
||||
"date-now": "^0.1.4"
|
||||
}
|
||||
},
|
||||
"console-control-strings": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz",
|
||||
"integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4="
|
||||
},
|
||||
"consolidate": {
|
||||
"version": "0.14.5",
|
||||
"resolved": "https://registry.npmjs.org/consolidate/-/consolidate-0.14.5.tgz",
|
||||
@ -3455,6 +3482,11 @@
|
||||
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
|
||||
"integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk="
|
||||
},
|
||||
"delegates": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz",
|
||||
"integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o="
|
||||
},
|
||||
"depd": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz",
|
||||
@ -3481,6 +3513,11 @@
|
||||
"integrity": "sha1-8NZtA2cqglyxtzvbP+YjEMjlUrc=",
|
||||
"dev": true
|
||||
},
|
||||
"detect-libc": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz",
|
||||
"integrity": "sha1-+hN8S9aY7fVc1c0CrFWfkaTEups="
|
||||
},
|
||||
"di": {
|
||||
"version": "0.0.1",
|
||||
"resolved": "https://registry.npmjs.org/di/-/di-0.0.1.tgz",
|
||||
@ -5501,6 +5538,14 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"fs-minipass": {
|
||||
"version": "1.2.6",
|
||||
"resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-1.2.6.tgz",
|
||||
"integrity": "sha512-crhvyXcMejjv3Z5d2Fa9sf5xLYVCF5O1c71QxbVnbLsmYMBEvDAftewesN/HhY03YRoA7zOMxjNGrF5svGaaeQ==",
|
||||
"requires": {
|
||||
"minipass": "^2.2.1"
|
||||
}
|
||||
},
|
||||
"fs-write-stream-atomic": {
|
||||
"version": "1.0.10",
|
||||
"resolved": "https://registry.npmjs.org/fs-write-stream-atomic/-/fs-write-stream-atomic-1.0.10.tgz",
|
||||
@ -6115,6 +6160,21 @@
|
||||
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz",
|
||||
"integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A=="
|
||||
},
|
||||
"gauge": {
|
||||
"version": "2.7.4",
|
||||
"resolved": "https://registry.npmjs.org/gauge/-/gauge-2.7.4.tgz",
|
||||
"integrity": "sha1-LANAXHU4w51+s3sxcCLjJfsBi/c=",
|
||||
"requires": {
|
||||
"aproba": "^1.0.3",
|
||||
"console-control-strings": "^1.0.0",
|
||||
"has-unicode": "^2.0.0",
|
||||
"object-assign": "^4.1.0",
|
||||
"signal-exit": "^3.0.0",
|
||||
"string-width": "^1.0.1",
|
||||
"strip-ansi": "^3.0.1",
|
||||
"wide-align": "^1.1.0"
|
||||
}
|
||||
},
|
||||
"gaze": {
|
||||
"version": "1.1.3",
|
||||
"resolved": "https://registry.npmjs.org/gaze/-/gaze-1.1.3.tgz",
|
||||
@ -8323,6 +8383,11 @@
|
||||
"integrity": "sha1-uhqPGvKg/DllD1yFA2dwQSIGO0Q=",
|
||||
"dev": true
|
||||
},
|
||||
"has-unicode": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz",
|
||||
"integrity": "sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk="
|
||||
},
|
||||
"has-value": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/has-value/-/has-value-1.0.0.tgz",
|
||||
@ -8802,6 +8867,14 @@
|
||||
"integrity": "sha1-SMptcvbGo68Aqa1K5odr44ieKwk=",
|
||||
"dev": true
|
||||
},
|
||||
"ignore-walk": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/ignore-walk/-/ignore-walk-3.0.1.tgz",
|
||||
"integrity": "sha512-DTVlMx3IYPe0/JJcYP7Gxg7ttZZu3IInhuEhbchuqneY9wWe5Ojy2mXLBaQFUQmo0AW2r3qG7m1mg86js+gnlQ==",
|
||||
"requires": {
|
||||
"minimatch": "^3.0.4"
|
||||
}
|
||||
},
|
||||
"ignorefs": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/ignorefs/-/ignorefs-1.2.0.tgz",
|
||||
@ -9152,7 +9225,6 @@
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz",
|
||||
"integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"number-is-nan": "^1.0.0"
|
||||
}
|
||||
@ -11658,6 +11730,35 @@
|
||||
"resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz",
|
||||
"integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0="
|
||||
},
|
||||
"minipass": {
|
||||
"version": "2.5.0",
|
||||
"resolved": "https://registry.npmjs.org/minipass/-/minipass-2.5.0.tgz",
|
||||
"integrity": "sha512-9FwMVYhn6ERvMR8XFdOavRz4QK/VJV8elU1x50vYexf9lslDcWe/f4HBRxCPd185ekRSjU6CfYyJCECa/CQy7Q==",
|
||||
"requires": {
|
||||
"safe-buffer": "^5.1.2",
|
||||
"yallist": "^3.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"safe-buffer": {
|
||||
"version": "5.2.0",
|
||||
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.0.tgz",
|
||||
"integrity": "sha512-fZEwUGbVl7kouZs1jCdMLdt95hdIv0ZeHg6L7qPeciMZhZ+/gdesW4wgTARkrFWEpspjEATAzUGPG8N2jJiwbg=="
|
||||
},
|
||||
"yallist": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/yallist/-/yallist-3.0.3.tgz",
|
||||
"integrity": "sha512-S+Zk8DEWE6oKpV+vI3qWkaK+jSbIK86pCwe2IF/xwIpQ8jEuxpw9NyaGjmp9+BoJv5FV2piqCDcoCtStppiq2A=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"minizlib": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/minizlib/-/minizlib-1.2.1.tgz",
|
||||
"integrity": "sha512-7+4oTUOWKg7AuL3vloEWekXY2/D20cevzsrNT2kGWm+39J9hGTCBv8VI5Pm5lXZ/o3/mdR4f8rflAPhnQb8mPA==",
|
||||
"requires": {
|
||||
"minipass": "^2.2.1"
|
||||
}
|
||||
},
|
||||
"mississippi": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/mississippi/-/mississippi-3.0.0.tgz",
|
||||
@ -12394,6 +12495,31 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"needle": {
|
||||
"version": "2.4.0",
|
||||
"resolved": "https://registry.npmjs.org/needle/-/needle-2.4.0.tgz",
|
||||
"integrity": "sha512-4Hnwzr3mi5L97hMYeNl8wRW/Onhy4nUKR/lVemJ8gJedxxUyBLm9kkrDColJvoSfwi0jCNhD+xCdOtiGDQiRZg==",
|
||||
"requires": {
|
||||
"debug": "^3.2.6",
|
||||
"iconv-lite": "^0.4.4",
|
||||
"sax": "^1.2.4"
|
||||
},
|
||||
"dependencies": {
|
||||
"debug": {
|
||||
"version": "3.2.6",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz",
|
||||
"integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==",
|
||||
"requires": {
|
||||
"ms": "^2.1.1"
|
||||
}
|
||||
},
|
||||
"ms": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
|
||||
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"negotiator": {
|
||||
"version": "0.6.1",
|
||||
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.1.tgz",
|
||||
@ -12646,6 +12772,63 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node-pre-gyp": {
|
||||
"version": "0.12.0",
|
||||
"resolved": "https://registry.npmjs.org/node-pre-gyp/-/node-pre-gyp-0.12.0.tgz",
|
||||
"integrity": "sha512-4KghwV8vH5k+g2ylT+sLTjy5wmUOb9vPhnM8NHvRf9dHmnW/CndrFXy2aRPaPST6dugXSdHXfeaHQm77PIz/1A==",
|
||||
"requires": {
|
||||
"detect-libc": "^1.0.2",
|
||||
"mkdirp": "^0.5.1",
|
||||
"needle": "^2.2.1",
|
||||
"nopt": "^4.0.1",
|
||||
"npm-packlist": "^1.1.6",
|
||||
"npmlog": "^4.0.2",
|
||||
"rc": "^1.2.7",
|
||||
"rimraf": "^2.6.1",
|
||||
"semver": "^5.3.0",
|
||||
"tar": "^4"
|
||||
},
|
||||
"dependencies": {
|
||||
"deep-extend": {
|
||||
"version": "0.6.0",
|
||||
"resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz",
|
||||
"integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA=="
|
||||
},
|
||||
"minimist": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz",
|
||||
"integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ="
|
||||
},
|
||||
"nopt": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/nopt/-/nopt-4.0.1.tgz",
|
||||
"integrity": "sha1-0NRoWv1UFRk8jHUFYC0NF81kR00=",
|
||||
"requires": {
|
||||
"abbrev": "1",
|
||||
"osenv": "^0.1.4"
|
||||
}
|
||||
},
|
||||
"rc": {
|
||||
"version": "1.2.8",
|
||||
"resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz",
|
||||
"integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==",
|
||||
"requires": {
|
||||
"deep-extend": "^0.6.0",
|
||||
"ini": "~1.3.0",
|
||||
"minimist": "^1.2.0",
|
||||
"strip-json-comments": "~2.0.1"
|
||||
}
|
||||
},
|
||||
"rimraf": {
|
||||
"version": "2.7.1",
|
||||
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz",
|
||||
"integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==",
|
||||
"requires": {
|
||||
"glob": "^7.1.3"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"node-uuid": {
|
||||
"version": "1.4.8",
|
||||
"resolved": "https://registry.npmjs.org/node-uuid/-/node-uuid-1.4.8.tgz",
|
||||
@ -13746,6 +13929,20 @@
|
||||
"resolved": "https://registry.npmjs.org/notepack.io/-/notepack.io-2.1.3.tgz",
|
||||
"integrity": "sha512-AgSt+cP5XMooho1Ppn8NB3FFaVWefV+qZoZncYTUSch2GAEwlYLcIIbT5YVkMlFeNHnfwOvc4HDlbvrB5BRxXA=="
|
||||
},
|
||||
"npm-bundled": {
|
||||
"version": "1.0.6",
|
||||
"resolved": "https://registry.npmjs.org/npm-bundled/-/npm-bundled-1.0.6.tgz",
|
||||
"integrity": "sha512-8/JCaftHwbd//k6y2rEWp6k1wxVfpFzB6t1p825+cUb7Ym2XQfhwIC5KwhrvzZRJu+LtDE585zVaS32+CGtf0g=="
|
||||
},
|
||||
"npm-packlist": {
|
||||
"version": "1.4.4",
|
||||
"resolved": "https://registry.npmjs.org/npm-packlist/-/npm-packlist-1.4.4.tgz",
|
||||
"integrity": "sha512-zTLo8UcVYtDU3gdeaFu2Xu0n0EvelfHDGuqtNIn5RO7yQj4H1TqNdBc/yZjxnWA0PVB8D3Woyp0i5B43JwQ6Vw==",
|
||||
"requires": {
|
||||
"ignore-walk": "^3.0.1",
|
||||
"npm-bundled": "^1.0.1"
|
||||
}
|
||||
},
|
||||
"npm-run-path": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz",
|
||||
@ -13755,6 +13952,17 @@
|
||||
"path-key": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"npmlog": {
|
||||
"version": "4.1.2",
|
||||
"resolved": "https://registry.npmjs.org/npmlog/-/npmlog-4.1.2.tgz",
|
||||
"integrity": "sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==",
|
||||
"requires": {
|
||||
"are-we-there-yet": "~1.1.2",
|
||||
"console-control-strings": "~1.1.0",
|
||||
"gauge": "~2.7.3",
|
||||
"set-blocking": "~2.0.0"
|
||||
}
|
||||
},
|
||||
"number-is-nan": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz",
|
||||
@ -14053,8 +14261,7 @@
|
||||
"os-homedir": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz",
|
||||
"integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=",
|
||||
"dev": true
|
||||
"integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M="
|
||||
},
|
||||
"os-locale": {
|
||||
"version": "3.1.0",
|
||||
@ -14106,14 +14313,12 @@
|
||||
"os-tmpdir": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz",
|
||||
"integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=",
|
||||
"dev": true
|
||||
"integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ="
|
||||
},
|
||||
"osenv": {
|
||||
"version": "0.1.4",
|
||||
"resolved": "https://registry.npmjs.org/osenv/-/osenv-0.1.4.tgz",
|
||||
"integrity": "sha1-Qv5tWVPfBsgGS+bxdsPQWqqjRkQ=",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"os-homedir": "^1.0.0",
|
||||
"os-tmpdir": "^1.0.0"
|
||||
@ -16189,6 +16394,11 @@
|
||||
"sparse-bitfield": "^3.0.3"
|
||||
}
|
||||
},
|
||||
"sax": {
|
||||
"version": "1.2.4",
|
||||
"resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz",
|
||||
"integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw=="
|
||||
},
|
||||
"saxes": {
|
||||
"version": "3.1.11",
|
||||
"resolved": "https://registry.npmjs.org/saxes/-/saxes-3.1.11.tgz",
|
||||
@ -16310,8 +16520,7 @@
|
||||
"set-blocking": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz",
|
||||
"integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=",
|
||||
"dev": true
|
||||
"integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc="
|
||||
},
|
||||
"set-value": {
|
||||
"version": "2.0.0",
|
||||
@ -17068,7 +17277,6 @@
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz",
|
||||
"integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"code-point-at": "^1.0.0",
|
||||
"is-fullwidth-code-point": "^1.0.0",
|
||||
@ -17309,6 +17517,32 @@
|
||||
"integrity": "sha1-KcNXB8K3DlDQdIK10gLo7URtr9Q=",
|
||||
"dev": true
|
||||
},
|
||||
"tar": {
|
||||
"version": "4.4.10",
|
||||
"resolved": "https://registry.npmjs.org/tar/-/tar-4.4.10.tgz",
|
||||
"integrity": "sha512-g2SVs5QIxvo6OLp0GudTqEf05maawKUxXru104iaayWA09551tFCTI8f1Asb4lPfkBr91k07iL4c11XO3/b0tA==",
|
||||
"requires": {
|
||||
"chownr": "^1.1.1",
|
||||
"fs-minipass": "^1.2.5",
|
||||
"minipass": "^2.3.5",
|
||||
"minizlib": "^1.2.1",
|
||||
"mkdirp": "^0.5.0",
|
||||
"safe-buffer": "^5.1.2",
|
||||
"yallist": "^3.0.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"safe-buffer": {
|
||||
"version": "5.2.0",
|
||||
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.0.tgz",
|
||||
"integrity": "sha512-fZEwUGbVl7kouZs1jCdMLdt95hdIv0ZeHg6L7qPeciMZhZ+/gdesW4wgTARkrFWEpspjEATAzUGPG8N2jJiwbg=="
|
||||
},
|
||||
"yallist": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/yallist/-/yallist-3.0.3.tgz",
|
||||
"integrity": "sha512-S+Zk8DEWE6oKpV+vI3qWkaK+jSbIK86pCwe2IF/xwIpQ8jEuxpw9NyaGjmp9+BoJv5FV2piqCDcoCtStppiq2A=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"taskgroup": {
|
||||
"version": "4.3.1",
|
||||
"resolved": "https://registry.npmjs.org/taskgroup/-/taskgroup-4.3.1.tgz",
|
||||
@ -18635,7 +18869,6 @@
|
||||
"version": "1.1.3",
|
||||
"resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.3.tgz",
|
||||
"integrity": "sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"string-width": "^1.0.2 || 2"
|
||||
}
|
||||
|
||||
@ -22,6 +22,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"async": "^2.6.3",
|
||||
"bcrypt": "^3.0.6",
|
||||
"body-parser": "^1.19.0",
|
||||
"bower": "^1.8.8",
|
||||
"chalk": "^2.4.2",
|
||||
|
||||
13
ui/.editorconfig
Normal file
13
ui/.editorconfig
Normal file
@ -0,0 +1,13 @@
|
||||
# editorconfig.org
|
||||
root = true
|
||||
|
||||
[*]
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
end_of_line = lf
|
||||
charset = utf-8
|
||||
trim_trailing_whitespace = true
|
||||
insert_final_newline = true
|
||||
|
||||
[*.md]
|
||||
trim_trailing_whitespace = false
|
||||
23
ui/.eslintrc.js
Normal file
23
ui/.eslintrc.js
Normal file
@ -0,0 +1,23 @@
|
||||
module.exports = {
|
||||
root: true,
|
||||
env: {
|
||||
browser: true,
|
||||
node: true
|
||||
},
|
||||
parserOptions: {
|
||||
parser: 'babel-eslint'
|
||||
},
|
||||
extends: [
|
||||
'@nuxtjs',
|
||||
'plugin:nuxt/recommended',
|
||||
'plugin:prettier/recommended',
|
||||
'prettier',
|
||||
'prettier/vue'
|
||||
],
|
||||
plugins: [
|
||||
'prettier'
|
||||
],
|
||||
// add your custom rules here
|
||||
rules: {
|
||||
}
|
||||
}
|
||||
84
ui/.gitignore
vendored
Normal file
84
ui/.gitignore
vendored
Normal file
@ -0,0 +1,84 @@
|
||||
# Created by .ignore support plugin (hsz.mobi)
|
||||
### Node template
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
# Runtime data
|
||||
pids
|
||||
*.pid
|
||||
*.seed
|
||||
*.pid.lock
|
||||
|
||||
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||
lib-cov
|
||||
|
||||
# Coverage directory used by tools like istanbul
|
||||
coverage
|
||||
|
||||
# nyc test coverage
|
||||
.nyc_output
|
||||
|
||||
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
|
||||
.grunt
|
||||
|
||||
# Bower dependency directory (https://bower.io/)
|
||||
bower_components
|
||||
|
||||
# node-waf configuration
|
||||
.lock-wscript
|
||||
|
||||
# Compiled binary addons (https://nodejs.org/api/addons.html)
|
||||
build/Release
|
||||
|
||||
# Dependency directories
|
||||
node_modules/
|
||||
jspm_packages/
|
||||
|
||||
# TypeScript v1 declaration files
|
||||
typings/
|
||||
|
||||
# Optional npm cache directory
|
||||
.npm
|
||||
|
||||
# Optional eslint cache
|
||||
.eslintcache
|
||||
|
||||
# Optional REPL history
|
||||
.node_repl_history
|
||||
|
||||
# Output of 'npm pack'
|
||||
*.tgz
|
||||
|
||||
# Yarn Integrity file
|
||||
.yarn-integrity
|
||||
|
||||
# dotenv environment variables file
|
||||
.env
|
||||
|
||||
# parcel-bundler cache (https://parceljs.org/)
|
||||
.cache
|
||||
|
||||
# next.js build output
|
||||
.next
|
||||
|
||||
# nuxt.js build output
|
||||
.nuxt
|
||||
|
||||
# Nuxt generate
|
||||
dist
|
||||
|
||||
# vuepress build output
|
||||
.vuepress/dist
|
||||
|
||||
# Serverless directories
|
||||
.serverless
|
||||
|
||||
# IDE
|
||||
.idea
|
||||
|
||||
# Service worker
|
||||
sw.*
|
||||
4
ui/.prettierrc
Normal file
4
ui/.prettierrc
Normal file
@ -0,0 +1,4 @@
|
||||
{
|
||||
"semi": false,
|
||||
"singleQuote": true
|
||||
}
|
||||
14
ui/Dockerfile
Normal file
14
ui/Dockerfile
Normal file
@ -0,0 +1,14 @@
|
||||
FROM node:10 AS builder
|
||||
MAINTAINER OhMyForm <admin@ohmyform.com>
|
||||
|
||||
WORKDIR /opt/app
|
||||
|
||||
# just copy everhing
|
||||
COPY . .
|
||||
|
||||
RUN yarn install --frozen-lockfile
|
||||
RUN yarn build
|
||||
|
||||
FROM nginx
|
||||
|
||||
COPY --from=builder /opt/app/dist /var/share/nginx/html
|
||||
26
ui/README.md
Normal file
26
ui/README.md
Normal file
@ -0,0 +1,26 @@
|
||||
# OhMyForm
|
||||
|
||||
> Opensource alternative to TypeForm
|
||||
|
||||
## Design
|
||||
|
||||
https://github.com/ohmyform/ohmyform/issues/13
|
||||
|
||||
## Build Setup
|
||||
|
||||
``` bash
|
||||
# install dependencies
|
||||
$ yarn install
|
||||
|
||||
# serve with hot reload at localhost:3000
|
||||
$ yarn run dev
|
||||
|
||||
# build for production and launch server
|
||||
$ yarn run build
|
||||
$ yarn start
|
||||
|
||||
# generate static project
|
||||
$ yarn run generate
|
||||
```
|
||||
|
||||
For detailed explanation on how things work, checkout [Nuxt.js docs](https://nuxtjs.org).
|
||||
56
ui/assets/css/base.scss
Normal file
56
ui/assets/css/base.scss
Normal file
@ -0,0 +1,56 @@
|
||||
$accent: #fae596;
|
||||
$primary: #3fb0ac;
|
||||
|
||||
@import url('https://fonts.googleapis.com/icon?family=Material+Icons');
|
||||
|
||||
1html {
|
||||
font-family: 'Source Sans Pro', -apple-system, BlinkMacSystemFont, 'Segoe UI',
|
||||
Roboto, 'Helvetica Neue', Arial, sans-serif;
|
||||
font-size: 16px;
|
||||
word-spacing: 1px;
|
||||
-ms-text-size-adjust: 100%;
|
||||
-webkit-text-size-adjust: 100%;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.bg-primary {
|
||||
&.navbar-dark,
|
||||
&.dark {
|
||||
background-color: #173e43 !important;
|
||||
}
|
||||
|
||||
&.dark {
|
||||
a {
|
||||
color: #dddfd4;
|
||||
|
||||
&:hover {
|
||||
color: #fae596;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
color: #fff;
|
||||
background-color: $primary;
|
||||
border-color: $primary;
|
||||
|
||||
&:hover {
|
||||
color: #fff;
|
||||
background-color: darken($primary, 10%);
|
||||
border-color: darken($primary, 10%);
|
||||
}
|
||||
|
||||
&:not(:disabled):not(.disabled):active {
|
||||
color: #fff;
|
||||
background-color: darken($primary, 10%);
|
||||
border-color: darken($primary, 10%);
|
||||
}
|
||||
|
||||
&:not(:disabled):not(.disabled):active:focus,
|
||||
&:focus {
|
||||
box-shadow: 0 0 0 0.2rem transparentize($primary, 0.5);
|
||||
}
|
||||
}
|
||||
BIN
ui/assets/img/logo_white.png
Normal file
BIN
ui/assets/img/logo_white.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 59 KiB |
BIN
ui/assets/img/logo_white_small.png
Normal file
BIN
ui/assets/img/logo_white_small.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 33 KiB |
57
ui/layouts/admin.vue
Normal file
57
ui/layouts/admin.vue
Normal file
@ -0,0 +1,57 @@
|
||||
<template>
|
||||
<div>
|
||||
<b-navbar variant="primary" toggleable="sm" type="dark">
|
||||
<b-navbar-brand href="https://ohmyform.com">
|
||||
<img src="../assets/img/logo_white_small.png" style="height: 27px" />
|
||||
</b-navbar-brand>
|
||||
|
||||
<b-navbar-toggle target="nav-collapse"></b-navbar-toggle>
|
||||
|
||||
<b-collapse id="nav-collapse" is-nav>
|
||||
<b-navbar-nav>
|
||||
<b-nav-item
|
||||
:active="/\/admin\/forms/.test($route.fullPath)"
|
||||
to="/admin/forms"
|
||||
>
|
||||
Forms
|
||||
</b-nav-item>
|
||||
<b-nav-item
|
||||
:active="/\/admin\/users/.test($route.fullPath)"
|
||||
to="/admin/users"
|
||||
>
|
||||
Users
|
||||
</b-nav-item>
|
||||
<b-nav-item
|
||||
:active="/\/admin\/configuration/.test($route.fullPath)"
|
||||
to="/admin/configuration"
|
||||
>
|
||||
Configuration
|
||||
</b-nav-item>
|
||||
</b-navbar-nav>
|
||||
|
||||
<b-navbar-nav class="ml-auto">
|
||||
<b-nav-item-dropdown right>
|
||||
<template slot="button-content">
|
||||
<font-awesome-icon icon="user-circle" />
|
||||
</template>
|
||||
<b-dropdown-item to="/admin/me">Profile</b-dropdown-item>
|
||||
<b-dropdown-item @click="logout">Sign Out</b-dropdown-item>
|
||||
</b-nav-item-dropdown>
|
||||
</b-navbar-nav>
|
||||
</b-collapse>
|
||||
</b-navbar>
|
||||
<nuxt />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
methods: {
|
||||
async logout() {
|
||||
await this.$auth.logout()
|
||||
|
||||
this.$router.push('/login')
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
5
ui/layouts/default.vue
Normal file
5
ui/layouts/default.vue
Normal file
@ -0,0 +1,5 @@
|
||||
<template>
|
||||
<div>
|
||||
<nuxt />
|
||||
</div>
|
||||
</template>
|
||||
40
ui/layouts/screen.vue
Normal file
40
ui/layouts/screen.vue
Normal file
@ -0,0 +1,40 @@
|
||||
<template>
|
||||
<div class="screen bg-primary dark">
|
||||
<div class="content">
|
||||
<nuxt />
|
||||
</div>
|
||||
|
||||
<div class="footer">
|
||||
<nuxt-link to="/login">Login</nuxt-link>
|
||||
<nuxt-link to="/register">Register</nuxt-link>
|
||||
<nuxt-link to="/admin">Manage</nuxt-link>
|
||||
<a href="https://ohmyform.com">OhMyForm</a>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.screen {
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.content {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.footer {
|
||||
height: 40px;
|
||||
text-align: center;
|
||||
|
||||
a {
|
||||
padding-right: 16px;
|
||||
padding-left: 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
106
ui/nuxt.config.js
Normal file
106
ui/nuxt.config.js
Normal file
@ -0,0 +1,106 @@
|
||||
import pkg from './package'
|
||||
|
||||
export default {
|
||||
mode: 'spa',
|
||||
|
||||
/*
|
||||
** Headers of the page
|
||||
*/
|
||||
head: {
|
||||
title: pkg.name,
|
||||
meta: [
|
||||
{ charset: 'utf-8' },
|
||||
{ name: 'viewport', content: 'width=device-width, initial-scale=1' },
|
||||
{ hid: 'description', name: 'description', content: pkg.description }
|
||||
],
|
||||
link: [{ rel: 'icon', type: 'image/png', href: '/favicon.png' }]
|
||||
},
|
||||
|
||||
proxy: {
|
||||
'/api': { target: 'http://localhost:3000', pathRewrite: { '/api/': '/' } }
|
||||
},
|
||||
|
||||
router: {
|
||||
middleware: ['auth']
|
||||
},
|
||||
|
||||
server: {
|
||||
port: 3100
|
||||
},
|
||||
|
||||
auth: {
|
||||
strategies: {
|
||||
local: {
|
||||
endpoints: {
|
||||
login: { url: '/api/auth/login', method: 'post', propertyName: 'accessToken' },
|
||||
logout: { url: '/api/auth/logout', method: 'post' },
|
||||
user: false
|
||||
},
|
||||
tokenRequired: true,
|
||||
tokenType: 'Bearer'
|
||||
}
|
||||
},
|
||||
|
||||
redirect: {
|
||||
login: '/login',
|
||||
logout: '/',
|
||||
home: '/admin'
|
||||
}
|
||||
},
|
||||
|
||||
/*
|
||||
** Customize the progress-bar color
|
||||
*/
|
||||
loading: { color: '#fff' },
|
||||
|
||||
/*
|
||||
** Global CSS
|
||||
*/
|
||||
css: [
|
||||
'@/assets/css/base.scss'
|
||||
],
|
||||
|
||||
/*
|
||||
** Plugins to load before mounting the App
|
||||
*/
|
||||
plugins: [
|
||||
'@/plugins/font-awesome.js',
|
||||
'@/plugins/fab.js'
|
||||
],
|
||||
|
||||
/*
|
||||
** Nuxt.js modules
|
||||
*/
|
||||
modules: [
|
||||
'@nuxtjs/auth',
|
||||
'@nuxtjs/proxy',
|
||||
'@nuxtjs/axios',
|
||||
'bootstrap-vue/nuxt'
|
||||
],
|
||||
/*
|
||||
** Axios module configuration
|
||||
*/
|
||||
axios: {
|
||||
// See https://github.com/nuxt-community/axios-module#options
|
||||
},
|
||||
|
||||
/*
|
||||
** Build configuration
|
||||
*/
|
||||
build: {
|
||||
/*
|
||||
** You can extend webpack config here
|
||||
*/
|
||||
extend(config, ctx) {
|
||||
// Run ESLint on save
|
||||
if (ctx.isDev && ctx.isClient) {
|
||||
config.module.rules.push({
|
||||
enforce: 'pre',
|
||||
test: /\.(js|vue)$/,
|
||||
loader: 'eslint-loader',
|
||||
exclude: /(node_modules)/
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
48
ui/package.json
Normal file
48
ui/package.json
Normal file
@ -0,0 +1,48 @@
|
||||
{
|
||||
"name": "OhMyForm",
|
||||
"version": "1.0.0",
|
||||
"description": "Opensource alternative to TypeForm",
|
||||
"author": "Michael Schramm",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "nuxt",
|
||||
"build": "nuxt build",
|
||||
"start": "nuxt start",
|
||||
"generate": "nuxt generate",
|
||||
"lint": "eslint --ext .js,.vue --ignore-path .gitignore .",
|
||||
"precommit": "npm run lint"
|
||||
},
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome-svg-core": "^1.2.22",
|
||||
"@fortawesome/free-solid-svg-icons": "^5.10.2",
|
||||
"@fortawesome/vue-fontawesome": "^0.1.7",
|
||||
"@nuxtjs/auth": "^4.8.1",
|
||||
"@nuxtjs/axios": "^5.3.6",
|
||||
"@nuxtjs/proxy": "^1.3.3",
|
||||
"bootstrap": "^4.1.3",
|
||||
"bootstrap-vue": "^2.0.0-rc.11",
|
||||
"cross-env": "^5.2.0",
|
||||
"node-sass": "^4.12.0",
|
||||
"nuxt": "^2.4.0",
|
||||
"sass-loader": "^8.0.0",
|
||||
"vue-fab": "^2.3.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@nuxtjs/eslint-config": "^0.0.1",
|
||||
"babel-eslint": "^10.0.1",
|
||||
"eslint": "^5.15.1",
|
||||
"eslint-config-prettier": "^4.1.0",
|
||||
"eslint-config-standard": ">=12.0.0",
|
||||
"eslint-loader": "^2.1.2",
|
||||
"eslint-plugin-import": ">=2.16.0",
|
||||
"eslint-plugin-jest": ">=22.3.0",
|
||||
"eslint-plugin-node": ">=8.0.1",
|
||||
"eslint-plugin-nuxt": ">=0.4.2",
|
||||
"eslint-plugin-prettier": "^3.0.1",
|
||||
"eslint-plugin-promise": ">=4.0.1",
|
||||
"eslint-plugin-standard": ">=4.0.0",
|
||||
"eslint-plugin-vue": "^5.2.2",
|
||||
"nodemon": "^1.18.9",
|
||||
"prettier": "^1.16.4"
|
||||
}
|
||||
}
|
||||
11
ui/pages/admin/configuration/index.vue
Normal file
11
ui/pages/admin/configuration/index.vue
Normal file
@ -0,0 +1,11 @@
|
||||
<template>
|
||||
<div>
|
||||
config XD
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
layout: 'admin'
|
||||
}
|
||||
</script>
|
||||
22
ui/pages/admin/forms/_id.vue
Normal file
22
ui/pages/admin/forms/_id.vue
Normal file
@ -0,0 +1,22 @@
|
||||
<template>
|
||||
<div>Form: {{ form }}</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
layout: 'admin',
|
||||
data() {
|
||||
return {
|
||||
form: null
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.load()
|
||||
},
|
||||
methods: {
|
||||
async load() {
|
||||
this.form = await this.$axios.$get(`/api/forms/${this.$route.params.id}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
46
ui/pages/admin/forms/index.vue
Normal file
46
ui/pages/admin/forms/index.vue
Normal file
@ -0,0 +1,46 @@
|
||||
<template>
|
||||
<div>
|
||||
<b-alert show variant="info" class="m-3">
|
||||
All created forms, they are publicly visible if live is true
|
||||
</b-alert>
|
||||
|
||||
<fab bg-color="#173e43" />
|
||||
<b-table striped hover :items="provider" :fields="fields">
|
||||
<template slot="[menu]" slot-scope="data">
|
||||
<nuxt-link :to="'/admin/forms/' + data.item.id">Open</nuxt-link>
|
||||
</template>
|
||||
</b-table>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
layout: 'admin',
|
||||
data() {
|
||||
return {
|
||||
fields: [
|
||||
{
|
||||
key: 'title'
|
||||
},
|
||||
{
|
||||
key: 'created'
|
||||
},
|
||||
{
|
||||
key: 'live'
|
||||
},
|
||||
{
|
||||
key: 'responses'
|
||||
},
|
||||
{
|
||||
key: 'menu'
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
provider(ctx) {
|
||||
return this.$axios.$get('/api/forms')
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
11
ui/pages/admin/index.vue
Normal file
11
ui/pages/admin/index.vue
Normal file
@ -0,0 +1,11 @@
|
||||
<template>
|
||||
<div>
|
||||
ADMIN
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
layout: 'admin'
|
||||
}
|
||||
</script>
|
||||
11
ui/pages/admin/me.vue
Normal file
11
ui/pages/admin/me.vue
Normal file
@ -0,0 +1,11 @@
|
||||
<template>
|
||||
<div>
|
||||
My Profile XD
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
layout: 'admin'
|
||||
}
|
||||
</script>
|
||||
22
ui/pages/admin/users/_id.vue
Normal file
22
ui/pages/admin/users/_id.vue
Normal file
@ -0,0 +1,22 @@
|
||||
<template>
|
||||
<div>User: {{ user }}</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
layout: 'admin',
|
||||
data() {
|
||||
return {
|
||||
user: null
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.load()
|
||||
},
|
||||
methods: {
|
||||
async load() {
|
||||
this.user = await this.$axios.$get(`/api/users/${this.$route.params.id}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user